Mac 上出现 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED:先核对指纹,再删 known_hosts 里的那一条
Frank Zhang
- 13 分钟阅读 - 6135 字TL;DR
对面的主机密钥和 known_hosts 里保存的不是同一把。先分清这次变化是不是你预期的,用控制台或官方页面核对 SHA256,再只删这一条。
你刚连上的那台机器,现在给出的主机密钥,和 ~/.ssh/known_hosts 里为这台主机保存的不是同一把。端口不是 22 时,文件里的名字是 [host]:port。OpenSSH 会停在这里。警告说明这次核对生效了。第一步不是把整个 known_hosts 删掉。
先对上这段警告。下面的句子来自写这篇时读到的 OpenSSH sshconnect.c。指纹、路径、行号、密钥类型以你屏幕上的为准。这里的 you 和 example.com 是占位。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:…
Please contact your system administrator.
Add correct host key in /Users/you/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/you/.ssh/known_hosts:12
Host key for example.com has changed and you have requested strict checking.
Host key verification failed.
这两行说的是两把钥匙。「The fingerprint for the ED25519 key」是服务器刚才送来的那把,后面的 SHA256 是它的指纹。「Offending ECDSA key in …:12」是文件第 12 行已经存着的那把。类型可以不一样。Offending 那一行里的路径,才是要打开的文件。在 Mac 上,客户端印出来的通常是 /Users/<名字>/.ssh/known_hosts。
最后一句 Host key verification failed. 也会出现在另一种情况:这台主机还没进过 known_hosts,又没有人能在提示里输入 yes。那种情况没有上面这段警告。先读这句话上面的几行,再决定你落在哪一种。后一种放在后面「没有人能输入 yes」那一节。
写这篇时没有在 Mac 或 VPS 上重跑这些命令。旗标含义来自手册。文中的 OpenSSH 源码链接是写作时读到的 master 树。Mac 自带的是它自己的那一份 OpenSSH。你机器上的 man 和这里不一致时,以你的手册为准。
三十秒:这次换钥匙,你能不能说出原因
两行都要换一条不经过这次 SSH 的渠道来对指纹。差别只在于:你是不是已经知道为什么会有一把新钥匙。
| 你能不能指出变化 | 常见情形 | 下一步 |
|---|---|---|
| 能。VPS 重装或重建过,这个 IP 分给了另一台机器,frp、cpolar 或其他端口转发现在指到了别的主机,路由器或 NAS 重置过。 | 出现新的主机密钥说得通。 | 读 SHA256 那一行。用厂商控制台里的指纹来对,主机是 GitHub 时用它公布的那一页。对上之后,只删 known_hosts 里的这一条;重新连接时,提示里的指纹还是这一串,才输入 yes。 |
| 不能。你没有重装、没有改地址、没有动转发、也没有重置设备。 | 警告里的另一半:连接可能被截听。 | 停。known_hosts 先留着。用这条 SSH 以外的渠道核对指纹。拿不到指纹,或者对不上,这一条就继续留着。 |
「今天早上刚重装」也不能跳过比对。重装和截听可以发生在同一天。用来对指纹的渠道是厂商的网页控制台、已经接在那台机器上的键盘、能在那台机器上读主机密钥的人,或者运营方公布的指纹页。刚印出这句警告的那条 SSH,就是被怀疑的那条路。
能看到这段警告,说明 TCP 已经接上了。还停在 port 22: Connection refused 时,主机密钥还没开始核对。那一段只读顺序在 SSH 报 connect to host port 22: Connection refused:从 Mac 连出去,还是连进这台 Mac。
只读核对
这一节不删行,也不改 ~/.ssh/config。
文件里用的是哪个名字
ssh-keygen(1) 的 -F 在 known_hosts 里查找主机名,端口写在可选的 [hostname]:port 里。同一段写明,这个查找能找到被哈希过的名字。括号要加引号,不然 shell 会把它们当成路径模式。
ssh-keygen -F host
ssh-keygen -F '[host]:2222'
22 端口在文件里就是主机名本身。写这篇时读到的客户端源码里,misc.c 的 put_host_port 在端口为 22 时保留裸名字,否则写成 [host]:port。sshconnect.c 旁边的注释说,加上端口是为了让转发过来的连接、或同一台机器上的另一个 sshd,能各自有一把钥匙。ssh-keygen -F host 看不到 -F '[host]:2222' 才能看到的那一条。
同一本手册的写法里,-F 可以带 -l,用来打印已保存钥匙的指纹:
ssh-keygen -lF host
ssh-keygen -lF '[host]:2222'
什么都没打印,就是你传入的名字和文件里的名字不一致。ssh_config(5) 的 Hostname 才是真正要登录的主机,Port 默认 22。你敲的短名字,可能在写入钥匙之前就被改写了。不发起连接、只看展开结果:
ssh -G host | grep -iE '^(hostname|port) '
ssh(1) 写明 -G 会在展开 Host 和 Match 之后把配置打出来,然后退出。用它打出的主机名和端口再跑一次 -F。你用 IP 连过的话,IP 是另一个查找名。对主机名做 -F,列不出 IP 那一行。
「Offending … key in …:N」仍然值得看。第 N 行就是比对失败的那把旧钥匙。文件如果做过哈希,这一行上看不到主机名。找出它是 -F 的事,手册也因此把 -F 用于哈希过的名字。
网络上现在回答的,和控制台上的那把
ssh-keyscan(1) 收集主机公钥。-p 是端口。ssh-keygen(1) 的 -l 打印公钥指纹,-f - 从标准输入读。写这篇时读到的 ssh-keygen 源码里,文件名 - 就是 stdin,以 # 开头的行会被跳过,而 ssh-keyscan 的注释横幅就是这种行。
ssh-keyscan -p 2222 host | ssh-keygen -lf -
端口用报错里的,或 ssh -G 打出来的。本来就是 22 的话,写 -p 22。不加 -t 时,扫描可能打出好几种类型。要和警告里「The fingerprint for the … key sent by the remote host」点名的那一种对齐。-t ed25519(或 ecdsa、rsa)只取这一种。手册的默认是把 ecdsa、ed25519、安全密钥类型和 rsa 都取回来。
这条管道走的是和 SSH 同一条网络。ssh-keyscan 的手册写明,它不能证明取到的钥匙是真的;能截听流量的人可以换上一把自己的。把它当作「这条网络现在在说什么」的第二次观察。它代替不了控制台或官方页面上的那次核对。
要在 sshd 那一侧对:厂商控制台,或者一条不依赖你正在怀疑的连接的会话。ssh(1) 的「VERIFYING HOST KEYS」给出的例子是:
ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key
警告里点了哪种类型,就读哪种公钥文件。服务器还用 OpenSSH 常见文件名时,警告写 ED25519,就读 ed25519 这一份:
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
ECDSA 和 RSA 是旁边的另外两个文件。手册的例子指向 RSA 文件,而且不带 .pub;-l 会去找配对的公钥。这台服务器的 HostKey 若写在别处,控制台上的命令就改用实际存在的那个文件。这篇没有逐台核对主机密钥目录,也包括 Mac 上 sshd 把钥匙放在哪。
接受新钥匙之前,控制台(或官方公布的那一页)上的指纹要和另外两处一致:
- 警告里的 SHA256(刚才递过来的那把)。
- 控制台上的 SHA256(那台机器磁盘上的那把)。
- 删掉旧记录之后,「authenticity of host … can’t be established」提示里的 SHA256。
提示里的指纹和控制台上对过的那一串不一样,就停下来再读。警告和提示彼此相同,只说明这条网络两次给出了同一把钥匙。
哈希过的行不改变这次比对。ssh_config(5) 的 HashKnownHosts 会在写入时把名字哈希掉。那本手册里的默认值是 no。哈希之后的行上看不到主机名,grep 也找不到。-F 和 -R 仍然吃明文的主机名或 [hostname]:port。这篇没有读过一台 Mac 上 HashKnownHosts 的默认值。
主机是 github.com 时
警告里的 github.com 是 GitHub 的 SSH 主机密钥,不是你的 deploy key,也不是 ~/.ssh/id_ed25519。后两把用来证明「你是谁」,而且要等主机密钥已经通过才会用到。看到 Permission denied (publickey),说明主机密钥这一关已经过了。这段警告的意思是这一关没过。
GitHub 把用来比对的指纹公布在 GitHub’s SSH key fingerprints。警告里的 SHA256 去对警告点名的那一种。下面四个值是 2026-11-12 那一页上的原文。GitHub 再换钥匙时,以那一页为准,不以这篇为准。
SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s(RSA)SHA256:br9IjFspm1vxR3iA35FWE+4VTyz1hYVLIE2t1/CeyWQ(DSA,该页标注为 closing down)SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM(ECDSA)SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU(Ed25519)
2023-03-24 大约 05:00 UTC,GitHub 更换了 GitHub.com 上用于 Git 操作的 RSA SSH 主机密钥。文章是 We updated our RSA SSH host key(Mike Hanley,署期 2023-03-23)。文中写的是:RSA 私钥曾短暂出现在一个公开仓库里,更换是出于谨慎,ECDSA 和 Ed25519 没有换。网页和 HTTPS 的 Git 流量不在那次范围内。github.com 上一条旧的 RSA 记录会打出这句警告,原因 GitHub 已经写过。先把警告里的指纹和上面的 RSA 那一行对上,再按和其他主机一样的办法删掉 github.com 这一条。那篇文章自己给出的删除命令是 ssh-keygen -R github.com。
其他 GitHub 主机名这篇不展开。警告里写的那个名字,就是要查的名字。
只删这一条
指纹已经和 SSH 连接以外的来源对上,再做这一步。
ssh-keygen(1) 对 -R 的说明是:从 known_hosts 里删除指定主机名(可带端口)的全部钥匙。同一句写明,这就是删除哈希主机的办法。你传入一个主机名,这个名字下的各种类型会一起删掉。另一个名字不会被带走。IP,以及 [host]:2222,如果各自有行,就各自再跑一次 -R。
ssh-keygen -R host
ssh-keygen -R '[host]:2222'
手册里 -R 那一段没有写备份。写这篇时读到的 ssh-keygen 源码在原地删除时,会把改写前的内容留在同一路径加上 .old,然后打印 Original contents retained as 和那个路径。默认的用户文件,备份就在 known_hosts 旁边。同一函数会先删掉已经存在的 .old,所以这份备份只是这一次运行之前的文件,只留一代。新连接的指纹对上之前,先留着它。删 .old 不是修复的一步。
源码里的「没找到」那条路径会记一行 Host <名字> not found in <文件>,并且不替换原文件。常见原因是一边用了带端口的写法,另一边用了裸名字。用 -F 已经找到的那种写法。
-f 用来指定文件。默认是用户自己的 known_hosts。Offending 那一行如果写的是 /etc/ssh/ssh_known_hosts 或其他路径,就把那个路径传给 -f。系统文件可能要 sudo。不带 -f 的 -R 不会去动系统文件。
站上的 known_hosts 修复命令生成器 会按你填的主机和端口填好 -F 和 -R。它不替你比对指纹。比对还是你自己做。
删完再连:
ssh -p 2222 user@host
StrictHostKeyChecking 保持默认时,客户端会先问,再保存新钥匙。ssh_config(5) 写明默认是 ask:你确认之后才写入新钥匙,钥匙变了则拒绝。这时该出现的是第一次连接的那句「The authenticity of host … can’t be established」,后面跟着 SHA256,以及 yes/no/[fingerprint]。这一串和你已经对过的指纹一致,才输入 yes。yes 会把钥匙写进用户的 known_hosts。没对过的指纹,就是还没核对的钥匙。
StrictHostKeyChecking yes 不会自己添加钥匙。手册对 yes 的说明是:新主机要你手工加进去。这篇的重连步骤按 ask 来写。
会把核对拆掉的写法
ssh-keygen -R 删的是你点名的那一台。其余你已经接受过的主机还在文件里。把 ~/.ssh/known_hosts 整个删掉,这些记录会一起消失,包括和这次警告无关的主机。
StrictHostKeyChecking no 和 UserKnownHostsFile none 若作用到每一台主机,核对就对每一台都关掉了。别人也常贴 UserKnownHostsFile /dev/null。手册里的值是 none:ssh 因此不去读用户自己的 known_hosts。手册对 no(以及 off)的说明是:新钥匙会自动写入,钥匙变了的连接也继续,但「subject to some restrictions」(受一些限制)。写这篇时读到的客户端源码里,严格检查关掉、钥匙又对不上时,这把钥匙会被标成不可信:密码和键盘交互认证被关掉,agent、X11、端口转发和隧道转发也被关掉。Host *,或者写在任何 Host 块之前的同样几行,会套到 GitHub 和其余每一台服务器上。那些主机保持默认。
实验机器因为你反复重装而换钥匙,可以单独写一块。Host 只把后面的行套到它匹配的名字上。
Host lab-box
HostName lab.example
Port 2222
UserKnownHostsFile ~/.ssh/known_hosts.lab
StrictHostKeyChecking accept-new
同一本手册里,accept-new 会写入文件中还没有的主机,钥匙变了仍然拒绝。重装把钥匙换掉之后,还是要对 ~/.ssh/known_hosts.lab 做 -R(或 [host]:port 那种写法)。单独这份文件的用处,是让这次 -R 碰不到你给其他机器用的那份 known_hosts。
真正不再拒绝「钥匙变了」的设置,是只匹配这台实验机器的块里的 StrictHostKeyChecking no。代价就是警告里写的那件事:这个名字不再绑在你核对过的那把钥匙上,有人在这个名字上截听,known_hosts 拦不住。上面源码里的那些限制,在送来的钥匙并不可信时仍然在。同一个只匹配实验机器的块里写上 UserKnownHostsFile none,表示这些名字没有用户文件可对。系统的 known_hosts 如果存在,是另一份清单。账号、GitHub,以及你不能让人冒充的机器,这篇不给它们写 no 或 none。
git clone、scp 和 CI:没有人能输入 yes
Host key verification failed. 是 sshconnect2.c 在主机密钥核对失败时打出的 fatal。git、scp 和 CI 日志里看到的就是这一句。现行 OpenSSH 的 scp 走的是同一套客户端核对。某一种 git 或 scp 发行版的细节算一般判断;这句 fatal 来自上面的源码。
钥匙已经变了的那条路径,没有 yes/no 可答。这段警告打出来,严格检查又没有关掉,客户端就失败。终端前和任务日志里,这一段看起来一样。
主机还没进 known_hosts,是另一条路径。提示是「Are you sure you want to continue connecting (yes/no/[fingerprint])?」。客户端源码里的 confirm() 在打开了 BatchMode 时直接当作 no 返回。ssh_config(5) 写明 BatchMode yes 会关掉用户交互,包括主机密钥确认,用于脚本和批处理。没有终端时,这句提示同样没法回答。git clone、脚本里的 scp、GitHub Actions、GitLab CI 通常没有人坐在那里,缺记录时就会停在同一句 fatal 上。
这些任务的一般做法:把你已经核对过的一行 known_hosts 放到这个任务的 ssh 会去读的地方。GitHub 的核对来源是上面的指纹页,不是在 runner 上现扫。你自己的服务器,来源是厂商控制台里的指纹。任务的家目录是空的时,可以用 UserKnownHostsFile 指向那份文件。在任务里跑 ssh-keyscan,拿到的是 runner 所在网络上回答的那把钥匙。ssh-keyscan 的手册写明,这份输出不能证明钥匙是真的。
在 GIT_SSH_COMMAND 里加上 StrictHostKeyChecking=no,和全局关掉是同一笔代价,只是范围缩到这一次命令:核对不发生。还想留着这次核对,就把一行已经核对过的记录放进 known_hosts。
GitHub 2023 年的那篇文章写过:使用 actions/checkout 且带 ssh-key 选项的 Actions,会在那次 RSA 更换时失败;他们当时在更新这个 action 受支持的 tag。那是 GitHub 对自己那次更换的说明,不是一份可以抄到别的主机上的 workflow。GitLab CI 也一样,这是一般判断:把你已经核对过的那一行写进任务的 known hosts。这篇不公布 GitLab 的指纹。
这篇停在这里
- 这些命令没有为这篇在 Mac 或 VPS 上重跑。依据是上面链接的手册和源码。机器上的
man优先。 - 这篇不锁定某一个 macOS 版本,也不锁定 Apple 对 OpenSSH 的补丁级别。
-R一个名字,留着其他名字。IP 和[host]:port是其他名字。同一个名字下的几种类型,会在你把这个名字传给-R时一起去掉。- 上面那本手册里
HashKnownHosts的默认是no。哈希过的名字,grep 看不见。-F和-R找得到。 ssh-keyscan代替不了控制台或官方页面上的核对。- 上面那本
ssh_config(5)里CheckHostIP的默认是no。仍把它开着的那一版 ssh,可能打出POSSIBLE DNS SPOOFING DETECTED。那是另一段警告。这篇不展开。 @cert-authority、主机证书和@revoked是另一套机制。WARNING: REVOKED HOST KEY DETECTED是另一段警告。这篇不手工改那些行。- IPv6 字面量这篇不展开。带端口时,
-F和-R仍然要手册里的[hostname]:port。 - Windows 上的 OpenSSH 客户端不在这篇里。本站那次实录也写了 Windows 需要换一部分命令。
ssh_config(5)的UpdateHostKeys可以在一次已经信任的登录之后,再记下额外的主机密钥,有计划的轮换可以靠这个学到新钥匙。刚才送来的钥匙已经对不上时,它不代替这次核对。- 搜索量。这篇没有另做外部数据。
隧道地址和端口一直在变的那次经过,在 一次ssh排错经历。命令生成器是 known_hosts 修复命令生成器。其他阅读路线见 自部署阅读路线。英文稿是同一题的另一份写法:WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED on Mac: check before you delete known_hosts。
这篇如果有用,欢迎自愿赞助,用来继续更新博客和自部署笔记。这不是慈善捐赠。