前言
还记得刚开始向 GitHub 推送代码时,首要任务就是通过设置 HTTP 或 SSH 认证方式来执行 git 操作。由于很早以前设置好后就忘了当时的具体操作,最近在处理服务器连接时,又想到应该深入理解并记录其背后的原理与相关安全机制。
为什么不用密码就好?
SSH 本身支持密码登录,但密码认证存在几个先天问题:
- 密码会被传输:即使传输过程经过加密,密码本身仍然会被发送到服务器端,服务器有机会接触到它。
- 可被暴力破解:对外开放 22 端口的机器,每天都会收到大量自动化的字典攻击尝试。
SSH 密钥认证要解决的核心问题是:如何在不发送秘密的前提下,证明自己拥有这个秘密。
非对称加密的基础
SSH 密钥是一组非对称加密密钥对(Key Pair),由数学上相互关联的两把密钥组成:
- 私钥(Private Key):
~/.ssh/id_ed25519,始终保存在自己的计算机上,绝对不能泄露。 - 公钥(Public Key):
~/.ssh/id_ed25519.pub,可以随意公开,并复制到任何想要登录的服务器上。
这组密钥的关键特性是:私钥可以为数据生成签名,而任何人都能使用公钥验证该签名是否确实由对应的私钥生成,但反过来无法从公钥推导出私钥。
一次 SSH 连接中发生了什么
完整的连接可以分为两个阶段:先建立加密通道,再进行身份认证。
第一阶段:建立加密通道
在进行任何认证之前,客户端与服务器会先协商建立一条加密通道:
- 双方交换各自支持的算法列表(密钥交换、加密、MAC、压缩)
- 通过 Diffie-Hellman(或目前常见的 ECDH)进行密钥交换,双方各自计算出一把只有彼此知道的对称密钥(会话密钥),而这把密钥从未在网络上传输过
- 该过程还会生成一个唯一的会话 ID,后续认证阶段会用到
- 此后的所有数据包都会使用这把会话密钥,通过对称加密(如 AES-GCM、ChaCha20-Poly1305)进行保护
主机密钥:你连接的真是那台机器吗?
在交换密钥的同时,服务器也会使用自己的私钥进行签名,以证明自己的身份。这就是首次连接时会看到的提示:
无法确认主机 'example.com (93.184.216.34)' 的真实性。ED25519 密钥指纹为 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx。是否确定要继续连接(yes/no/[fingerprint])?输入 yes 后,这台主机的公钥指纹会被记录到 ~/.ssh/known_hosts。下次连接时,如果指纹不匹配,SSH 会直接拒绝连接并发出警告,因为这可能意味着遭遇了中间人攻击。
第二阶段:公钥认证
加密通道建立后,才开始证明「是谁在登录」:
- 客户端:我想使用这把公钥登录,你接受吗?(仅发送公钥)
- 服务器:在该用户的
~/.ssh/authorized_keys中查找是否存在这把公钥,如果没有则拒绝。 - 客户端:使用私钥对一段包含会话 ID、用户名、服务名称……等信息的数据生成签名。
- 服务器:使用
authorized_keys中的那把公钥验证签名。验证通过 = 对方确实持有对应的私钥 = 登录成功。
其中有两个关键设计:
- 私钥从始至终都没有离开过本机,即使服务器遭到入侵,也只会泄露公钥。
- 签名内容绑定了会话 ID,每次连接的会话 ID 都不同,因此攻击者截获的签名无法用于对另一个连接发起重放攻击。
实际操作
生成密钥
OpenSSH 广泛应用于所有主流操作系统,并提供 ssh-keygen 及其他工具来生成和管理密钥对。
ssh-keygen -t ed25519 -C "公钥末尾的注释"虽然常见教程使用的是 -t rsa -b 4096,但现在建议优先使用 Ed25519:密钥更短、签名更快、安全强度相当于 RSA-3072 以上,而且不存在 RSA 参数容易配置错误的问题。
生成的两个文件:
~/.ssh/id_ed25519 # 私钥,绝不能泄露~/.ssh/id_ed25519.pub # 公钥,可以公开生成过程中会询问密码短语,强烈建议务必设置:设置后,私钥文件才会以加密形式存储,密码短语就是解密它的密码。万一笔记本电脑被盗或私钥文件泄露,攻击者在没有密码短语的情况下仍然无法使用它;如果直接按 Enter 留空,私钥将以明文形式存储,拿到文件就相当于拿到了账号。
使用 ssh-agent 避免每次输入密码短语
设置密码短语后,每次连接都要输入会很麻烦。ssh-agent 是一个常驻后台的程序,它会将解密后的私钥保存在内存中,并代为执行签名:
eval "$(ssh-agent -s)"ssh-add ~/.ssh/id_ed25519macOS 可以将密码短语存入 Keychain,并在 ~/.ssh/config 中设置首次使用密钥时自动将其加入 agent:
Host * IgnoreUnknown UseKeychain UseKeychain yes AddKeysToAgent yes IdentityFile ~/.ssh/id_ed25519将公钥放到远程服务器
authorized_keys 是一个纯文本文件,每行存放一把公钥。手动粘贴很容易出错(换行、权限),建议使用 ssh-copy-id:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@hostname它所做的事情,就是使用现有方式(通常是密码)登录一次,然后将公钥追加到远程服务器的 ~/.ssh/authorized_keys 中,并修正文件权限。
连接
ssh -i ~/.ssh/id_ed25519 user@hostname与其每次都输入一长串命令,更实用的方式是将配置写入 ~/.ssh/config:
Host myserver HostName 93.184.216.34 User ubuntu Port 22 IdentityFile ~/.ssh/id_ed25519之后只需执行 ssh myserver 即可,scp、rsync、git 也都会读取这份配置。
文件权限
SSH 对权限的要求非常严格。如果权限过于宽松,它会直接拒绝使用该密钥(因为同一台机器上的其他用户可能也能读取):
chmod 700 ~/.sshchmod 600 ~/.ssh/id_ed25519chmod 644 ~/.ssh/id_ed25519.pubchmod 600 ~/.ssh/authorized_keys典型的错误信息:
'~/.ssh/id_ed25519' 的权限 0644 过于宽松。私钥文件不得被其他用户访问。搭配 GitHub 使用
GitHub 的 SSH 认证使用的是同一套机制,区别仅在于 GitHub 不会向你提供 Shell,而是通过公钥判断「这次 push 是由哪个账号发起的」:
- 将
~/.ssh/id_ed25519.pub的内容粘贴到 GitHub 的 Settings → SSH and GPG keys。 - 测试连接:
ssh -T git@github.com# 你好,username!你已成功通过身份认证,但 GitHub 不提供 Shell 访问权限。- 之后 clone 时使用 SSH URL(
git@github.com:user/repo.git),而不是 HTTPS URL。
调试时,-v(可以叠加到 -vvv)会输出完整的协商与认证过程,从中可以清楚看到它尝试了哪些密钥,以及为什么被拒绝:
ssh -vT git@github.com总结
- SSH 连接分为两个阶段:先通过密钥交换建立对称加密通道,再进行身份认证。
- 主机密钥验证的是「服务器是不是它本人」,公钥认证验证的是「你是不是你本人」。
- 签名绑定了会话 ID,因此无法被重放。
- 新密钥应优先选择 Ed25519,务必设置密码短语,并搭配
ssh-agent使用。 - 私钥泄露等同于账号泄露。一旦发生,应立即从所有
authorized_keys和 GitHub 中移除对应的公钥,并重新生成密钥对。