How to Use SSH Keys for Secure Passwordless Connections

如何使用 SSH 密钥进行安全的无密码连接

前言

还记得刚开始向 GitHub 推送代码时,首要任务就是通过设置 HTTP 或 SSH 认证方式来执行 git 操作。由于很早以前设置好后就忘了当时的具体操作,最近在处理服务器连接时,又想到应该深入理解并记录其背后的原理与相关安全机制。

为什么不用密码就好?

SSH 本身支持密码登录,但密码认证存在几个先天问题:

  • 密码会被传输:即使传输过程经过加密,密码本身仍然会被发送到服务器端,服务器有机会接触到它。
  • 可被暴力破解:对外开放 22 端口的机器,每天都会收到大量自动化的字典攻击尝试。

SSH 密钥认证要解决的核心问题是:如何在不发送秘密的前提下,证明自己拥有这个秘密。

非对称加密的基础

SSH 密钥是一组非对称加密密钥对(Key Pair),由数学上相互关联的两把密钥组成:

  • 私钥(Private Key):~/.ssh/id_ed25519,始终保存在自己的计算机上,绝对不能泄露。
  • 公钥(Public Key):~/.ssh/id_ed25519.pub,可以随意公开,并复制到任何想要登录的服务器上。

这组密钥的关键特性是:私钥可以为数据生成签名,而任何人都能使用公钥验证该签名是否确实由对应的私钥生成,但反过来无法从公钥推导出私钥。

一次 SSH 连接中发生了什么

完整的连接可以分为两个阶段:先建立加密通道,再进行身份认证。

第一阶段:建立加密通道

在进行任何认证之前,客户端与服务器会先协商建立一条加密通道:

  1. 双方交换各自支持的算法列表(密钥交换、加密、MAC、压缩)
  2. 通过 Diffie-Hellman(或目前常见的 ECDH)进行密钥交换,双方各自计算出一把只有彼此知道的对称密钥(会话密钥),而这把密钥从未在网络上传输过
  3. 该过程还会生成一个唯一的会话 ID,后续认证阶段会用到
  4. 此后的所有数据包都会使用这把会话密钥,通过对称加密(如 AES-GCM、ChaCha20-Poly1305)进行保护

主机密钥:你连接的真是那台机器吗?

在交换密钥的同时,服务器也会使用自己的私钥进行签名,以证明自己的身份。这就是首次连接时会看到的提示:

Terminal window
无法确认主机 'example.com (93.184.216.34)' 的真实性。
ED25519 密钥指纹为 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx。
是否确定要继续连接(yes/no/[fingerprint])?

输入 yes 后,这台主机的公钥指纹会被记录到 ~/.ssh/known_hosts。下次连接时,如果指纹不匹配,SSH 会直接拒绝连接并发出警告,因为这可能意味着遭遇了中间人攻击。

第二阶段:公钥认证

加密通道建立后,才开始证明「是谁在登录」:

  1. 客户端:我想使用这把公钥登录,你接受吗?(仅发送公钥)
  2. 服务器:在该用户的 ~/.ssh/authorized_keys 中查找是否存在这把公钥,如果没有则拒绝。
  3. 客户端:使用私钥对一段包含会话 ID、用户名、服务名称……等信息的数据生成签名。
  4. 服务器:使用 authorized_keys 中的那把公钥验证签名。验证通过 = 对方确实持有对应的私钥 = 登录成功。

其中有两个关键设计:

  1. 私钥从始至终都没有离开过本机,即使服务器遭到入侵,也只会泄露公钥。
  2. 签名内容绑定了会话 ID,每次连接的会话 ID 都不同,因此攻击者截获的签名无法用于对另一个连接发起重放攻击。

实际操作

生成密钥

OpenSSH 广泛应用于所有主流操作系统,并提供 ssh-keygen 及其他工具来生成和管理密钥对。

Terminal window
ssh-keygen -t ed25519 -C "公钥末尾的注释"

虽然常见教程使用的是 -t rsa -b 4096,但现在建议优先使用 Ed25519:密钥更短、签名更快、安全强度相当于 RSA-3072 以上,而且不存在 RSA 参数容易配置错误的问题。

生成的两个文件:

Terminal window
~/.ssh/id_ed25519 # 私钥,绝不能泄露
~/.ssh/id_ed25519.pub # 公钥,可以公开

生成过程中会询问密码短语,强烈建议务必设置:设置后,私钥文件才会以加密形式存储,密码短语就是解密它的密码。万一笔记本电脑被盗或私钥文件泄露,攻击者在没有密码短语的情况下仍然无法使用它;如果直接按 Enter 留空,私钥将以明文形式存储,拿到文件就相当于拿到了账号。

使用 ssh-agent 避免每次输入密码短语

设置密码短语后,每次连接都要输入会很麻烦。ssh-agent 是一个常驻后台的程序,它会将解密后的私钥保存在内存中,并代为执行签名:

Terminal window
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

macOS 可以将密码短语存入 Keychain,并在 ~/.ssh/config 中设置首次使用密钥时自动将其加入 agent:

Terminal window
Host *
IgnoreUnknown UseKeychain
UseKeychain yes
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519

将公钥放到远程服务器

authorized_keys 是一个纯文本文件,每行存放一把公钥。手动粘贴很容易出错(换行、权限),建议使用 ssh-copy-id:

Terminal window
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@hostname

它所做的事情,就是使用现有方式(通常是密码)登录一次,然后将公钥追加到远程服务器的 ~/.ssh/authorized_keys 中,并修正文件权限。

连接

Terminal window
ssh -i ~/.ssh/id_ed25519 user@hostname

与其每次都输入一长串命令,更实用的方式是将配置写入 ~/.ssh/config:

Terminal window
Host myserver
HostName 93.184.216.34
User ubuntu
Port 22
IdentityFile ~/.ssh/id_ed25519

之后只需执行 ssh myserver 即可,scp、rsync、git 也都会读取这份配置。

文件权限

SSH 对权限的要求非常严格。如果权限过于宽松,它会直接拒绝使用该密钥(因为同一台机器上的其他用户可能也能读取):

Terminal window
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/authorized_keys

典型的错误信息:

Terminal window
'~/.ssh/id_ed25519' 的权限 0644 过于宽松。
私钥文件不得被其他用户访问。

搭配 GitHub 使用

GitHub 的 SSH 认证使用的是同一套机制,区别仅在于 GitHub 不会向你提供 Shell,而是通过公钥判断「这次 push 是由哪个账号发起的」:

  1. 将 ~/.ssh/id_ed25519.pub 的内容粘贴到 GitHub 的 Settings → SSH and GPG keys。
  2. 测试连接:
Terminal window
ssh -T git@github.com
# 你好,username!你已成功通过身份认证,但 GitHub 不提供 Shell 访问权限。
  1. 之后 clone 时使用 SSH URL(git@github.com:user/repo.git),而不是 HTTPS URL。

调试时,-v(可以叠加到 -vvv)会输出完整的协商与认证过程,从中可以清楚看到它尝试了哪些密钥,以及为什么被拒绝:

Terminal window
ssh -vT git@github.com

总结

  • SSH 连接分为两个阶段:先通过密钥交换建立对称加密通道,再进行身份认证。
  • 主机密钥验证的是「服务器是不是它本人」,公钥认证验证的是「你是不是你本人」。
  • 签名绑定了会话 ID,因此无法被重放。
  • 新密钥应优先选择 Ed25519,务必设置密码短语,并搭配 ssh-agent 使用。
  • 私钥泄露等同于账号泄露。一旦发生,应立即从所有 authorized_keys 和 GitHub 中移除对应的公钥,并重新生成密钥对。

延伸阅读