1. 从一次“惊魂”连接说起:为什么我们需要 known_hosts?
我记得几年前,有一次我需要紧急登录一台客户的服务器去排查一个线上问题。那台服务器我之前没连过,所以很自然地,我在终端里敲下了 ssh root@server_ip。命令输完,回车,屏幕上没有立刻出现熟悉的密码输入提示,而是跳出来几行让我心里“咯噔”一下的文字:
The authenticity of host 'xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx)' can't be established.
ECDSA key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no)?
当时情况紧急,我脑子里只想着快点进去看日志,差点就下意识地打了 yes。但就在手指要按下去的那一刻,我停住了。我忽然想起来,这台服务器的管理员前几天好像提过一句,他们刚做过系统升级。会不会是重装系统导致密钥变了?还是说,我输错了IP,连到了别的机器上?又或者,更糟糕的情况——我正遭遇传说中的“中间人攻击”?
最后,我赶紧打了个电话确认,果然是服务器重装后SSH主机密钥重置了。虚惊一场,但也给我上了一课:这个小小的提示,以及背后默默工作的 known_hosts 文件,是我们网络安全的第一道,也是极其重要的一道手动确认防线。它就像一个你信任的通讯录,记录着你每个“朋友”(服务器)独一无二的“声纹”(公钥)。下次“朋友”来电,它会先核对一下声纹,对上了才让你通话,对不上就立刻报警。
那么,这个 ~/.ssh/known_hosts 文件到底是什么?它凭什么能判断对方是敌是友?今天,我就结合自己这些年踩过的坑和积累的经验,带你彻底搞懂它。你会发现,这个看似简单的文本文件,其实蕴含着保证SSH连接安全的精巧设计。
2. 拆解 known_hosts:一行记录里的安全密码
别被它的名字吓到,known_hosts 文件本质上就是一个文本文件,你可以用任何编辑器打开它看看。它的位置就在你用户目录下的 .ssh 文件夹里(~/.ssh/known_hosts)。里面每一行,都代表一个你曾经信任并连接过的主机。我们随便拿出一行来“解剖”一下:
|github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/4wCEjQYolqY1y67iAwSywGeybkcw==
这一长串看起来有点乱,但其实结构非常清晰,主要分为三个核心部分,我把它叫做“主机身份证的三要素”。
2.1 主机标识:它到底是谁?
第一部分,就是开头的 github.com。这叫做 主机标识。它的作用就是告诉系统:“这条记录是关于哪个主机的”。它可以是以下几种形式:
- 主机名:就像上面的
github.com,或者server.alibaba.com。这是最直观的方式。 - IP地址:比如
192.168.1.100。当你的网络配置里没有DNS解析,或者你更习惯直接用IP连接时,就会记录IP。 - 主机名和IP的组合:有时候你会看到
github.com,192.30.255.112这样的格式,用逗号隔开。这表示“无论是通过主机名github.com还是IP192.30.255.112连接,都适用同一条公钥记录”。这很实用,因为一个服务器可能对应多个访问入口。 - 哈希值(Hashed):这是一种安全增强措施。你可能会看到像
|1|F1WbJqKJq7U9QzVXyZwRkQ==|rAbWcD12EeF3GhIjk=...这样以|1|开头的乱码。这是对主机名或IP进行了 HMAC-SHA1 哈希处理后的结果。这么做的目的是保护隐私,即使你的known_hosts文件被他人窃取,攻击者也无法直接知道你都连接过哪些主机。你可以通过ssh-keygen -H -f ~/.ssh/known_hosts命令来哈希化现有的文件。
我踩过的坑:早期我管理服务器集群时,有些服务器既通过域名也通过内网IP访问。有一次服务器迁移,IP变了但主机名没变。我用域名连接一切正常,但用旧IP连接时就出现了可怕的“密钥已更改”警告。就是因为 known_hosts 里同时记录了域名和旧IP的组合。后来我学乖了,在自动化脚本中,尽量使用唯一且持久的主机标识(比如内部DNS域名),避免因IP变动带来的麻烦。
2.2 公钥类型:用的是哪种“锁”?
第二部分,紧跟在主机标识后面的 ssh-rsa。这叫做 公钥类型 或 密钥类型。它指明了后面那串长长的公钥是使用哪种加密算法生成的。常见的类型有:
ssh-rsa:最经典、支持最广泛的RSA算法。历史上最常用,但现在出于安全性考虑,较新版本的OpenSSH可能会默认禁用较小密钥长度(如1024位)的RSA。ssh-dss(DSA):数字签名算法。现在已不推荐使用,因为存在安全弱点。ecdsa-sha2-nistp256/ecdsa-sha2-nistp384/ecdsa-sha2-nistp521:基于椭圆曲线密码学的ECDSA算法。在相同安全强度下,它比RSA的密钥短得多,处理速度也更快,是当前的主流推荐之一。ssh-ed25519:基于Edwards-curve的EdDSA算法。这是目前安全性和性能综合表现最好的选择,密钥短,速度快,且设计上规避了许多传统算法的陷阱。我现在的个人服务器和重要环境,都首选强制使用ed25519密钥。
这个类型就像是一把锁的型号。你不能拿RSA的钥匙去开Ed25519的锁。客户端在验证时,必须使用对应类型的算法来处理公钥。
2.3 公钥内容:独一无二的“指纹”
第三部分,就是后面那串从 AAAAB3Nza... 开始一直到行尾的巨长字符串。这就是 公钥内容 本身,是经过Base64编码后的二进制数据。它是主机密钥对中的公钥部分。
你可以把它想象成一把锁的唯一齿形。世界上没有两把齿形完全相同的锁(在密码学意义上,碰撞概率极低)。服务器持有对应的私钥(钥匙),当它需要向你证明“我是我”时,会用私钥对一段数据进行签名。你的SSH客户端拿到签名后,就用 known_hosts 里存的这把“锁”(公钥)去尝试解开签名。如果能成功解开并验证数据有效,就证明对方确实拥有匹配的私钥,身份无误。
一个非常实用的命令:我们不需要肉眼去对比那串长长的Base64码。OpenSSH提供了 ssh-keygen -l -f ~/.ssh/known_hosts 命令来列出所有已知主机的指纹。指纹是公钥的一个哈希摘要(通常是SHA256),像一串短很多的“指纹ID”,方便人类核对。第一次连接服务器时,如果管理员能提供给你一个正确的指纹让你比对,那么你就能完全确认主机的真实性,这是防御中间人攻击最可靠的手动方法。
3. 验证机制全景解析:一次SSH连接背后的“暗战”
知道了 known_hosts 的构成,我们来看看在你敲下 ssh 命令后,到成功登录前,它到底参与了怎样一场静默的安全验证“暗战”。这个过程远比我们想象的要有趣。
3.1 初次邂逅:信任的建立
当你第一次连接一台新主机(比如 ssh user@new.server.com)时,会发生以下步骤:
- 客户端发起连接:你的SSH客户端向
new.server.com的22端口发起TCP连接。 - 服务器出示“身份证”:服务器将其SSH主机密钥对的公钥(比如一个
ssh-ed25519公钥)发送给你的客户端。 - 客户端查“通讯录”:客户端立刻去翻阅本地的
~/.ssh/known_hosts文件,查找是否有关于new.server.com的记录。 - “查无此人”:因为是第一次连接,所以肯定找不到记录。这时,客户端不会贸然放行。
- 弹出安全警告:你会在终端看到我们文章开头提到的那段提示。它核心告诉你两件事:第一,我无法验证这个主机的身份;第二,这是它的公钥指纹(SHA256格式)。这是整个SSH安全体系中,依赖“人”来判断的关键一步!
- 你的抉择时刻:你必须做出选择:
yes:表示“我信任这个主机”。客户端会将new.server.com和收到的公钥一起,作为新的一行写入known_hosts文件。从此,它就成了你的“熟人”。no:拒绝连接,流程终止。
这里有个重要细节:为什么不是直接相信,而要你手动确认?因为此时无法区分对方是真正的 new.server.com,还是一个伪装成它的中间人攻击者。攻击者完全可以拦截你的连接请求,然后冒充目标服务器,把他自己的公钥发给你。如果你盲目信任,就会把攻击者的公钥存进“通讯录”,以后所有连接实际上都连到了攻击者那里,他可以在中间窃听甚至篡改所有数据。所以,第一次连接的信任,必须基于带外验证,比如通过电话、加密聊天工具等另一个可信渠道,向服务器管理员核实那个指纹是否正确。
3.2 再次相见:自动化的身份核验
当你第二次及以后连接 new.server.com 时,流程就变得自动化且快速了:
- 客户端发起连接。
- 服务器发送它的公钥。
- 客户端查阅
known_hosts文件,成功找到了new.server.com的记录。 - 关键比对:客户端将文件中存储的公钥与本次服务器发来的公钥进行逐字节比对。
- 结果分支:
- 匹配成功:客户端确认主机身份无误,验证通过。接下来继续进行用户身份验证(密码或密钥登录)。这个过程用户无感知,连接迅速建立。
- 匹配失败:警报响起! 你会看到著名的 “WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!” 警告。连接会被强制中断。这通常意味着以下几种情况之一:
- 服务器端正常变更:服务器操作系统重装、SSH服务重置、主机密钥轮换。
- 网络配置变更:你连接的IP地址背后换了一台不同的机器(比如云服务器换了实例)。
- 最危险的情况:你正在遭受中间人攻击。
遇到警告时,切勿轻易跳过!正确的做法是,首先通过其他可信渠道联系服务器管理员,确认密钥变更是否合法。如果合法,你需要使用 ssh-keygen -R new.server.com 命令从 known_hosts 中删除旧记录,然后重新连接并接受新密钥。
3.3 深入原理:挑战-响应与数字签名
光说比对公钥可能还有点抽象,我们稍微深入一点点,看看服务器到底是如何用私钥证明自己的。这涉及一个经典的密码学概念:数字签名。
在SSH协议交换的早期阶段,服务器为了证明它拥有与 known_hosts 中公钥对应的私钥,会进行一个“挑战-响应”:
- 客户端生成一个随机的“挑战”字符串。
- 客户端将这个挑战字符串发送给服务器。
- 服务器用它的主机私钥对这个挑战字符串进行数字签名。签名过程利用了私钥的唯一性和保密性。
- 服务器将签名结果发回给客户端。
- 客户端用
known_hosts中存储的该服务器的公钥,去尝试验证这个签名。 - 如果验证成功,说明签名确实是由匹配的私钥生成的,从而证明了服务器的身份。如果失败,则连接终止。
所以,known_hosts 文件提供的公钥,本质上是用来验证服务器数字签名的“验签器”。整个机制的精妙之处在于,公钥可以公开,私钥绝对保密。公开的公钥不会泄露任何能推导出私钥的信息,却能有效验证私钥的持有者。
4. 实战管理与疑难排坑
理解了原理,我们来看看日常工作中怎么高效、安全地管理这个文件,以及如何处理那些让人头疼的警告。
4.1 日常管理命令手册
你不需要手动编辑 known_hosts 文件,OpenSSH提供了一套完整的工具链:
-
查看已知主机列表及指纹:
ssh-keygen -l -f ~/.ssh/known_hosts这个命令会列出文件中所有主机及其对应的指纹(默认SHA256),方便你审计。
-
删除特定主机的旧记录(当密钥变更后):
ssh-keygen -R hostname_or_ip例如
ssh-keygen -R github.com或ssh-keygen -R 192.168.1.100。这是处理“主机密钥已更改”警告的首选安全方法,比手动编辑文件更可靠。 -
将整个 known_hosts 文件哈希化(保护隐私):
ssh-keygen -H -f ~/.ssh/known_hosts执行后,文件中的主机标识部分会被哈希值替代。注意:哈希化是单向的,操作前最好备份原文件。哈希化后,
ssh-keygen -l命令依然可以工作。 -
在连接时忽略 known_hosts 检查(极度危险,仅用于特定测试):
ssh -o StrictHostKeyChecking=no user@host强烈警告:这会完全禁用主机密钥验证,使你暴露在中间人攻击之下。仅在封闭的、绝对可信的测试环境(如一次性容器、虚拟局域网内)中临时使用,并切记不要用于生产环境或连接互联网主机。
4.2 常见警告与解决方案
-
场景一:“Host key verification failed.” 或 “WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!”
- 原因:
known_hosts中记录的密钥与当前服务器提供的密钥不匹配。 - 解决步骤:
- 保持冷静,不要盲目输入
yes。 - 通过电话、钉钉、企业微信等其他可信渠道,联系该服务器的管理员,询问主机密钥是否合法变更(例如,服务器是否重装、迁移、密钥是否主动轮换)。
- 如果变更合法,在本地使用
ssh-keygen -R 主机名删除旧记录,然后重新连接,接受新密钥。 - 如果管理员确认密钥不应改变,那么你的网络可能存在中间人攻击或DNS劫持,应立即停止连接并检查网络环境。
- 保持冷静,不要盲目输入
- 原因:
-
场景二:自动化脚本(如Ansible、CI/CD流水线)首次连接失败
- 原因:脚本在非交互模式下运行,遇到未知主机提示时会卡住。
- 解决方案(按推荐度排序):
- 预填充 known_hosts(推荐):在运行自动化脚本前,先手动或用脚本以交互方式连接一次目标主机,将公钥录入
known_hosts。或者,使用ssh-keyscan工具安全地获取公钥并追加到文件:
但请注意,ssh-keyscan -H target_host >> ~/.ssh/known_hostsssh-keyscan本身不验证主机身份,所以你应确保在安全的初始环境下(如通过控制台直接获取公钥指纹进行比对后)执行此操作。 - 使用自定义的已知主机文件:通过SSH配置
-o UserKnownHostsFile=/path/to/custom_known_hosts指定一个单独的已知主机文件,便于管理。 - 临时禁用检查(仅限绝对可信的内网环境):在Ansible命令或脚本中设置环境变量
ANSIBLE_HOST_KEY_CHECKING=False,或SSH参数-o StrictHostKeyChecking=no。务必清楚其安全风险。
- 预填充 known_hosts(推荐):在运行自动化脚本前,先手动或用脚本以交互方式连接一次目标主机,将公钥录入
-
场景三:known_hosts 文件条目重复或混乱
- 原因:同一主机用不同标识(IP、域名)多次连接,或文件被手动编辑损坏。
- 解决:使用
ssh-keygen -l -f查看,用ssh-keygen -R清理不需要的条目。保持文件整洁是良好习惯。
5. 安全进阶:超越 known_hosts 的思考
known_hosts 是SSH安全模型的基石,但它主要解决的是“服务器身份验证”问题。要构建更强大的SSH安全体系,我们还需要考虑其他层面。
第一层:用户身份验证。 这是 known_hosts 之后的下一个步骤。服务器验证完了,该你向服务器证明你是谁了。常见方式有:
- 密码登录:最基础,但易受暴力破解和嗅探攻击。
- 公钥登录(推荐):你在客户端生成一对密钥(
ssh-keygen),将公钥(id_rsa.pub)上传到服务器的~/.ssh/authorized_keys文件中。登录时,客户端用私钥签名,服务器用公钥验证。这比密码安全得多,且可实现免密登录。我强烈建议为所有服务器配置公钥登录,并给私钥加上强密码短语(passphrase)。
第二层:网络层加固。 known_hosts 防不住网络窃听(虽然SSH传输本身是加密的)。可以通过以下方式加固:
- 禁用密码登录:在服务器SSH配置(
/etc/ssh/sshd_config)中设置PasswordAuthentication no,强制使用密钥登录。 - 更改默认端口:将
Port 22改为一个非标准端口,可以减少自动化扫描工具的骚扰。 - 使用防火墙限制源IP:只允许特定的、可信的IP地址访问服务器的SSH端口。
- 使用Fail2ban等工具:自动封锁多次尝试失败IP地址。
第三层:审计与监控。 仅仅依靠静态的 known_hosts 文件还不够。
- 集中化管理SSH密钥:在大型企业,可以使用像HashiCorp Vault、Teleport或商业化的SSH密钥管理系统,集中颁发、轮换和吊销主机密钥与用户密钥,彻底告别分散的
known_hosts和authorized_keys文件。 - 启用详细日志:配置SSH服务端记录详细的认证日志(
LogLevel VERBOSE),并接入SIEM(安全信息和事件管理)系统进行异常登录分析。
回过头看,known_hosts 文件的设计体现了“信任第一次,验证每一次”的朴素安全哲学。它把最初建立信任的重大责任交给了用户自己(那个 yes/no 的提示),之后则通过自动化的密码学验证来保障每次连接的安全。这个小小的文件,就像一位忠实的门卫,默默守护着每一次远程登录的入口。理解它、用好它、管理好它,是你迈向服务器安全运维扎实的第一步。下次再看到那个警告提示时,希望你能会心一笑,从容应对,因为你知道,这背后是一场精心设计的安全守护。

925

被折叠的 条评论
为什么被折叠?



