从 known_hosts 到 authorized_keys:ssh 双向身份认证深度解析

双向身份认证

在进行 ssh 加密通讯的过程中,避免不了去接触公钥私钥这样的密钥对。

  • 如果你在使用服务器的过程中重装过系统,可能你也会知道 ~/.ssh/known_hosts 这个文件。
  • 如果你配置了密钥登录服务器,可能你会知道 ~/.ssh/authorized_keys 这个文件。

当登录一个服务器的时候,需要先后两步:

  1. 验证服务器的身份(确认服务器是真实的,而非中间人伪造)
  2. 向服务器证明自己的身份(证明自己有权限登录)

你可能知道配置密钥登录服务器,是配置了一对密钥,公钥在服务器上,私钥在本地,本质是验证登录的权限。那么这其实就是第二步,你向服务器去证明自己的身份。

实际上,当你第一次登录服务器或者重装系统后登录服务器,会提示你验证服务器公钥指纹,当你确定后,会将这个公钥写到 ~/.ssh/known_hosts 这个文件中。这其实是服务器中的密钥对,当你拿到公钥,服务器上有私钥,此时就可以去验证服务器的身份了。

客户端必须先通过服务器的身份认证(先确认对方是真实服务器),才会发起用户身份认证(再证明自己有权登录);反之,若服务器身份存疑(如公钥不匹配),客户端会直接终止连接,不会进行后续的用户认证。

两者共同确保了 “双向可信”:用户信任服务器是真实的,服务器信任用户是有权限的,在此基础上才能建立安全的加密通信。

这就是 SSH 最核心的安全设计 ——双向身份认证

  • 服务器通过 “公钥给客户端、私钥自己留存” 证明自己的真实性。
  • 用户通过 “私钥在本地、公钥在服务器” 证明自己的权限。

known_hosts 和 authorized_keys 分别是这两个过程的 “凭证存储文件”:

  • 服务器公钥存储在用户端的 known_hosts 文件中。(用户验服务器)
  • 用户端公钥存储在服务器的 authorized_keys 文件中。(服务器验用户)

公钥指纹

基于 SSH 的双向身份认证,一般会先验证服务器的身份,再向服务器证明自己的身份。

验证服务器身份的方式其实也是通过密钥,这个密钥是服务器上的,将服务器上的密钥对中的公钥给到用户,然后进行验证。在用户第一次请求服务器公钥的时候,通常会碰到如下界面:

图片[1]-从 known_hosts 到 authorized_keys:ssh 双向身份认证深度解析 - 渔网札记-渔网札记

本质就是问你是否信任这个公钥指纹,这其实就是服务器中通过 ED25519 算法生成的密钥对中的公钥,不过这里显示的字符串是这个公钥的指纹。

一般情况下,我们都会无脑输入 yes,然后这个公钥就会存入到本地 ~/.ssh/known_hosts 文件当中,后续登录遍不会再询问,而是直接用这里面存储的公钥进行服务器身份的验证。

但是如果这个时候有一个人,他截获了我们获取服务器公钥的请求,然后他去请求了服务器的公钥,在他拿到公钥后,用他自己的公钥给我们,我们还以为这就是服务器的公钥。至此以后,我们跟服务器之间的会话通讯都会经过他。这就是著名的中间人攻击(Man-in-the-middle attack)

中间人攻击的解决方案:将发送过来的公钥指纹和服务器中的公钥进行比对。

公钥指纹的意义:使用 ssh-keygen 工具将服务器的公钥转换成指纹,就可以直观且方便的对比。

站在公司员工的场景来看:

一般只能通过寻求管理员将服务器的公钥文件发给你,然后进行对比,最好的情况是管理员直接发给你公钥指纹。

图片[2]-从 known_hosts 到 authorized_keys:ssh 双向身份认证深度解析 - 渔网札记-渔网札记

而站在管理员的场景来看:

我要不就登录服务器去下载文件,或者登录服务器将公钥使用 ssh-keygen -lf 拿到公钥指纹给员工怎样都要登录服务器。

实际上可以借助 ssh-keyscan 这个工具直接拿到服务器的公钥信息,再通过拿到的公钥用 ssh-keygen 计算出公钥指纹,全程不用登录服务器,但是需要服务器的 IP。

图片[3]-从 known_hosts 到 authorized_keys:ssh 双向身份认证深度解析 - 渔网札记-渔网札记

首先通过 ssh-keyscan server_ip 拿到服务器上的所有主机公钥,然后将拿到的内容通过管道输送给 ssh-keygen -lf 进行处理,但是这个 -lf 选项是从文件中读取内容计算指纹的,而通过 ssh-keyscan 拿到的内容原本是输出在显示器上的,我们将其通过管道输送给 ssh-keyscan 作为它的输入,让它进行处理。故此在后面跟上一个连字符 -,它的意思是不要从文件读取了,从标准输入中读取内容。

整条命令 ssh-keyscan server_ip | ssh-keygen -lf - 的执行流程如下:

  1. ssh-keyscan 首先运行,成功获取到服务器 server_ip 的所有主机公钥。
  2. ssh-keyscan 本来打算把这些公钥文本打印到你的屏幕上,但管道 | 拦截了这一切。
  3. 管道 | 把这些公钥文本,原封不动地传送给了 ssh-keygen 命令。
  4. ssh-keygen -lf - 命令启动,它被告知要计算指纹(-l),并且从标准输入(-)而不是具体文件(-f)中读取公钥数据。
  5. ssh-keygen 接收到从管道传来的公钥文本,逐一计算出它们的指纹,并最终将这些指纹打印到你的屏幕上。

加密算法协商

确定接受指纹后,会将多个公钥写到本地 ~/.ssh/known_hosts 文件中,而非一个公钥。而这些公钥是以不同的算法生成的密钥,命名格式基本都是 ssh_host_*_key 的形式。

图片[4]-从 known_hosts 到 authorized_keys:ssh 双向身份认证深度解析 - 渔网札记-渔网札记

既然如此,这些密钥的目的很明确,就是要告诉你,我这个密钥匙是哪一个算法生成的。

服务器就像一个外交官,我掌握多国的语言,然后我会判断你说的哪一种语言来和你去进行交流,同样你也可能会多国语言,但是我一定会选择你的母语来跟你进行交流,这是最优的选择。

当你去访问服务器的时候,你会告诉服务器我都支持哪些算法,而服务器会选择一个相对双方都最好的算法(通常是最安全)来对本次会话进行加密。

服务器保留着所有这些不同类型的密钥,是为了确保无论客户端支持哪种算法,总有一款能匹配上,并且尽可能地使用最安全的那一种。

  • ssh_host_ed25519_key: 最现代、最高效、最推荐的密钥类型。
  • ssh_host_ecdsa_key: 基于椭圆曲线的密钥,比 RSA 安全,但速度可能不如 Ed25519。
  • ssh_host_rsa_key: 最传统、兼容性最好的密钥,为了照顾一些非常老旧的客户端。

这就是所谓的加密算法协商 (Algorithm Negotiation)

我不禁有一个疑惑。既然协商后只用一个密钥,为什么要把服务器所有的公钥都保存到本地?

事实上,老版本的 SSH 客户端正是如我想的这样工作的,它只保存协商成功的那一个密钥。

这就导致,如果服务器端的管理员出于某种原因禁止掉了这个跟你通讯的算法,换成了其他的加密算法,就会出现严重的问题。

故此,后来就会在第一次登录服务器的时候,把服务器所有支持的算法生成的公钥保存到本地。这样就算服务器禁止掉了其中一个加密算法,还可以平滑过渡到其他的加密算法。

这就是现代 OpenSSH 客户端(大约从 6.8 版本开始)所做的事情:

  1. 获取所有公钥: 在初次连接的密钥交换阶段,客户端不仅接收用于协商的那个公钥,还会请求服务器“请把你所有有效的主机公钥都告诉我”。
  2. 用户验证一个: 客户端会选择一个最优先的公钥(比如 ed25519)生成指纹,并呈现给你进行人工确认。
  3. 保存所有公钥: 当你输入 yes 确认后,客户端会将从服务器收到的所有类型的公钥(ed25519, ecdsa, rsa等)都记录到你的 ~/.ssh/known_hosts 文件中。

你可以打开你的 ~/.ssh/known_hosts 文件看一眼,对于同一个IP或主机名,你很可能会看到类似下面这样的三行记录:

116.205.170.29 ssh-rsa AAAA...
116.205.170.29 ecdsa-sha2-nistp256 AAAA...
116.205.170.29 ssh-ed25519 AAAA...

故此,客户端之所以保存所有公钥,是一种非常智能的“预缓存”行为,它用首次连接时的一次确认,换取了未来在服务器算法升级或变更时连接的健壮性和连续性。

© 版权声明
喜欢就支持一下吧
点赞279赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码快捷回复

    暂无评论内容