Quickly Configuring SSH Server
在 Git 仓库或指定账号的 Gist 中独立管理服务器 SSH 公钥,通过同一个 Cloudflare Worker 发布网页、脚本、公钥清单、签名和公钥证书。
- 不默认读取 GitHub 账号的 SSH 登录钥匙,不依赖独立 GitHub Pages 部署。
- 普通模式仅校验公钥格式;可选高信任模式由服务器脚本验证 GPG 签名,Worker 无法远程关闭它。
- 脚本落盘后固定,定时任务只同步公钥。升级、修改信任指纹或关闭验签需要显式重新安装。
- 首页有浅色 / 深色 / 系统主题、响应式布局和逐条命令复制;不显示或内嵌公钥正文、指纹、个人注释。
初始生产清单和备用源为空,没有配置实际签名证书或发布凭据。生产构建不会偷偷使用测试密钥。仓库中的公钥文件不是 GitHub 账号登录凭据。
使用与重新安装
先配置并部署自己的 Worker,再使用自己的域名。以下地址只是本项目的部署配置示例。
普通 HTTPS 安装 / 显式重新安装并启用定时同步:
curl -fsSL 'https://ssh.lamgc.me/script.sh' | sh -s -- --install --download-script -c
再次执行安装命令会检测已安装脚本,备份并升级。签名策略只在已替换旧客户端之后激活;若后续磁盘写入失败,可能留下新脚本和未完成的配置,请按错误信息检查并显式重新安装,不要把失败当作策略已启用。已有本地配置和自定义定时计划默认保留,不因远端仓库默认值变化而重置。新安装默认每 15 分钟同步。
这条命令信任下载到的可执行脚本。 HTTPS、同一 Worker 提供的摘要和后续公钥验签,不能证明首次安装器本身可信。高信任安装应先取得独立渠道核对过的脚本摘要和完整 GPG 指纹,下载、核验本地文件后执行安装。不要把从同一被攻陷来源获得的指纹当作独立信任依据。
日常同步、预览、状态使用已经落盘的脚本:
sh "$HOME/.conf-sshd/conf-sshd.sh" -o
sh "$HOME/.conf-sshd/conf-sshd.sh" -o --dry-run
sh "$HOME/.conf-sshd/conf-sshd.sh" --status
查看完整参数,不修改配置:
curl -fsSL 'https://ssh.lamgc.me/script.sh' | sh -s -- -h
指定 / 取消自己的定时计划:
sh "$HOME/.conf-sshd/conf-sshd.sh" -c '0 */6 * * *'
sh "$HOME/.conf-sshd/conf-sshd.sh" -c false
sh "$HOME/.conf-sshd/conf-sshd.sh" --uninstall
卸载保留授权密钥、SSH 服务配置、本地信任配置与防回滚状态。-u 不再下载或替换脚本,会提示显式重新安装。没有自动更新周期;延长更新周期本身也不是安全验证。
Git / Gist 管理模式
Git
keySource.type 为 git(默认)。普通公钥逐行放入 keys/authorized_keys;构建校验并去掉所有注释,再发布。若注释曾提交公开仓库,它仍可能存在于历史中,因此发布前也应从源文件去掉个人信息。
支持 ED25519、RSA(2048–16384 位)、ECDSA nistp256/384/521 和 OpenSSH 安全密钥格式。不接受 DSA、SSH 证书、command= 等 authorized_keys 选项、重复公钥或私钥。
签名发布使用精确签名文档和 detached ASCII-armored GPG 签名。两者与网页、脚本一同构建,作为同一 Worker 发布产物交付;不会在发布后重新生成签名正文。
在持有签名私钥的可信电脑上,先生成新的文档(序号必须递增,时间自行选择):
node scripts/prepare-key-release.mjs --input keys/authorized_keys --output keys/ssh.keys --list-id my-servers --sequence 1 --issued-at 1791856800 --expires-at 1794448800
上例时间仅展示参数格式,不应照抄为正式有效期。工具会拒绝覆盖已有文件;更新时先保留旧版并明确选择新输出,再审阅替换。清空发布必须使用空输入并加 --revoke。
审核精确文档后,使用你自己的授权签名钥匙手动签名:
gpg --armor --detach-sign --local-user FULL_PRIMARY_FINGERPRINT --output keys/ssh.keys.sig keys/ssh.keys
FULL_PRIMARY_FINGERPRINT 必须替换为独立核对的完整指纹;GPG 可选择该主钥的签名子钥。不要上传私钥,也不要在 Actions 中自动运行签名步骤。上传的 ssh.keys 与 ssh.keys.sig 必须保持原始字节;Gist 使用同样文件即可。Git 也可使用工具打包的 keys/release.bundle,但不能与 keys/ssh.keys 同时存在。
Gist
配置固定的 Gist ID、账号数字 ownerId、显示用 ownerLogin 和文件名。Worker 通过 GitHub REST API 核对所属账号,取得当前修订后,再从那个固定修订读取公钥与签名。不能仅靠 URL 中的用户名判断所属,也不能从任意请求参数传入 Gist/上游地址。
公开 Gist 无需读取 token;仅使用明确配置的文件,不跟随 API 返回的任意 raw URL。缺失文件、截断响应、owner 不匹配、超时或非 200 均视为来源失败。
同一 Gist 同时含 release.bundle 与拆分文件时,bundle 优先;切换到拆分文件前应明确移除旧 bundle,避免持续发布旧内容。ownerLogin 只是显示信息,数字 owner ID 才是身份固定值。
更新签名文档后必须重新签名。Worker 不持有签名私钥,不会为 Gist 的任意修改自动背书。普通模式可以发布无签名清单;客户端是否验签由其本地策略决定。
Gist 配置示例(将 ID 与账号数字 ID 替换为经过核对的真实值):
{"keySource":{"type":"gist","id":"0123456789abcdef0123456789abcdef","ownerId":123456,"ownerLogin":"example","file":"ssh.keys","signatureFile":"ssh.keys.sig","bundleFile":"release.bundle"}}
可见性
网页不嵌入密钥,不代表数据保密。/ssh.keys 可公开读取,公开 Git/Gist 和历史也可访问;去掉注释后,相同公钥字节仍能被关联。私钥始终留在使用者或可信签名端,不能上传本项目、Worker、Actions 或 Gist。
客户端 GPG 验证(默认 OFF)
- OFF:只下载
/ssh.keys,校验协议、公钥格式和明确空清单;不请求/ssh.keys.sig或/public-key.gpg,即使它们存在也不验签。 - ON:取得
/ssh.keys原始字节和同源/ssh.keys.sig,用安装时固定的专用本地钥匙环验证。HTTP 下载每次都必须是 200;404、其他状态、重定向、下载失败、证书指纹不符或验签失败均拒绝该更新。绝不自动回退为 OFF。 - 验签覆盖完整响应,包括元数据、换行和公钥正文。不能先去注释、重新排序或转换编码再验签。
- 本地记录完整 40 位 OpenPGP v4 指纹,不接受短 key ID。
primary模式接受该主密钥及经 GPG 验证绑定的有效签名子钥;subkey只接受指定签名子钥,不接受同主钥的其他子钥。主钥下后来新增的签名子钥仍需通过显式重新安装更新本地证书;固定指纹不会自动下载最新子钥集合。 - 日常验证使用隔离的专用本地 GPG keyring,不信任用户默认钥匙环,也不自动访问 keyserver;网络返回的其他钥匙不能扩大本地授权。
仓库可设置新安装默认值,安装时可显式覆盖。已安装机器的本地配置优先,之后改仓库默认值不能关闭它的验证。开启验证、换指纹/信任范围或关闭已有验证都必须显示具体信息并取得本地明确确认;取消或无法确认时不应用修改。
安装在本地明确确认后依序寻找对应证书:已有本地证书、Worker /public-key.gpg、可选 Email 的 WKD、配置的有序 keyserver。每一步仍必须匹配完整指纹;没有 Email 时跳过 WKD;内置有序服务器为 hkps://keys.openpgp.org、hkps://keyserver.ubuntu.com,允许显式覆盖。它们只是证书分发源,不是信任根;缺少 UID、无法导入或不支持指定子钥查询时继续下一源,绝不缩短指纹。全部失败则保留现有配置并停止。通过验证的公共证书保存到专用本地钥匙环。仓库默认 Email 会写入公开安装脚本,只应填写愿意公开的邮箱;它只是发现提示,不能取代指纹。
公钥证书可公开分发,信任来自独立核对并固定的完整指纹。证书内容即使具有相同主指纹,也未必是最新撤销状态:恶意分发端可能重放旧证书、隐瞒新撤销。客户端检查收到的证书中已有的过期/撤销信息,但不能保证及时获知被隐瞒的撤销。复用本地证书也不会自动获知之后的撤销。重新安装仍优先使用有效的本地证书;若需更新同指纹证书,显式提供从可信渠道取得的新公共证书,再核对其撤销/子钥状态。
完整指纹和 Email 可在 clientDefaults 设置,也可显式安装覆盖。例如先独立核验已下载的本地脚本,再执行:
sh ./conf-sshd.sh --install -c --gpg-verification on --gpg-fingerprint FULL40HEX --gpg-pin-mode primary --list-id my-servers --gpg-email [email protected]
占位指纹必须替换为真实完整指纹;Email 可省略。安装会在本地终端要求输入展示的完整指纹确认,没有静默确认参数。关闭已有验证使用显式重新安装加 --gpg-verification off,同样需要终端确认。重新安装可用 --fallback-urls-file 指定按行排列的备用文档 URL;--keyserver 可重复覆盖内置列表,原有设置默认保留。
签名文档、防回滚与故障
列表 ID 使用 1–64 个字母、数字、点、下划线或连字符,单独的 - 保留给本地未配置状态,不能作为列表名。
签名文档使用固定 LF 格式:协议标记、列表 ID、单调递增序号、签发/到期 Unix 秒、明确撤销标志、数量、结束标记,以及无注释的规范公钥正文。签名覆盖精确全部字节。
ON 客户端检查固定列表 ID、有效期和持久化的最高序号/摘要。低序号拒绝;同序号只接受同一文档;明确撤销也必须签名并增加序号。首次安装的最低序号应通过可信渠道确定,否则仅有指纹不能排除有效期内的旧发布。
更新使用锁、待提交记录、快照检查和原子替换来处理进程中断。待提交发布已过期时不会强行应用,保留其序号下限后允许有效的新发布继续;损坏且无法可信恢复的状态会停止并提示人工检查。不要删除或恢复旧版本的本地防回滚状态;整机快照回滚、本地管理员篡改或底层文件系统掉电持久性不由一个 POSIX 脚本保证。
下载/验签失败或清单过期会保留最后成功的密钥并报错,不会自动清空密钥造成锁死。攻击者仍可阻断更新;离线机器无法保证及时撤权。删除公钥不会终止已有会话,也不会移除管理区块外的手工重复钥匙。部署签名私钥或服务器本地管理员权限失陷超出这层保护范围。
有序备用源
有两份有意区分的配置:
- Worker 部署的
fallbackSources:主来源不可用或普通主清单为空时,按顺序取首个有效集合,不合并多个来源。明确撤销阻止回退。 - 每台服务器本地保存的备用 URL:主 Worker 更新失败时,按本地顺序尝试,最多一个主源加五个备用源。每个端点单次请求最多 30 秒;由后续源或下次计划同步重试。只通过显式安装配置改变,远端响应不能覆盖。
服务器本地备用 URL 必须返回本项目 v1 或 signed-v1 文档;普通原始公钥文件可通过 Worker 来源适配器发布。客户端 ON 时,备用 URL 和对应 .sig 必须满足相同指纹、列表 ID、签名、时效和防回滚策略。不能用另一来源的签名拼接,更不能在失败后接受未签名公钥。Worker 不能替未签名备用源伪造可信签名。
Worker 上游请求有时间、流式字节数、数量限制,并拒绝重定向。没有 Worker 陈旧公钥缓存,每个相关请求重新读取;API 响应 Cache-Control: no-store。Gist 的正文与签名取自同一修订。客户端两次 HTTP 请求之间若恰逢新发布,签名不匹配会安全拒绝,后续同步再重试。
配置与部署
deployment.json:
baseUrl:正式 HTTPS origin,无路径、凭据或末尾斜杠。repositoryUrl:项目 GitHub 地址。defaultCron:新安装默认计划。allowEmpty:普通 Git 模式明确允许撤销全部管理密钥;默认 false。fallbackSources:最多五个公开 HTTPS URL,不含凭据、query、fragment 或本地/IP 地址。keySource:Git 或固定账号 Gist 配置。signedRelease:签名列表身份和可公开发布的证书配置;不是私钥。clientDefaults:首次安装的验签默认值、完整指纹、primary/subkey 范围、列表 ID 和可选公开 Email;不覆盖现有本地设置。
构建需要 Node.js 22+、npm、Git、OpenSSH 客户端和 POSIX sh;GPG 验证/测试需要 gpg。服务器需要 curl、ssh-keygen、awk、mktemp、stat、常用 POSIX 工具及 sha256sum/shasum/openssl 之一,ON 还需要 gpg;计划任务需要 crontab。脚本不会自动安装软件。
npm ci
npm test
npm run build
node scripts/verify-release.mjs
正式上线前配置自己的域名、Worker 名称、正式公钥/签名和最小权限 Cloudflare Secrets。将仓库 ENABLE_WORKER_DEPLOY=true 才启用 main 发布;默认关闭。PR 测试不部署。
可选 Git 提交签名门禁
REQUIRE_GPG_SIGNATURE=true 时,发布验证任务先验证本次精确 GITHUB_SHA,然后才执行依赖安装、测试和构建。授权完整主指纹和公钥从 GitHub 设置中的 GPG_ALLOWED_PRIMARY_FINGERPRINTS、GPG_PUBLIC_KEYS 读取,而不是让待部署提交自行指定。
GitHub 的通用 Verified 标记不是本项目授权名单。最终 merge/squash 提交必须由授权 GPG 钥匙签署;仅 PR 内旧提交签名不够。手动发布也不能绕过同一验证。
这是附加门禁,不能替代服务器验签。 同仓库 workflow/验证器可被有权限者修改,需配合主分支与工作流强制审阅、受保护 production 环境和最小权限凭据。能修改验证工作流并访问发布凭据的人仍在信任边界内。
可选部署邮件通知 hook
默认关闭。通过 ENABLE_MAIL_NOTIFICATION、MAIL_NOTIFICATION_WEBHOOK 和可选 MAIL_NOTIFICATION_TO 接入你控制的邮件提供方 webhook;本项目不内置 SMTP 服务,也不会自行创建凭据。
通知说明部署结果、代码 SHA、来源模式、列表身份和部署配置中的备用 URL。它不是服务器清单,不知道各机器后来修改的本地备用源,也不是每次 Gist 内容变更的监视器。通知失败或被压制不能当作部署未发生/系统安全的证明。
API
| 路径 | 内容 |
|---|---|
/ |
浏览器首页;curl/libcurl/wget/HTTPie 返回脚本,显式 HTML 优先 |
/script.sh |
安装脚本 |
/ssh.keys |
精确公钥文档;签名存在时就是被签名的原始字节 |
/ssh.keys.sig |
对上述文档的 detached GPG 签名;没有可用签名时 404 |
/public-key.gpg |
配置的公共 GPG 证书;未配置时 404 |
/release.json |
来源、数量、摘要、代码修订及签名是否提供;不表示服务器已验签 |
脚本、Git 公钥/签名与网站一起构建发布;不需要额外 Pages。动态 Gist/备用数据的序号和摘要与代码修订分别表示。静态 CSS/JS 使用内容哈希缓存,未知接口返回 404,不返回 HTML 冒充密钥。
从旧版迁移
- 保留恢复会话和备用登录方式,先核对独立服务器清单。
- 显式重新安装新版脚本,检查本地配置、自定义计划、管理区块和手工钥匙。
- 逐台核对脚本版本、完整指纹、验证模式、列表 ID 和序号;等待一段时间不是所有服务器升级完成的证据。
- 再切换新的签名文档格式并按确认流程启用 ON。新版 OFF 可以解析新文档但不验签;旧 v1 客户端会拒绝新格式并保留旧钥匙,不能把它视为已得到保护。
- 另开 SSH 会话验证登录。回滚旧 Worker 可能重新发布历史钥匙;ON 客户端有序号防回滚,但不要依赖普通模式抵抗历史重放。
客户端操作边界
仅操作当前用户 $HOME/.ssh/authorized_keys 的唯一配对管理区块。临时文件、锁、备份、同目录替换和写前快照检查避免可观察到的并发覆盖;不同编辑器不共享锁时仍无操作系统级原子比较交换保证。
-p yes/no 是独立 root 高级操作,仅修改 PermitRootLogin,验证 sshd -t/-T 后替换,遇 Include/Match 等复杂配置拒绝;不会自动 reload/restart。普通安装/同步不修改 SSH 服务策略。
目标为 Linux dash、Bash POSIX 与 BusyBox ash。测试不等于真实服务器登录、SELinux、发行版服务管理或自定义 AuthorizedKeysFile 已验证。异常强制终止可能留下锁,人工确认原进程状态后再处理。
测试
npm test
npm run build:test
npx wrangler deploy --dry-run --config cf-worker/wrangler.toml
测试只使用临时 HOME、命令替身、公开测试 SSH 钥匙及临时无身份/无访问权限的 GPG 测试钥匙。真实浏览器测试仅访问 localhost。build:test 产物标记为测试,生产验证拒绝它;上线必须重新生产构建。
License
Apache-2.0,见 LICENSE。