# 你的 SSH 主机密钥轮换准备好了吗？

SSH 主机密钥应当按照你的计划更换，而且客户端应当已经信任替代密钥。如果开发者第一次听说轮换，是因为终端里出现了红色警告，那么轮换计划已经失败。该警告无法告诉他们，服务器是正常变更了，还是有人截获了连接。匆忙通知所有人删除 `known_hosts` 中的一行，会毁掉他们作出判断所需的证据。

安全轮换包含四个明确动作：通过可信渠道公布旧指纹和新指纹，让两把密钥并存足够长的时间，使客户端学会新密钥，在规定窗口内移除旧密钥，并证明客户端会拒绝已退役的身份。我见过一些团队把服务器端工作做得完全正确，最后却让开发者养成了最糟的习惯：把主机验证当成障碍。运维工作的目标，是在警告出现之前让安全路径成为日常路径。

## 主机身份和用户身份验证不是一回事

主机密钥证明是哪台 SSH 服务器响应了请求；用户密钥证明是谁在申请登录。轮换其中一个不会轮换另一个。这个区别听起来很基础，但事故处理手册经常把 `authorized_keys`、个人 SSH 密钥、主机证书和 `/etc/ssh/ssh_host_*` 文件混在一起，只给出一条含糊的「轮换 SSH 密钥」指令。

RFC 4253 把服务器身份验证放在传输握手中。服务器用自己的主机私钥为交换哈希签名，客户端则对照可信来源检查公钥，例如 `known_hosts`、主机证书颁发机构或经过认证的 SSHFP 记录。密码、用户证书和用户公钥都在此后才会处理。如果客户端不再检查服务器，开发者完全可能把有效的用户凭据交给冒充者。

这正是主机变更警告格外严厉的原因。它表示连续身份声明已经中断：开发者输入的名称现在出示了另一个身份。按计划替换、重建虚拟机、负载均衡器指向错误的服务器池、DNS 被篡改以及主动拦截，在这一刻看起来都可能相同。客户端看不到你的变更工单。

生成任何密钥之前，先写清范围。记录人们使用的每个主机名和别名、每个非标准端口、脚本里直接出现的每个地址、堡垒机路由、CI 运行器、部署代理，以及任何共享的 `GlobalKnownHostsFile`。`[host]:port` 和 `host` 在已知主机记录中是两个不同的名称，而别名可能隐藏规范目标。轮换覆盖了 `git.example.net` 却漏掉 `git.internal` 时，即使两个名称都指向同一台机器，故障也会显得时有时无。

还要盘点当前提供的每种主机密钥算法。一台服务器可以同时保存 Ed25519、ECDSA 和 RSA 主机密钥。OpenSSH 会与客户端协商算法，因此两名开发者连接同一守护进程时，可能固定不同的公钥。只轮换你在自己笔记本上看到的密钥，并不能确定服务器的完整身份集合。把已配置的 `HostKey` 项和经过验证的公钥文件作为权威清单，再将它与各类受支持客户端实际协商的结果比较。

## 改动服务器之前先公布两个指纹

在当前密钥仍为流量提供服务时，公布当前指纹和替代指纹。公布信息必须经过不依赖待变更 SSH 主机的可信渠道。已签名的运维仓库、经过身份验证的内部状态页、受管理的设备配置，或由另一组管理员管理的 DNSSEC 区域都可以。直接从同一个可能被截获的 SSH 会话中复制出来的消息不行。

应当在可信管理机器上根据公钥文件生成指纹，不要询问生产网络当前提供了什么。OpenSSH 默认显示 SHA-256 指纹。命令和输出形式如下：

```text
$ ssh-keygen -lf ssh_host_ed25519_key.pub -E sha256
256 SHA256:<base64-fingerprint> host.example.net (ED25519)
```

公布内容不应只有短摘要。每个身份都要写明主机名和端口、算法、SHA-256 指纹、状态（`current`、`new` 或 `retired`）、首次启用时间、退役时间，以及批准该记录的人员或系统。标清时区。如果多个别名共用密钥，就把它们列出来。如果一个名称后面的不同节点有意提供不同密钥，应公布完整的允许集合，并解释为何需要这个集合。

一份实用的公布记录如下：

```text
Host: build.example.net:22
Algorithm: ssh-ed25519
Current: SHA256:<old-fingerprint>
New: SHA256:<new-fingerprint>
Overlap begins: 2026-08-10 15:00 UTC
Old key removed: 2026-08-17 15:00 UTC
Old key marked revoked: 2026-08-17 15:30 UTC
Owner: Platform operations
```

日期只是示例，顺序却不是。服务器变更后才公布，会把计划工作变成临时的信任决定。只公布新值，会让开发者无法对照检查已有的固定记录。变更完成后，仍应把旧记录以已退役状态保留下来，以便调查人员识别陈旧机器和意外的密钥重用。

指纹是公开标识符，不是秘密。主机私钥必须在服务器上得到保护，但团队应当充分分发公钥和指纹，不能让验证工作依赖于变更窗口里恰好能找到某一名管理员。把公布内容的批准当成一次安全变更：两份来源文件生成了相同指纹，比聊天中粘贴的一张截图更可靠。

## 重叠期让客户端安全学会替代密钥

移除旧密钥之前，应同时提供旧主机密钥和新主机密钥。在重叠期内，已有客户端先用受信任的旧密钥验证服务器，随后可以通过 OpenSSH 的 `hostkeys@openssh.com` 扩展学会额外密钥。身份连续性来自旧密钥，因此新密钥不是一个未经认证的声明。

OpenSSH 的 `ssh_config` 手册明确把 `UpdateHostKeys` 描述为平稳轮换支持。手册也列出了在真实设备群中很重要的限制。客户端只有在使用已经信任或由用户明确接受的普通密钥完成身份验证后，才会接受额外密钥，而且验证必须使用用户的已知主机文件。如果服务器通过主机证书验证，或只通过全局已知主机文件验证，这条学习路径不会生效。当用户覆盖 `UserKnownHostsFile` 或启用 `VerifyHostKeyDNS` 时，默认设置也可能关闭该功能。

不要假设默认值。检查有代表性主机的客户端有效配置：

```text
$ ssh -G build.example.net | grep -E '^(updatehostkeys|userknownhostsfile|stricthostkeychecking) '
stricthostkeychecking ask
updatehostkeys true
userknownhostsfile ~/.ssh/known_hosts ~/.ssh/known_hosts2
```

在服务器上保留独立的私钥文件，并声明两者。这里的文件名只是示例；请使用符合操作系统和配置管理方式的路径：

```text
HostKey /etc/ssh/ssh_host_ed25519_key_old
HostKey /etc/ssh/ssh_host_ed25519_key_new
```

重新加载前运行 `sshd -t`。`sshd` 手册说明，该模式会检查配置有效性和密钥是否正常，因此能在守护进程重新读取配置前发现不可读文件和错误设置。除非服务管理器另有说明，否则应重新加载，而不是中止活动会话。随后，从只信任旧密钥的干净测试客户端连接，并在成功完成身份验证的会话后检查其隔离的已知主机文件。

配置多个密钥并不保证每个客户端都会协商出同一个密钥。算法偏好、客户端版本、定制的 `HostKeyAlgorithms`、证书和全局信任库都会影响结果。每类受管理客户端都至少要经历一个正常连接周期，重叠期不能只设成随意的几个小时。每周才连接一次的笔记本设备群，与每隔几分钟连接一次的 CI 工作节点需要不同的窗口。

不要因为看不到每个人的 `known_hosts` 文件，就假装可以精确衡量采用率。受管理客户端可以报告其分发信任文件里是否存在新公钥。对于不受管理的客户端，在隐私政策允许时按客户端版本跟踪成功连接，公布明确的验证命令，并让重叠期覆盖日常使用。没有报错不等于新密钥已经传播。

## 变更窗口需要状态和停止条件

变更窗口应定义可以观察的状态、负责人和回滚条件。「下午 3 点轮换」不是计划，因为它没有说明重叠期、客户端准备情况，也没有说明旧身份从哪一刻起不再允许。应把信任切换和服务器部署放在同一条时间线上。

设置五道关口：

1. 公布完成：独立复核人员根据批准的公钥文件重新生成每个新旧指纹。
2. 重叠上线：服务器提供两个身份，配置通过 `sshd -t`，干净客户端通过旧固定记录验证，同时学会新记录。
3. 准备达标：受管理信任库已包含替代密钥，测试客户端覆盖支持的操作系统和路由，帮助台持有准确的预期指纹。
4. 切换完成：服务器不再提供旧私钥，新会话在严格检查下成功，监控没有发现仍提供旧密钥的意外节点。
5. 吊销已证实：提供旧身份的测试端点被拒绝，已退役指纹仍以吊销状态公开。

指定一人有权停止切换。出现以下情况就应停止：任何生产节点提供了未公布的密钥，某条路由到达了清单之外的节点，某类受支持客户端无法学习或收到替代密钥，或者回滚需要恢复一把事故响应团队认为已经泄露的私钥。例行生命周期轮换可以在重叠期内回滚到旧密钥。泄露事件处置不能把已泄露密钥当成安全回滚目标。

把服务回滚和信任回滚分开。你可以恢复较早的服务器软件包，而不必恢复已经退役的主机身份。保留确认可用的配置、替代私钥访问方式，以及控制台或云服务商访问权限，团队才能在不削弱验证的前提下修复 SSH。如果 SSH 是修复 SSH 的唯一途径，这个窗口就有一个隐藏的单点故障。

窗口还必须覆盖长期存在的多路复用会话。OpenSSH 连接共享可以让主连接保持活动，新 shell 复用已有传输通道。这些 shell 不会重新进行主机密钥交换，因此无法证明切换后的身份。适用时用 `ssh -O exit host` 关闭测试控制主连接，并保证验证探针建立新的 TCP 连接。

准确安排退役时间，并让沟通渠道在退役后继续开放。休假归来的开发者会持有陈旧的固定记录。正确结果是得到有文档说明的失败和经过验证的更新途径，而不是例外放行。支持人员应先比较指纹，用 `ssh-keygen -F` 找出陈旧条目，再根据批准记录进行替换。

## 用隔离的信任文件演练警告

测试轮换时，不要触碰任何人的真实 `~/.ssh/known_hosts`。隔离文件使每个状态都能重现，也防止测试因为工作站几个月前学到的密钥而误报成功。使用能够按生产密钥顺序提供身份的预演端点，或用相同配置约束在另一个端口运行临时 `sshd`。

首先，根据已批准的旧公钥创建 `known_hosts.test`。这一行应来自复核过的文件，而不是在线扫描结果。然后用 `HostKeyAlias` 固定生产逻辑名称并连接预演端点：

```text
$ ssh -F /dev/null \
  -o HostKeyAlias=build.example.net \
  -o UserKnownHostsFile=./known_hosts.test \
  -o GlobalKnownHostsFile=/dev/null \
  -o StrictHostKeyChecking=yes \
  -o UpdateHostKeys=yes \
  -p 2222 test-host.example.net true
```

端点同时提供两个密钥时，第一次运行应当成功。用 `ssh-keygen -F build.example.net -f ./known_hosts.test` 检查文件，确认其中已经包含获批的替代密钥。为返回的每个公钥字段生成指纹，并与公布记录比较。退出状态为零只证明连接成功，不能证明身份集合正确。

接下来，让测试端点只提供新密钥并建立新连接。客户端已通过经过认证的重叠期学会替代密钥，所以连接应当成功。恢复只包含旧固定记录的快照，再对只提供新密钥的端点重复测试。在 `StrictHostKeyChecking=yes` 下，该尝试必须失败。这个负面用例证明，错过重叠期的不受管理客户端会被硬性拦截，而不是静默接受。

最后，让测试连接指向提供无关密钥的端点。为操作手册捕获失败文本和退出状态。不要为了让测试永远通过而把这项测试弱化；拒绝变更密钥正是你要保护的行为。确认脚本会传递非零退出状态，而不是在重试循环中吞掉它。

绝不能把 `StrictHostKeyChecking=no` 当作演练修复方案。当前 OpenSSH 手册说明，该设置会在一些限制条件下允许变更密钥继续连接，而 `accept-new` 仍会拒绝变更密钥。单独使用 `accept-new` 也无法处理轮换，因为已经记录的名称遇到替代密钥时，看到的仍是变更密钥。应当提供经过认证的信任资料，或使用重叠机制。

## 吊销必须导致拒绝，不能只给出清理建议

从目标服务器移除旧私钥，只能证明这一台服务器不再提供它。吊销意味着客户端在任何地方再次看到该公钥时都会拒绝，包括被遗忘的节点，或持有被盗私钥的攻击者端点。从 `known_hosts` 删除旧行恰恰相反：它会删除识别已退役身份所需的记忆。

OpenSSH 已知主机文件支持 `@revoked` 标记。这样标记的匹配密钥不得被接受，并且出现时会触发警告。受管理设备群可以分发全局吊销条目，小型环境也可以维护专用吊销文件或自动生成的信任包。使用准确的获批旧公钥，并有意限定主机名模式：

```text
@revoked build.example.net ssh-ed25519 <old-public-key-data>
```

当吊销集合变大或涉及主机证书时，密钥吊销列表会更方便。根据旧公钥生成测试 KRL，然后在部署前查询它：

```text
$ ssh-keygen -k -f revoked-hostkeys.krl ssh_host_ed25519_key_old.pub
$ ssh-keygen -Q -f revoked-hostkeys.krl ssh_host_ed25519_key_old.pub
ssh_host_ed25519_key_old.pub (<comment>): REVOKED
```

查询遇到已吊销密钥时会返回非零状态，因此自动化流程必须有意解释该状态，不能把它当成测试故障。为测试客户端配置指向 KRL 的 `RevokedHostKeys`，连接提供旧身份的测试端点，并要求连接遭到拒绝。随后连接只提供新身份的端点，并要求成功。两部分同样必要：格式错误或不可读的吊销文件可能拒绝所有连接，而不只是已退役身份。

`ssh-keygen -R host` 很适合编辑经过哈希处理的用户文件，但它不是吊销。它会删除该名称的所有条目，包括有效替代项，并把下一次连接重新变成一次信任建立。只有在比较存储条目和公布记录后，才能使用它，然后还要添加获批的新密钥。先运行 `-R` 再运行未经认证的 `ssh-keyscan`，这样的支持脚本只是把信任放弃自动化了。

保留已吊销的公开资料。你可以按照保留政策销毁没有泄露的退役私钥，但应保留它的公钥、指纹、批准记录和吊销测试结果。几个月后旧镜像重新上线时，响应人员可以依靠这些资料识别它。

## 自动化应安全失败，但不能脆弱

非交互作业需要维护良好的信任来源，不能关闭检查。CI 运行器、部署代理和自主编程代理经常最先遇到轮换，因为它们整天都在建立短连接。如果镜像中固化了一把主机密钥，却没有人为更新负责，常见的紧急补丁就是 `StrictHostKeyChecking=no`。这个补丁会把可用性错误变成身份验证漏洞。

为每种自动化选择一种信任交付方式。在重叠期把两把获批密钥写入镜像，挂载集中管理的全局已知主机文件，通过 `KnownHostsCommand` 返回获批记录，使用经过 DNSSEC 验证的 SSHFP，或信任主机证书颁发机构。OpenSSH 手册说明，`KnownHostsCommand` 输出采用普通已知主机行格式，并且会与用户和全局文件一同使用。命令失败时连接也会终止，这是正确的默认行为，但信任服务必须满足作业的可用性要求。

明确设置批处理行为：

```text
Host build.example.net
    BatchMode yes
    StrictHostKeyChecking yes
    UserKnownHostsFile /etc/company/ssh_known_hosts
    UpdateHostKeys no
```

这个受管理文件示例有意使用 `UpdateHostKeys no`。文件由配置管理负责，让每个临时工作节点修改它会产生无法保留或审计的状态。对于开发者自己管理的用户文件，`UpdateHostKeys yes` 可能更合适。同一条指令是否正确，取决于信任分发由谁负责。

把别名、跳板主机、直接 IP 连接和非标准端口作为独立身份测试。`ProxyJump` 路由仍然会验证目标主机，而跳板主机也有自己的固定记录。容器可能挂载只读已知主机文件。沙箱代理可能运行另一个 SSH 二进制文件，或者忽略用户配置目录。应在实际执行环境内运行 `ssh -G target` 查看有效设置，不能读取工作站配置后就假设两者相同。

对于代理驱动的 SSH，应把凭据和主机信任当作独立控制。Sallyport 通过其附带的 `sp-ssh` 辅助程序执行 SSH 操作，同时 SSH 密钥留在加密保管库中，因此代理不会收到这些秘密。这保护了凭据托管；你的轮换计划仍需要经过认证的主机身份来源和测试过的拒绝路径。

窗口期间自动化失败时，应报告预期和实际指纹，但不能打印私密资料。不要自动用更弱的设置重试。停止作业，保留标准错误输出和有效配置，并把不匹配事件交给公布记录中的负责人。

从没有主目录状态的一次性工作节点运行同一项自动化测试。给它提供生产环境将使用的确切受管理信任包，调用真实作业命令，并把 `ssh -G` 输出和结果一起归档。这样能抓住轮换测试中的一个常见假象：运行器之所以显得正常，只是因为先前的交互会话填充了镜像规范从未声明的可写文件。

服务器改为只提供新密钥后，从信任包副本中暂时拿掉替代密钥，测试失败路径。作业必须在发送远程命令之前停止，包装程序必须保留 SSH 退出状态。随后分发替代密钥并重复测试。如果部署框架把所有 SSH 故障都转换成普通超时，应在窗口开始前修复这个可观察性缺口，因为响应人员需要区分身份拒绝、网络丢失和用户身份验证失败。

注意会扩大信任范围的设备群配置。通配符已知主机行可能让一把获批密钥对多个名称都有效，而共享主机密钥会让客户端无法区分不同机器。两种设计都可能是有意的，但轮换记录必须说明范围。机器身份不同时应优先使用主机专属记录，并测试作业实际使用的主机名字符串。

最后，固定自主作业使用的 SSH 二进制文件和配置约定。基础镜像更新可能在主机身份变化的同时更改默认值，使根因难以隔离。切换前后都应记录客户端版本、有效主机密钥算法、信任文件摘要和逻辑主机名。这些都是公开的运维事实，合在一起可以说明失败作业遇到的是错误服务器、缺少新密钥，还是无法协商服务器提供的算法。

## DNS 和主机证书改变的是分发工作

SSHFP 和主机证书可以减少逐主机固定记录管理，但都不会取消信任轮换规划。它们把稳定信任锚移到了别处。如果你能比成千上万个独立已知主机条目更好地管理这个信任锚，就应考虑使用它们。

RFC 4255 定义了 SSHFP 记录，并要求客户端必须先确认 DNS 数据经过认证，才能信任其中的指纹。实际而言，DNSSEC 验证链必须从客户端一直成立到该记录。普通的未签名 DNS 记录可以用于比较，却无法在攻击者能够修改 DNS 答案时建立身份。OpenSSH 的 `VerifyHostKeyDNS yes` 只会隐式信任安全匹配；不安全的结果仍要经过普通确认路径。

在重叠期公布新旧 SSHFP 记录，等待 DNS 缓存和受管理解析器看到它们，在旧私钥退出服务时再移除旧记录。用 `ssh-keygen -r hostname -f public_key_file` 根据获批公钥文件生成记录。通过客户端实际使用的同一解析器路径验证已提供的记录，包括验证状态。较低的 TTL 能缩短缓存保留时间，但不能修复损坏的 DNSSEC 或错误记录。

主机证书让客户端信任类似 `@cert-authority *.example.net` 的主机 CA 条目，而不是固定每个主机密钥。当服务器得到带有正确主体名称和有效期的新认证主机密钥时，客户端可以在现有 CA 下接受它。这对大型动态设备群更干净，但 CA 会成为权限很大的信任锚。保护其私钥，限制签发范围，记录证书序列号和主体名称，并单独演练 CA 吊销。

不要只为解决一次麻烦轮换就部署主机 CA。它会带来签发、到期、主体命名和 CA 轮换工作。小型稳定设备群可以很好地管理明确固定记录。大型临时设备群往往受益于证书，因为实例身份的变化比组织权威更频繁。

无论选择哪种模型，都要记录信任起点。使用主机证书完成身份验证时，`UpdateHostKeys` 不适用；使用全局已知主机文件的客户端也不会通过用户文件机制学习附加项。混合模型却不记录优先级，会让轮换在一台笔记本上成功，在 CI 中失败。

## 用窗口结束后仍可用的证据收尾

完成的轮换应留下证据，说明变更了什么、谁批准了变更、客户端接受了什么，以及旧身份是否遭到拒绝。保留生成的公钥、重新生成的 SHA-256 指纹、`sshd -t` 结果、服务器有效配置、公布版本、客户端测试矩阵、切换时间戳、吊销资料和捕获的负面测试。工单中不要保存任何私钥。

窗口结束后记录服务器的实际状态。通过每条生产路由查询每个节点，将其提供的内容与获批集合比较，但要记住，网络采集是观察结果，不是独立的信任来源。应将它与经过签名或其他方式认证的公布记录比较。还要检查自动扩缩镜像和已关机的恢复节点；陈旧主机密钥经常通过替换容量重新出现，而不是来自窗口期间改动的那台机器。

Sallyport 的 Activity journal 和 Sessions journal 可以保留代理执行的 SSH 操作的防篡改记录，这些记录来自其加密的哈希链式审计日志。`sp audit verify` 可以在没有保管库密钥的情况下，对密文离线检查该链，为变更记录补充操作证据，但它不能取代本文介绍的主机密钥测试。

根据设备群行为设置后续检查日期。查找仍因已退役指纹而失败的客户端、提供意外身份的节点，以及事故期间添加了绕过参数的脚本。移除临时重叠配置和测试端点。只要已退役私钥仍可能存在于备份、镜像或未经授权的副本中，就应保持吊销有效。

主机密钥警告应当保持罕见，也应当让人警觉。好的轮换工作不会压制它。好的工作会提前安排信任切换，让预期客户端无需忽略警告，并证明已退役身份再次出现时，警告仍会阻止连接。
