有人第一次连 VPS 或公司内网新机器时,被那行“The authenticity of host ... can't be established”搞得很慌;也有人反过来,天天自动部署脚本里被这行交互确认卡住,烦到想拍桌子。其实这背后就是 SSH 的 host key 校验机制在起作用,而我们要做的就是把这个“大姑娘上轿头一回”的确认动作,提前做好,也就是把新IP的SSH指纹写进 known_hosts。
这篇就围绕“新IP的SSH指纹如何正确加入known_hosts”这个操作,把原理、手动姿势、批量玩法、常见坑一次讲明白。适合刚接触 Linux 服务器的新手,也适合被各种 CI/CD、自动化运维脚本困扰的同行。内容全部基于我自己的踩坑记录和实操经验,不是那种网上抄来抄去的命令堆砌。
1. 先搞清楚 known_hosts 到底管什么
1.1 为什么每次连新IP都要问一次
SSH 协议本身是不带身份认证的,客户端和服务器建立连接时,服务器会把自己的 host key(主机公钥)发给客户端。这个公钥的指纹如果不在你本地的 known_hosts 文件里,SSH 客户端就无法确认“对面到底是不是真的那台服务器”,所以它宁愿停下来问你一句:指纹对不对?要不要存下来?
这个设计其实跟浏览器访问 HTTPS 网站时校验证书一样,都是为了防中间人攻击。假如没有这一步,你连的服务器可能根本不是你以为的那台,而是被人劫持后伪装出来的,你的密码、会话内容都会被偷走。所以 known_hosts 这个文件,就是客户端本地的“信任白名单”。
很多新手第一次看到这条提示时都是直接敲 yes,这没问题,但如果是在自动化场景下,连 yes 都没法敲,那就得换一种方式提前写入。这也是“新IP的SSH指纹添加到known_hosts文件”这个需求最常见的出现场景。
1.2 known_hosts 文件的存储位置和格式
known_hosts 分两个层级存在:
- 全局文件:
/etc/ssh/ssh_known_hosts,对所有用户生效,普通用户一般没有写权限。 - 用户文件:
~/.ssh/known_hosts,只对当前用户生效,这是绝大多数情况下我们操作的文件。
文件内容的格式一般是:
192.168.1.10 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... [192.168.1.10]:2222 ssh-rsa AAAAB3NzaC1yc2EAAA... github.com,140.82.112.3 ssh-rsa AAAAB3NzaC1yc2EAAA...每一行由“主机名(或IP,或 [IP]:端口)”、“密钥类型”、“公钥内容(Base64编码)”三部分组成,用空格分开。如果 SSH 客户端开了 HashKnownHosts 选项(默认macOS上开着),主机名部分会变成一串哈希值,比如|1|xxxx=|yyyy=,普通人肉眼看不出这行对应哪台机器,但 SSH 客户端自己能算出来。
另外需要注意:该文件的权限必须是 600(owner 可读写,其他人啥都不能干),否则 SSH 客户端会直接拒绝读取,甚至报出 “Bad owner or permissions” 错误。这个坑在后面排查部分我会再提。
提示:不要随便手动编辑 known_hosts 去改内容,格式错了很容易导致 SSH 连接直接失败。正确做法是优先用
ssh-keyscan生成,或者用ssh-keygen -R删除旧记录后重新连接生成。
2. 手动添加新IP指纹的几种可靠姿势
2.1 用 ssh-keyscan 一键抓取并追加
ssh-keyscan是 OpenSSH 自带的小工具,作用是主动连接某台服务器的 SSH 端口,抓取它返回的 host key,然后输出成 known_hosts 兼容的格式。它不会真正建立 SSH 会话,也不会要求输入密码,所以非常适合用在脚本里。
单台机器手动添加的姿势:
ssh-keyscan -t ed25519,rsa,ecdsa 192.168.1.10 2>/dev/null >> ~/.ssh/known_hosts-t指定抓取哪些密钥类型,建议写全ed25519,rsa,ecdsa,防止你服务器上只启用了某一种类型而漏掉。2>/dev/null是为了屏蔽掉 ssh-keyscan 输出到 stderr 的各种错误和提示信息,只把真正的密钥行追加进去。
执行完之后可以用下面的命令检查是否写入成功:
ssh-keygen -F 192.168.1.10如果成功,会打印出对应主机的公钥信息;如果没输出来,说明没写进去。
如果目标 SSH 端口不是默认的 22,需要加-p参数:
ssh-keyscan -p 2222 -t ed25519 192.168.1.11 >> ~/.ssh/known_hosts这时的 known_hosts 里记录的主机名会带有端口号,格式类似[192.168.1.11]:2222。这个细节容易忽略,后面排查会展开讲。
2.2 交互式确认方式的底层逻辑和注意事项
如果你不想用 ssh-keyscan,也想在第一次连接时人工确认指纹,那就老老实实看提示信息。SSH 客户端会显示三种 key 指纹的哈希(MD5/SHA256),你得自己判断这个值是否正确。判断依据通常是:
- 云厂商控制台一般会提供 host key 指纹(阿里云、腾讯云、AWS 都有这个字段)。
- 公司内部资产管理系统里如果登记了初始指纹,可以比对。
- 如果什么渠道都没有,那就只能按“首次连接信任”的宽松策略来了。
首次连接时的提示长这样:
The authenticity of host '192.168.1.10 (192.168.1.10)' can't be established. ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxx. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])?注意这个[fingerprint]选项,意思是你可以直接输入一个指纹值让客户端校验,而不只是无脑敲 yes。这在安全要求高、你又已经提前知道指纹的场景下非常有用。
输入 yes 后,记录会被写进~/.ssh/known_hosts。这个过程里 SSH 客户端会检查目标主机名/IP是否已经存在于文件中,如果存在但内容对不上,就会进入另一条“指纹冲突”的错误流程,而不是白白问你一句。
3. 批量场景下的指纹管理和自动化
3.1 内网上百台机器,如何一次搞定
真实工作中,运维手里一堆机器、开发要在十台跳板机上做免密登录、CI 环境要连几十个部署节点,这种“新IP加指纹”的需求如果靠一台台手动 yes,效率太低,而且不可复现。
批量抓取可以这样写:
for ip in $(cat server.list); do ssh-keyscan -t ed25519,rsa "$ip" 2>/dev/null >> ~/.ssh/known_hosts done但直接这样跑会有个问题:如果server.list里某个 IP 连不上,ssh-keyscan 可能超时很久(默认没有超时限制,会比较慢),而且多次运行脚本会导致 known_hosts 文件里出现大量重复记录。所以更稳妥的做法是:
cat server.list | xargs -P 10 -I {} ssh-keyscan -t ed25519,rsa {} 2>/dev/null >> ~/.ssh/known_hosts sort -u ~/.ssh/known_hosts -o ~/.ssh/known_hostsxargs -P 10表示并行10个进程去抓取,速度能快很多。sort -u用来去重,避免同一条记录被多次写入。
如果你管理的是几百上千台机器,或者想做到更规范的“集中式管理”,可以用 Ansible 分发 known_hosts 文件,思路是:先在一台管理机上生成好全量 known_hosts,然后通过 Ansible 推送到所有目标机器的/etc/ssh/ssh_known_hosts,实现全局信任同一批主机。这比每台机器各维护一份高效得多,也方便后续加新机器时统一更新。
3.2 StrictHostKeyChecking 和 UserKnownHostsFile 的正确用法
自动化场景里,除了 ssh-keyscan,OpenSSH 还提供了几个影响 host key 校验的配置项,这里重点说StrictHostKeyChecking和UserKnownHostsFile。
StrictHostKeyChecking有三个取值:
yes:最严格,如果主机不在 known_hosts 里直接拒绝连接。no:最宽松,自动接受新主机的 key 并写进 known_hosts,也不检查指纹是否变化。安全风险极大,千万不要在生产环境用。accept-new:介于两者之间,自动接受新主机的 key 并写入 known_hosts,但一旦 known_hosts 里已经有该主机的记录且指纹对不上,连接还是会失败。
对自动化任务来说,accept-new是一个比较实用的折中方案。第一次连接时不再交互确认,之后的连接保持校验:
ssh -o StrictHostKeyChecking=accept-new user@192.168.1.10在 CI/CD 流水线里,更规范的做法是使用独立的 known_hosts 文件,不污染运维人员本机的 known_hosts:
ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/known_hosts user@192.168.1.10UserKnownHostsFile指定一个临时文件路径,这个文件不存在也会自动创建,相当于把信任列表隔离在流水线项目内部。这个思路和 Docker 容器里跑 SSH 的场景也很契合——容器每次重建后是全新的文件系统,把 known_hosts 挂载成 Volume 或提前生成到镜像里,能避免每次构建都重新询问指纹。
提醒:
StrictHostKeyChecking=no看起来很省事,但它同时会禁用对 host key 变化时的告警,等于把 SSH 的防中间人机制关了。在同一局域网、公网环境里,这等于裸奔。就算是内网测试,我也建议用accept-new而不是no。
4. 实战中踩过的坑和排查经验
4.1 端口变化导致 keyscan 抓不到指纹
有一回一个测试环境把 SSH 端口从 22 改成了 2299,我没注意,直接跑ssh-keyscan 192.168.1.20,结果什么都抓不到。看 stderr 才发现它是去连的 22 端口,而那个端口已经没有任何服务了。
ssh-keyscan 默认连 22 端口。遇到非标端口必须显式指定:
ssh-keyscan -p 2299 192.168.1.20而且写入 known_hosts 后,后续连接也必须用一样的端口写法,比如ssh -p 2299 user@192.168.1.20。这时候文件里的记录会是[192.168.1.20]:2299,如果你用默认端口去连,SSH 客户端会去找192.168.1.20这条,找不到,又把它当成新IP提示确认。看起来是“明明加过了为什么还要问”,实际是端口号导致 known_hosts 里的 key 匹配不上。
4.2 服务器重装系统导致的 host key verification failed
这个坑很多人都碰到过。比如一台机器重装系统后,host key 全部重新生成,原来 known_hosts 里存的指纹对不上了,连接时报错:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Host key verification failed.正确做法是先把旧的记录删掉:
ssh-keygen -R 192.168.1.20如果你还用了非标端口,删除时也要带上端口说明:
ssh-keygen -R "[192.168.1.20]:2299"然后再重新连接生成新记录,或者用 ssh-keyscan 加回去。
这里要特别提醒:如果这台机器的域名和IP都指向同一个主机,known_hosts 里可能会有多条记录,改哪个删哪个要看清,别一股脑全删了再把合法的也丢了。可以用ssh-keygen -F 192.168.1.20和ssh-keygen -F host.example.com分别查看。最坏情况下,可以直接备份后清空 known_hosts,重新把所有常用主机连一遍,但这会干扰指纹校验的连续性,对安全敏感的环境不建议这么做。
4.3 known_hosts 文件权限错误导致的连接失败
一个很隐蔽的问题是 known_hosts 文件权限被改错了。比如你用chmod 777 ~/.ssh/known_hosts或者用 root 编辑过后再赋权给普通用户,SSH 客户端可能会拒绝加载这个文件,或者报这样的错误:
Bad owner or permissions on /home/user/.ssh/known_hosts修复方法:
chmod 600 ~/.ssh/known_hosts同时~/.ssh目录本身权限也建议保持 700,不然部分系统也会警告。这类问题在工作中出现频率不低,尤其是多用户共用同一台跳板机,有人图方便用 root 把文件拷来拷去,结果权限就乱了。
4.4 多条相同主机记录导致连接时指纹冲突
有时候因为人为编辑或者脚本重复执行,known_hosts 里同一台主机会有好几条不同类型的 key 记录。比如同时有ssh-rsa和ssh-ed25519两条,服务器端如果同时启用了两种 key,那没问题;但如果服务器端实际只返回其中一种,而你 known_hosts 里恰好另一种类型不匹配(比如服务器改了 host key 后旧记录没删干净),就会莫名其妙地报错。
排查手段就是ssh-keygen -F IP看当前匹配的记录,再配合grep IP ~/.ssh/known_hosts看全部相关行,有重复就用ssh-keygen -R IP清掉再重新添加。
5. 几个少见但实用的进阶玩法
5.1 同时记录域名和IP,减少不必要的确认
连接使用域名时,SSH 客户端默认会用你输入的域名去匹配 known_hosts。比如你ssh user@web01.example.com,它会找web01.example.com的记录,而不会自动拿 DNS 解析出来的 IP 去匹配。
实际情况是,很多机器既有域名又有固定IP,如果你一会儿用域名连,一会儿用IP连,第一次都会触发确认。解决办法是在抓取时同时写入域名和IP两种别名:
ssh-keyscan -t ed25519 web01.example.com 192.168.1.30 >> ~/.ssh/known_hostsssh-keyscan 支持一次传入多个主机名/IP,输出时会分别为每个名字生成一条记录。这样你在任何一端发起连接,都能命中已有记录,省去重复确认。
5.2 用 HashKnownHosts 隐藏主机名,兼顾安全和隐私
如果你细心一点,会发现有些系统(比如 macOS 默认就是开启的)known_hosts 里的主机名不是明文,而是一串哈希值。这是HashKnownHosts选项的作用,开启后即使 known_hosts 文件泄露,攻击者也没法一眼看出你连过哪些机器。
如果你希望自己生成的 known_hosts 也采用哈希形式,可以修改~/.ssh/config中的全局配置:
Host * HashKnownHosts yes然后重新生成或连接,后续写入的记录就会以哈希形式存储。注意,如果你需要手动ssh-keygen -R删除某台主机,即使文件里是哈希值,-R IP也能正常工作,因为它内部会做同样的哈希计算来匹配。
5.3 服务器 host key 轮换后如何平滑过渡
生产环境出于合规要求,可能需要定期轮换服务器的 host key(尤其是已经发生过 key 泄露的场景)。轮换后如果直接重启 sshd,所有客户端的 known_hosts 都会因为不匹配而报错,导致线上业务连接中断。
平滑的做法是:预先在服务器端生成好新的 host key,通过安全的带外渠道把新指纹通知到各客户端,客户端提前把旧记录删除、新记录预写入 known_hosts,再约定时间统一重启 sshd。整个过程可以配合 Ansible、SaltStack 之类的配置管理工具批量下发,避免业务窗口内出现大面积连接失败。
这个操作在网络上其实不太常见,因为多数团队是一台台敲命令处理的,但一旦机器多了,不搞平滑轮换,光是几十台机器的告警就能让人焦头烂额。
6. 与SSH其他易混淆概念的边界
6.1 known_hosts 和 authorized_keys 别搞混
刚开始用 SSH 的人很容易把这两个文件搞混:
known_hosts是用来验证“我连的服务器是不是真的服务器”,它存在客户端,解决的是“客户端信任服务器”的问题。authorized_keys是用来做免密登录的,它存在服务器的~/.ssh/下,解决的是“服务器信任客户端”的问题。
简单类比:known_hosts 是你手机里存的“房东钥匙指纹”,确认开门的人是不是房东;authorized_keys 是房东门禁系统里登记的“你的指纹”,用来识别你能不能进这扇门。方向不一样,千万别把这两个文件路径混淆,否则要么连不上,要么免密登录配置半天不生效。
6.2 StrictHostKeyChecking 和 PasswordAuthentication 是两码事
有时候改了 StrictHostKeyChecking 还是连不上,因为服务器端禁用了密码登录,或者密钥认证没配置好。这两个配置不在同一个层次:StrictHostKeyChecking 只管主机指纹校验,PasswordAuthentication 管的是登录认证方式。就算你把 host key 校验全部跳过,没有正确配置登录凭据,SSH 连接照样失败。
排查问题时先把这两件事分开:先确认“能不能连上”(网络、端口、host key 是否匹配),再确认“登录是否成功”(用户名、密码/密钥)。别在 known_hosts 上纠结半天,结果根本是密钥认证失败。
6.3 known_hosts 记录和 sshd 配置的 HostKey 指令
服务器端/etc/ssh/sshd_config里有HostKey指令,指定了 sshd 启动时加载哪些主机密钥文件,常见的是ssh_host_rsa_key、ssh_host_ecdsa_key、ssh_host_ed25519_key。当你发现 ssh-keyscan 抓取到的 key 类型和 known_hosts 里的记录不一致时,可以顺便检查一下服务器端 HostKey 配置,确认 sshd 到底在提供哪些类型的 host key。
还有一个很少人注意的情况:服务器新增或移除了某个 HostKey 文件后,重启 sshd 会改变它对外提供的密钥类型组合,客户端 known_hosts 里如果只存了某个类型,而新指纹缺失,也会导致认证报错。所以服务端在调整 host key 类型后,客户端一定要同步更新 known_hosts。
7. 我的经验总结和小建议
从第一次在实验室里无脑敲 yes,到现在管着几十台服务器的 SSH 信任链,我最大的感受是:known_hosts 的管理看起来是小得不能再小的事,但一旦出问题,要么连接失败得莫名其妙,要么被中间人攻击的风险闷在鼓里。平时多做一点规划,能让后面省掉很多麻烦。
我的个人习惯可以给大家参考:所有服务器的 key 抓取都用脚本批量完成,写入后立即ssh-keygen -F验证;自动化任务统一用StrictHostKeyChecking=accept-new,绝不妥协成no;每季度巡检一次 known_hosts,把已经不用的机器记录清掉,把变更过 IP 的记录更新到位;手动清理旧 key 时永远先用-F查一遍再动手,避免误删。
最后再分享一个小技巧:如果你不确定新抓的指纹对不对,可以把 keyscan 结果放到一个临时文件里,用ssh-keygen -lf查看可读指纹,再跟你掌握的实际指纹做比对。确认无误后再追加进正式的 known_hosts,这样既省掉了交互确认的麻烦,又不会盲目信任一个“陌生人”。
ssh-keyscan -t ed25519 192.168.1.40 > /tmp/new_hostkey.pub ssh-keygen -lf /tmp/new_hostkey.pub # 比对指纹无误后 cat /tmp/new_hostkey.pub >> ~/.ssh/known_hosts这套流程我用了很久,稳妥且高效,遇到问题也知道从哪下手排查,希望能给你们省点时间。