feat: independently publish server SSH keys with safe sync and Worker homepage

This commit is contained in:
2026-10-12 23:11:42 +08:00
parent f37cb51e06
commit 5798bf8900
29 changed files with 4943 additions and 525 deletions
+180 -83
View File
@@ -1,98 +1,195 @@
# Quickly Configuring SSH Server
快速地将 SSH 密钥部署到服务器中,并定期更新。
这个脚本可以帮你做到这些事情:
- 快速配置服务器的 SSH Server;
- 定期自动更新 SSH 公钥到服务器中;
- 集中管理注册到服务器的 SSH 公钥(例如使用 Github 管理 SSH 公钥);
在 Git 仓库中独立管理服务器 SSH 公钥,通过 Cloudflare Worker 发布,再由服务器定期同步。
- **优先使用本仓库的 `keys/authorized_keys`**;主清单为空时,可按配置顺序读取备用 HTTPS 公钥源。没有默认 GitHub 账号 SSH Keys 来源。
- 网页、脚本与仓库公钥一起构建发布,不再依赖 GitHub Pages;备用公钥独立标记来源和内容摘要。
- 首页支持浅色 / 深色 / 跟随系统、手机与宽屏,以及 GitHub 风格的代码块复制按钮。
- POSIX Shell 客户端只替换自己的密钥管理区块,保留手工密钥;下载或校验失败保留上次成功的内容。
> 仓库初始没有服务器公钥,生产构建会拒绝空清单。先填写你独立管理的服务器**公钥**或显式配置备用源;不要上传私钥,也不要直接拿测试夹具上线。
## Quick Usage
部署密钥并设置定期更新:
```bash
curl -s https://ssh.lamgc.me | bash -s -- -c
```
如果不希望自动更新密钥,可以移除 `-c` 参数:
```bash
curl -s https://ssh.lamgc.me | bash -s
```
跳过检查 SSH Server,只需要安装秘钥的话,就需要单独使用 `-o` 参数:
```bash
curl -s https://ssh.lamgc.me | bash -s -- -o
```
添加 `-p yes` 参数可以设置允许 Root 用户使用密码登录 SSH(需使用 Root 用户执行脚本):
```bash
curl -s https://ssh.lamgc.me | bash -s -- -p yes
```
要查看脚本的帮助信息,请使用 `-h` 参数:
```bash
curl -s https://ssh.lamgc.me | bash -s -- -h
以下地址是部署配置中的示例。Fork 后先修改 `deployment.json`,填写公钥并完成自己的 Worker 部署,再使用自己的域名。执行前建议下载并检查脚本。
安装公钥并设置定时同步:
```sh
curl -fsSL 'https://ssh.lamgc.me/script.sh' | sh -s -- -c
```
> Note: 更多用法请参见脚本帮助信息~
新安装默认每 15 分钟同步一次;已有的有效自定义计划会保留。只更新一次:
```sh
curl -fsSL 'https://ssh.lamgc.me/script.sh' | sh -s -- -o
```
预览、帮助与本地状态:
```sh
curl -fsSL 'https://ssh.lamgc.me/script.sh' | sh -s -- -o --dry-run
```
```sh
curl -fsSL 'https://ssh.lamgc.me/script.sh' | sh -s -- -h
```
```sh
sh "$HOME/.conf-sshd/conf-sshd.sh" --status
```
指定计划、取消计划、单独更新已安装脚本:
```sh
sh "$HOME/.conf-sshd/conf-sshd.sh" -c '0 */6 * * *'
```
```sh
sh "$HOME/.conf-sshd/conf-sshd.sh" -c false
```
```sh
sh "$HOME/.conf-sshd/conf-sshd.sh" -u
```
卸载定时同步与脚本,保留密钥及 SSH 服务配置:
```sh
sh "$HOME/.conf-sshd/conf-sshd.sh" --uninstall
```
这些命令用途不同,按需单独执行。网页只提供逐块复制,不把安装、卸载、密码策略合成“执行全部”。
## 公钥清单与发布
将普通 OpenSSH 公钥逐行放入 `keys/authorized_keys`,可附注释和空行。构建会规范化换行与编码,逐条调用 `ssh-keygen` 校验,拒绝重复钥匙、私钥、无效编码和未知行。
支持 ED25519、RSA(至少 2048 位)、ECDSA nistp256/384/521 和 OpenSSH 安全密钥类型。第一版不接受 DSA、证书或 `command=`、`environment=` 等 authorized_keys 选项。私钥应始终保留在你的客户端或安全密钥设备上。
配置文件 `deployment.json`:
- `baseUrl`:Worker 的正式 HTTPS origin,不带路径或末尾斜杠。
- `repositoryUrl`:自己的项目仓库地址。
- `defaultCron`:新安装的默认同步计划,默认 `*/15 * * * *`。
- `allowEmpty`:默认 `false`;明确设为 `true` 时,空主清单表示撤销全部,禁止回退到备用源。
- `fallbackSources`:默认空数组。可填写最多 5 个公开 HTTPS URL,按顺序尝试;不能带凭据、查询参数或片段,不接受请求端传入的地址。
页面展示的是**当前已发布版本**,不是每台服务器的实时同步状态。提交合并后还要等待部署成功;服务器会在下一次成功同步时应用。
### 有序备用源
只有仓库清单为空,且 `allowEmpty` 不是 `true` 时,Worker 才依次读取 `fallbackSources`。取第一个成功且有效的非空公钥集合,不聚合多个源。仓库清单非空始终优先;明确撤销全部时不调用任何备用源。
每个备用源有 3 秒超时(含正文读取)、1 MiB 正文限制、最多 1000 个公钥,并拒绝重定向。非 2xx、空正文、无效 UTF-8、私钥/选项/证书、损坏密钥或重复密钥会跳过并尝试下一个。全部失败时 `/ssh.keys` 和 `/release.json` 返回 503,首页说明暂不可用;客户端保留上次成功的密钥。
不设置 Worker 备用公钥缓存,不使用陈旧内容兜底;每个相关请求重新获取,要求上游也正确提供当前数据。首页的一次响应中,公钥、复制内容、来源、数量与内容摘要来自同一个结果。不同时间请求可能看到备用源的不同版本,因此页面分别显示代码修订与公钥摘要,不能只用 Git 提交号代表动态公钥版本。
撤销备用源公钥取决于上游更新、下一次成功请求和服务器同步周期;这不是即时断开连接的机制。主清单为空、未允许明确撤销且未配置备用源时,生产构建仍会拒绝。
### 明确撤销全部管理密钥
确实需要删除全部受管理密钥时,在同一提交中清空公钥清单,并显式将 `allowEmpty` 改为 `true`。发布与新客户端都会检查数量、版本和内容摘要,只有合法的明确空清单才会清空管理区块。
先确认其他恢复登录方式可用。手工放在管理区块之外的密钥不会删除;同一把钥匙若还存在于其他 authorized_keys 来源,仍可能允许登录。删除密钥也不会终止已有 SSH 会话。
网络故障、错误正文、未知协议或校验失败都会保留本地旧密钥并报错。因此断网时不能保证及时撤权。旧版本客户端不支持明确空清单撤权,应先升级。
### 安全边界
服务器密钥与 GitHub 账号认证密钥相互独立。在仓库里保存公钥不会把它注册为 GitHub 账号登录凭据。
但能修改并发布这个项目的人,以及被配置为备用公钥源的发布者,都能改变使用它的服务器的授权密钥。请保护仓库主分支、CI、Cloudflare 账号和部署权限。HTTPS 保护传输,SHA-256 用于检测截断或版本不一致,**不能抵抗发布源本身被攻陷**。`curl | sh` 仍意味着信任发布的可执行脚本;定时任务只拉公钥,不会自动更新脚本。
## 安装与部署
### 前提
构建环境需要 Node.js 22+、npm、Git、OpenSSH 客户端工具(`ssh-keygen`)及 POSIX `sh`。依赖固定在 `package-lock.json`。
客户端需要 `sh`、`curl`、`ssh-keygen`、`awk`、`mktemp`、`stat` 和常用 Unix 工具,以及 `sha256sum` / `shasum` / `openssl` 之一;设置定时同步还需 `crontab`。请自行安装所需软件。脚本不安装包、不启动或重载服务;`-n` 保留为兼容参数。
目标为 Linux 上的 dash、Bash 的 sh 用法和 BusyBox ash。真实发行版服务管理、SELinux 标签或自定义 `AuthorizedKeysFile` 不应仅凭单元测试视为已验证。默认目标文件是当前用户的 `$HOME/.ssh/authorized_keys`;自定义 SSH 授权路径需要人工核对。
### 一次性配置
1. Fork / 克隆仓库,填入自己的服务器公钥和 `deployment.json`。
2. 按需要修改 `cf-worker/wrangler.toml` 中的 Worker 名称,并为 `baseUrl` 配置 Worker 自定义域名。无需启用 GitHub Pages。
3. 安装依赖、运行检查、构建生产产物:
```sh
npm ci
npm test
npm run build
node scripts/verify-release.mjs
```
4. 在 GitHub 的生产环境中配置最小权限的 Cloudflare 部署 token 和账号 ID,对应 `CLOUDFLARE_API_TOKEN`、`CLOUDFLARE_ACCOUNT_ID`。凭据只存放在 Secrets,不能提交到代码或公钥清单。
5. 准备好正式公钥、域名和权限后,将仓库变量 `ENABLE_WORKER_DEPLOY` 设为 `true`。此后 main 的更新会运行生产发布流程,也可手动触发。
默认关闭自动生产发布,防止合并本次改动时意外上线。PR 验证不会读取部署凭据或发布 Worker。
生产流程先测试和严格构建,再部署,最后核对 `/release.json` 和 `/ssh.keys` 的版本与摘要。没有正式密钥、没有备用源且未显式允许空清单时会失败;不会偷偷改用 GitHub Keys 或测试密钥。
发布脚本、网页、仓库公钥和备用源配置属于同一构建产物;动态备用公钥有单独的内容摘要。若要回滚历史版本,先查看公钥差异:回滚可能重新启用已撤销的密钥。部署失败保留旧发布,但不能把这描述为新密钥已生效。
## Paths
获取 SSH 公钥:
```
https://ssh.lamgc.me/ssh.keys
| 路径 | 内容 |
| --- | --- |
| `/` | 浏览器首页;旧 curl/libcurl 请求仍返回脚本(显式请求 HTML 除外) |
| `/script.sh` | Shell 脚本,推荐所有新命令使用这个明确地址 |
| `/ssh.keys` | 带注释元数据的普通公钥文本 |
| `/release.json` | 当前代码版本、有效公钥来源、数量、摘要和空清单状态 |
动态入口不缓存旧数据;带内容哈希的静态 CSS / JS 可以长期缓存。未知路径返回 404,不能用首页 HTML 代替密钥或脚本错误。
## 客户端保护措施
- 所有参数先解析;未知选项、缺参数或冲突用途会报错。
- 下载具有 HTTPS 限制、HTTP 失败检测、超时、有限重试和大小限制。
- 响应包含协议版本、Git revision、密钥数量、明确空清单标记和 SHA-256;所有公钥逐条校验。
- 每个用户独占运行锁;临时文件使用不可预测名称、严格权限和异常清理。
- 只识别唯一且配对的原有 BEGIN / END 管理标记,损坏或重复时停止,避免误吞手工密钥。
- 同目录原子替换,写入前保存一个受限备份;重复执行内容相同时不重写文件。
- 拒绝符号链接、不合适的所有权/写权限和异常文件类型。
- `-c` 只管理自己的 cron 区块;原版已知命令可迁移,无法可靠判断的记录会停止并提示。
- 正常密钥同步不会修改 `sshd_config`,也不会重载 SSH 服务。
如果运行被不可捕获地终止,锁可能残留。先检查 `.conf-sshd/lock/pid` 对应的进程及工作状态,再人工处理锁,不能仅凭锁的年龄删除。
### Root 密码登录(高级)
保留 `-p yes` / `-p no`,必须单独显式使用并以 root 运行。不属于安装/同步默认流程。它只设置 `PermitRootLogin`,不会替你打开其他认证策略。
脚本会检查原配置、生成临时配置、验证语法与生效值、备份后替换,**不会自动 reload/restart**。发现 Include / Match 等无法安全判断的组合时,会拒绝修改并请你人工处理。应用服务变更前应保留恢复会话并测试新连接。
## 从旧版本迁移
1. 先建立独立服务器公钥清单并发布新 Worker;或显式配置备用源;不会自动导入 GitHub 账号公钥。
2. 检查页面公钥、版本和 `/ssh.keys`,确认是你的服务器密钥。
3. 显式更新已安装脚本,或从新的 `/script.sh` 下载并检查后重新运行 `-c`。
4. 验证自定义 cron 仍保留,手工密钥不变,另开 SSH 会话验证登录。
旧客户端对非空的 `/ssh.keys` 仍有基本格式兼容,但不具备新验证、明确空清单撤权与其他保护。不要因为 Worker 更新了就认为所有客户端已升级。
## 开发与测试
```sh
npm ci
npm test
npm run build:test
npx wrangler deploy --dry-run --config cf-worker/wrangler.toml
```
获取 Shell 脚本:
```
https://ssh.lamgc.me/script.sh
```
`build:test` 只用于测试/预览,使用公开的 RFC 测试公钥,并在产物中标记 `testFixture: true`。生产验证脚本拒绝该产物;生产构建只使用仓库公钥和显式配置的备用源,不允许回退到测试夹具。
获取 Shell 脚本(使用 Curl 访问):
```
https://ssh.lamgc.me/
```
测试覆盖构建配置与注入边界、逐条密钥验证、Worker 路由/缓存、主题/复制交互、Shell 参数/管理区块/失败保留/cron/自更新等。Shell 测试只在临时 HOME、假命令和测试文件中运行,不操作真实 SSH 配置或服务。
访问本页面(不使用 Curl 访问):
```
https://ssh.lamgc.me/
```
`npm run build:test` 之后若要部署正式版本,必须重新执行 `npm run build` 与 `node scripts/verify-release.mjs`。不得直接部署测试产物。
## Install
### Configuration
先 Fork 本仓库,Fork 后在仓库设置中启用 Github Pages,分支指向 main 分支,目录为仓库根目录。
配置好后将仓库克隆到本地,然后修改 `cf-worker/src/index.js` 中的前五个变量:
```javascript
// 改成你自己的 Github 用户名,注意是登录 Github 的那个用户名.
const githubUserName = "LamGC";
// 改成你 Fork 后的仓库名,记得要开启 Github Pages 功能.
const githubInstSshProjectName = "quickly-conf-sshd";
// 如果可以,建议在此设置备用的 SSH 公钥, 以防 Github 无法使用.
const backupSshKeys = ``;
// Worker 的访问地址, 如果不填的话默认为请求的地址, 填了就会用这里的地址(要去 Worker 的触发器那绑定, 否则无效).
const defaultBaseUrl = "";
// Cron 表达式, 默认 1 天执行一次更新.
const cronExpression = "0 0 * * *";
```
## License
### Use Wrangler
如果你安装了 Cloudflare 的 Wrangler,可以直接在 `cf-worker` 目录中执行:
```bash
wrangler publish
```
执行后将会自动创建 Worker 并上传 worker js 代码,Worker 的名字是 `quickly-conf-ssh-worker`。
Apache-2.0,见 [LICENSE](LICENSE)。
### Manual Install
如果你没有安装 Wrangler,那就手动在 Cloudflare Workers 中新建一个 Worker,名字你喜欢就好(也可以使用 `quickly-conf-ssh-worker`),选择 HTTP 处理程序,然后下一步;
![Create a new worker](docs/Create-a-new-worker.png)
创建完成后点击 Worker 名称右边的**快速编辑**,将 `cf-worker/src/index.js` 中的内容复制到编辑框中,把原本的代码覆盖掉,然后保存;
![Copy Code](docs/copy-js-code-to-worker.png)
保存后需要访问一下 Worker,检查是否有误(如果不检查仔细,把其他人的 SSH 密钥设置到服务器里了,那就寄咯):
- 直接访问 Worker 地址,看看是否能跳转到你 Fork 的仓库中(地址栏显示了 `https://{你的 Github 用户名}.github.io/{Fork的仓库名}/`);
- 访问 `{Worker地址}/ssh.keys`,应该显示你的在 Github 设置了的所有 SSH 公钥,如果是空的或者不是你的密钥,那就检查一下是不是 Github 用户名填错了;
- 访问 `{Worker地址}/script.sh`,应该返回一个脚本,脚本开头的配置应与你在 Worker 开头的有关配置一致。
检查无误后就算安装完成了,然后修改本 Readme 的上面几个部分,把域名都改成自己的,保存后提交 Push 上去就好。
(如果你不会用 Git,那就直接去 Github 网页上改 Readme.md)
### (可选)使用自定义域名
在 Worker 详情中,点击“触发器”,然后点击“添加自定义域”,按照提示输入已添加在 Cloudflare 中的域名即可。
![Add Custom Domain](docs/add-custom-domain.png)
![Input Custom Domain](docs/input-custom-domain.png)
![Added Custom Domain](docs/added-custom-domain.png)
客户端会检测写入期间可观察到的手工修改并停止;其他编辑器若不遵守同一锁,仍无法保证操作系统级的原子比较并交换。请避免同时手工编辑授权文件。