我刚带新人时,几乎每个人都会问同一个问题:为什么SSH登录服务器要搞公钥认证?直接输密码不是挺方便吗?这个问题看着简单,背后却是整套SSH认证登录机制的设计逻辑。今天我不打算把RFC文档翻译一遍,而是按照一次真实登录的轨迹,把SSH认证的过程与原理拆开讲清楚——从建立加密通道,到确认服务器身份,再到验证"你是你"。无论是第一次接触SSH的新人,还是配了很多次密钥但总遇到Permission denied的开发者,这篇都能给你一份相对完整的认知地图。
1. 从"输密码"说起:SSH认证到底在防什么
1.1 登录这个动作,藏着三个风险
在SSH出现之前,远程登录主要靠明文协议。那会儿的场景是:你在终端里敲一条命令,这条命令连同密码一起原样在网络上传输,任何一个能抓包的人都能看到你敲了什么、密码是什么。所以SSH要解决的第一件事,是让通讯内容变得不可读。
但"加密"只是第一步。一次完整的远程登录,其实包含三个相互独立的问题:
- 保密性:通讯内容不能被第三方截获后看懂;
- 身份认证:怎么确认对面那台服务器确实是我要连的那台,又怎么让服务器确认登录的人确实是我;
- 完整性:传输过程中的数据有没有被篡改。
很多人以为SSH认证就是"输入用户名密码",其实密码只是用户认证环节里的一种方式,而且它只解决"我是我"这一个问题。服务器本身是不是靠谱、数据有没有被人动手脚,是另外两套机制在负责。这三件事在协议里被分开处理,这也解释了为什么SSH登录有时会出现"加密已经建立但认证却失败"这种让人迷惑的状态。
1.2 SSH的认证不是一步,而是分层推进
SSH协议(这里说的是当今主流的SSH-2)在设计上把整个登录过程拆成了几个层次,每个层次只解决自己那一部分问题:
- 传输层:负责建立加密通道,包括版本协商、算法协商、密钥交换,以及用对称加密保护后续所有数据。
- 用户认证层:客户端向服务端证明自己拥有某个账号的合法身份,方式可以是密码、公钥、键盘交互等。
- 连接层:认证通过后,在这个加密通道上开出一个会话通道,承载shell、端口转发、文件传输等具体功能。
理解这个分层非常重要。很多人排错时一看到"认证失败"就只盯着密码和密钥,却忽略了传输层是否真的建立了、服务器身份校验是否通过。实际上,很多看似是"认证"的问题,根源在更早的层次。
2. 一次SSH登录的完整旅程:从TCP到会话通道
2.1 第一程:TCP连接与版本协商
一次SSH登录从TCP握手开始。客户端默认连到服务器的22端口,TCP三次握手完成之后,双方立刻开始"自我介绍"。这一步叫版本协商:客户端发送一行以SSH-2.0-开头的标识字符串,服务端也回一行类似的字符串。
SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6这一行看起来平平无奇,但它的作用很大:双方通过它知道对方的协议版本。如果任一方只支持老旧的SSH-1,双方就按较低的兼容档次走(当然现在主流实现默认禁用了SSH-1)。版本协商完成后,所有后续消息不再是裸文本,而是按照二进制包格式封装。从这一刻起,双方进入"还没加密但已经定了规矩"的状态。
2.2 第二程:算法协商与密钥交换
版本协商之后,客户端和服务端各自发送一份算法偏好列表,内容包括:密钥交换算法、主机密钥算法、对称加密算法、消息认证码算法、压缩算法。两端从各自的列表里找出"双方都支持"的交集,选出一个组合。
这里我多说一句为什么算法协商如此重要。SSH要兼容不同年代、不同安全策略的机器,所以协议允许双方"点菜"。但菜也不是随便点的,如果你用主流的SSH客户端连一台老服务器,双方会退而求其次选择都认得的最强算法。整个协商过程遵循"取双方支持列表的交集,按服务端偏好排序",所以不存在客户端强势指定一说。
算法谈妥之后,进入密钥交换环节。目前最常见的做法是基于椭圆曲线的密钥交换:双方各自生成临时密钥对,交换公钥部分,计算出一个只有双方能算出的共享密钥。这个共享密钥不会被直接当作加密密钥,而是作为"种子",配合前面的交换信息再派生出一组真正的会话密钥,分别用于客户端到服务端、服务端到客户端两个方向的加密和完整性校验。
2.3 第三程:服务器身份核验(Host Key)
很多人忽略这一步,但它恰恰是SSH认证体系里最容易踩坑、也最容易被误解的部分。在密钥交换过程中,服务端不只是给出算法参数,还会出示自己的主机公钥,并且用对应的主机私钥对"交换摘要"做一次签名。客户端拿到这份签名后,会做两件事:
- 检查这个主机公钥是否已经在自己的
known_hosts文件里; - 如果确实见过,就用里面的公钥验证服务端这次签名。
如果验证通过,客户端才能确认:我连接的这个服务端,确实持有那台主机私钥,不是中间人冒充的。如果是第一次连接,客户端会看到一个指纹提示,问你"真的要继续连接吗"。这个被称为"首次信任"的机制,逻辑上是有缝隙的,但配合指纹的二次核对,它在实际运维中已经足够可靠。
主机密钥和用户密钥是两个完全不同的东西。主机密钥是服务器用来证明"我是这台服务器"的,管理员通常放在/etc/ssh/ssh_host_*;用户密钥是登录用户用来证明"我是这个用户"的,放在用户自己的~/.ssh/下。排错时把这两者混为一谈,是最常见的初级错误。
2.4 第四程:用户认证与通道建立
传输层加密建立、服务器身份也确认了,这时候才轮到真正的"用户认证"。服务端会告诉客户端当前账号下允许哪些认证方法,客户端从中挑一种来尝试。常见的组合是publickey、password,有的环境还有keyboard-interactive。
认证一旦成功,客户端马上发送打开会话通道的请求,双方在这个加密通道里建出一个双向会话。之后你敲的每个命令、看到的每行输出,都在这条通道里流动。通道层面还支持端口转发、SCP/SFTP文件传输等不同用途。理解"认证成功后才开始干活"这个顺序,对后面的排错特别有用:如果通道建立失败,多半不是认证问题,而是连接层权限配置问题。
3. 密码认证与公钥认证:一条命令背后的两套机制
3.1 密码认证:流程简单,但问题出在"人"
密码认证在SSH里的流程是:客户端输密码,服务端拿它和本地存储的密码散列比对,一致就允许登录。这里有个很容易被误解的点——密码并不是明文在网络上传输的,它发生在加密通道建立之后,所以抓包抓不到密码本身。
那为什么运维老手都不推荐密码认证?问题不在技术,而在人。密码可以被猜测、被复用、被钓鱼,会因为用户偷懒而变弱。服务器只要开放22端口,就会不断有人尝试用字典去撞弱密码。即使有fail2ban这类工具兜底,也只是事后防御。相比之下,公钥认证的私钥不离开客户端,不存在"被人猜中"的问题,安全边界清楚得多。
3.2 公钥认证:核心是签名而不是"钥匙匹配"
公钥认证的流程,很多人理解成"客户端出示公钥,服务器比对一下就行"。这是错的。真正的流程分两步走:
第一步,客户端先试探:我这里有这么一把公钥(给指纹),请问服务器你认识吗?服务器去查看authorized_keys文件,如果找到对应公钥,就回复"认识"。
第二步,客户端才真正发起认证:用这把公钥对应的私钥,对一段包含会话标识和认证请求的数据做签名,然后连同公钥一起发过去。服务器用刚才验证过的那把公钥验证签名是否正确。
为什么要设计成"先试探、再签名"?因为这样可以提前过滤掉无效密钥,避免客户端在密钥根本不被认可时,还白白让用户输入私钥口令。更重要的是,签名这个动作本身就证明了一件事:持有私钥的人就在现场。私钥没有离开客户端,服务器看到的只有签名结果。这套逻辑和现实中的"刷身份证"不一样,它更像"盖章"——章在我手里,我盖出来的印就是证据。
3.3 三种主流密钥算法的取舍
现在生成SSH密钥对时,一般有几种算法可选,我按推荐程度给你排个序:
| 算法 | 密钥长度 | 特点 | 适用场景 |
|---|---|---|---|
| Ed25519 | 固定256位 | 签名短、速度快、安全性高,新一代服务器都兼容 | 日常首选 |
| RSA | 通常2048/4096位 | 兼容性最好,老设备基本都认 | 需要兼容极老系统时 |
| ECDSA | 256/384/521位 | 安全性不错,依赖特定曲线实现 | 特殊合规要求场景 |
有一点值得注意:算法和认证速度的关系没有想象中那么玄学。签名验证本身微秒级完成,真正影响登录体验的往往是网络往返次数和服务端的哈希计算。选Ed25519不是因为它能让你登录快多少,而是因为它在同等安全强度下密钥更短、更省资源,也少了很多"纠结到底选RSA 2048还是4096"的犹豫。
4. 免密登录从零落地:从生成密钥到authorized_keys
4.1 生成密钥对的正确姿势
我见过太多人直接敲ssh-keygen然后一路回车,生成出来一把没有注释的默认密钥,后期完全不知道哪把钥匙对应哪台机器。这里给你一套推荐参数:
ssh-keygen -t ed25519 -C "work-server-2024" -f ~/.ssh/work_ed25519-C用来写注释,建议写"用途+主机名"这种能一眼看懂的信息;-f用来指定密钥文件名。生成时如果提示输入口令,建议设置一个——这个口令保护的是私钥本身,即使私钥文件泄露,没有口令也没法直接用。
私钥生成后会产生两个文件:work_ed25519和work_ed25519.pub。.pub是公钥,可以随便分发;私钥则必须严密看管。权限上,私钥文件建议600,.ssh目录建议700。很多SSH登录失败就是私钥权限太宽松,服务端会出于安全考虑直接拒绝。
4.2 公钥分发与authorized_keys的管理
拿到公钥后,需要在服务器上把这个公钥追加到对应用户的~/.ssh/authorized_keys文件里。最省事的方式是用系统自带的复制命令:
ssh-copy-id -i ~/.ssh/work_ed25519.pub user@server它会自动帮你把公钥追加到服务器的authorized_keys里,还会顺手修正目录权限。如果没有这个命令,手动操作也完全可以:
cat ~/.ssh/work_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"需要注意,这一步执行的时候还是用的密码认证,所以前提是服务器暂时开着密码登录。这也是我反复强调"先确认密钥能登录,再关密码认证"的原因。
authorized_keys文件支持每行一个公钥,前面还可以附加选项,比如限制来源IP、限定只能执行某条命令、禁止端口转发:
from="192.168.1.0/24",command="/usr/bin/some-script",no-port-forwarding ssh-ed25519 AAAAC3...这种精细化管理在生产环境里很实用,尤其适合给自动化脚本准备的专用账号——把它的权限圈死,出了问题也不会波及其他业务。
4.3 多把钥匙并存时,服务器如何判断该验哪一把
很多人会往一台服务器上放好几把公钥,比如自己的笔记本一把、跳板机一把、自动化任务一把。服务器在收到公钥认证请求时,会一行一行扫描authorized_keys,发现有匹配的公钥后,就用这把公钥验证请求里的签名。匹配的依据是公钥内容本身,而不是文件名或注释,这也是为什么给不同机器生成不同密钥、并老老实实写注释,能让后期的维护省心不少。
客户端这边,如果~/.ssh/下有多个私钥,默认会逐个尝试。你可以用-i参数指定用哪把,也可以在~/.ssh/config里为主机配置对应的IdentityFile,这样连不同主机时系统会自动挑对应的钥匙,不用每次手动指定。
5. 认证失败排查:从日志到现场还原
5.1 Permission denied (publickey) 的完整排查链路
这是SSH认证里出现频率最高的报错。我通常按下面的链路一步步排查:
- 打开详细调试输出看看卡在哪一步:
ssh -vvv user@server观察输出里"Offering public key"出现几次、是否出现"Authentications that can continue"。
确认服务器端是否开了公钥认证。检查服务器配置里
PubkeyAuthentication yes这一项。核对客户端使用的公钥,是否真的追加到了服务器的
~/.ssh/authorized_keys里。这一步最常出错:有人把公钥贴到了自己账号下,结果用别的账号登录,自然失败。检查权限。服务器上用户主目录、
.ssh目录、authorized_keys文件的权限如果过于宽松,默认配置的SSH服务会直接忽略这个文件。最后看日志。Debian系机器在
/var/log/auth.log,RedHat系机器在/var/log/secure。日志里往往直接写着"Failed publickey for user",甚至给出原因。
整条链路里,第四步和第五步占了问题总量的八成以上。很多时候密钥本身没问题,纯粹是公钥放错了地方、或者权限不达标。
5.2 Host key verification failed 意味着什么
这个报错和用户认证没有直接关系,它是服务器身份校验环节抛出来的。出现它有三种可能:服务器重装过导致主机密钥变了;有人试图冒充服务器进行中间人攻击;你的known_hosts里记录了错误的指纹。
处理方法是谨慎的,不是直接删掉就完事:
ssh-keygen -R server.example.com删除之后重新连接,会再次看到首次连接提示。注意,如果这台服务器不是你自己能控制的重装,而是生产环境的关键机器,那么看到这个报错必须先找管理员确认,不能闷头删指纹。
5.3 多主机、多密钥场景下的认证顺序
当你管理几十台机器时,认证顺序的问题会冒出来。客户端会按~/.ssh/config里的主机匹配规则选择对应的IdentityFile。如果你的配置里写了一个通配的默认密钥,又希望某台特定机器用另一把钥匙,顺序就很重要——通常更具体的规则在前,通配规则在后。
Host *.example.com User deploy IdentityFile ~/.ssh/deploy_ed25519 Host jump-prod HostName 192.168.10.5 User admin IdentityFile ~/.ssh/admin_ed25519我曾经遇到过一整个项目组连不上某台测试机,最后发现是大家把服务器的/etc/ssh/sshd_config里某条配置改坏了,导致AuthorizedKeysFile指向了一个不存在的路径。这种情况日志里不会直接给出路径错误,只表现为"所有公钥都验证不过"。排查时如果发现权限、密钥都对,但就是失败,一定要回头看看服务端配置文件,确认AuthorizedKeysFile、PasswordAuthentication、PubkeyAuthentication这几项的实际状态。
有一点我觉得特别值得分享:不要一上来就禁用密码认证。稳妥的顺序是,先配好密钥并确认能登录,再写个测试命令确认一次非交互登录没问题,最后才在配置里关掉密码。我见过不少人在还没验证密钥可用时就关了密码登录,结果自己也被锁在外面,最后只能走带外管理通道改配置,折腾一晚上。SSH认证的原理看起来抽象,但只要顺着"建通道、验服务器、验用户"这条线走下来,大多数问题都能在十分钟内定位到具体环节。