《协议是怎样炼成的》· SSH(下篇) · 避坑:报错会骗你,握手不会
上篇把握手拆开给你看,中篇教你把它用好。这一篇讲的是:当它出问题时,怎么修。
而 SSH 的报错,是出了名的”嘴硬”。你输错了、权限不对、密钥没被接受,它统统只回你一句冷冰冰的 Permission denied (publickey)——到底差在哪,一个字都不肯多说。这不是它做得烂,而是故意的:把失败的真正原因告诉你,等于也告诉了正在试探的攻击者”再换个姿势还有戏”。所以对外,它永远只给一句最笼统的话。
这就决定了:排查 SSH,靠”背报错对照表”是走不远的——同一句 Permission denied,背后可能是十种毛病。真正管用的办法,是让握手过程自己开口。
你在上篇见过那条握手链:连上、协商算法、换密钥、主机认证、切换加密、用户认证、开会话。

几乎每一个故障,都是这条链上某一环断了。只要让它把每一步都念出来,看它断在第几步,毛病就现形了。
让它开口的工具,就一个:-v。

一、先学会用 -v:把握手读成一条进度条
给 ssh 加上 -v,它就会把握手的每一步打印出来(-vv、-vvv 更详细,一般 -v 就够定位):
1 | ssh -v user@host |
你不用读懂每一行,只要认得几个路标——它们正好对应上篇那条握手链:
debug1: Connecting to host [IP] port 22—— 正在建 TCP 连接(传输层之前)。debug1: Remote protocol version ...—— 版本握上了。debug1: kex: algorithm: curve25519-sha256 ...—— 算法协商成功(上篇第一关)。debug1: Server host key: ...与debug1: Host 'host' is known ...—— 主机认证那一环(上篇第三关)。debug1: Authentications that can continue: publickey,password—— 服务器告诉你它接受哪些认证方式。debug1: Offering public key: .../debug1: Server accepts key: ...—— 用户认证(上篇第四关):你在递钥匙、服务器认不认。debug1: Authenticated to host ...—— 认证通过,开始会话。
-v 在哪一行停下、或在哪一行开始报错,就锁定了毛病在握手的哪一环。 这是下面所有排查的总纲:先看它走到哪儿断了,再去查那一环。
服务器那头也能开口。如果你有服务器权限,最直接的是临时用调试模式跑一个 sshd、只接一个连接看个究竟:
1 | /usr/sbin/sshd -d -p 2222 # 前台、调试模式,监听在 2222,不影响正在跑的 sshd |
或者直接翻认证日志——它会写明拒绝的真实原因(客户端那句 Permission denied 不肯说的,日志里往往写得清清楚楚):
1 | journalctl -u sshd # systemd 系统 |
工具齐了,顺着握手链从头往下走。
二、连不上:握手还没真正开始
2.1 根本没握上:Connection refused / 连接超时
-v 卡在 Connecting to ... 之后就不动、或直接报错,说明 TCP 都没通——问题还在 SSH 之前。两种典型:
Connection refused:能到那台机器,但 22 端口没人应。多半是 sshd 没在跑,或端口/监听地址不对(比如 sshd 改了ListenAddress、或绑了别的端口)。上服务器systemctl status sshd、ss -tlnp | grep ssh看一眼。- 连接超时(卡很久才失败):包根本没到,或回不来。基本是网络/防火墙——本地防火墙、云上安全组、或中间网络没放行该端口。
判断口诀:**refused 是”敲了门没人开”,超时 是”门都没找到”。** 前者查服务端进程与端口,后者查网络与防火墙。
2.2 算法谈不拢:no matching host key type found / no matching key exchange method
1 | Unable to negotiate with X.X.X.X port 22: no matching host key type found. Their offer: ssh-rsa |
这是算法协商那一环(上篇第一关 KEXINIT)没谈拢:你的新客户端,和一台老服务器(老交换机、老存储、老打印机的管理口最常见),在”用哪套算法”上没有交集。最常见的导火索,正是中篇埋下的那个区分:
OpenSSH 8.8(2021)默认禁用了 ssh-rsa 这个用 SHA-1 的老签名算法。 这里要再强调一遍中篇那个关键区分——**ssh-rsa 指的是”用 SHA-1 的签名算法”,不是”RSA 密钥类型”。RSA 密钥本身完全没被淘汰**,被禁的只是那套老签名方式。新客户端碰上只会这套老签名的老设备,就谈不拢了。
应急(连单台老设备时,临时把老算法加回来):
1 | ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa user@host |
+ 是”在默认之上追加”,比直接写死整张算法表更稳妥。常连的话,把它写进 ~/.ssh/config 的对应 Host 段(只对那一台开口子,别全局放开——这毕竟是退回到较弱的算法)。no matching key exchange method、no matching cipher 是同一类病、同一个治法:缺哪类就用对应的 -oKexAlgorithms=+...、-oCiphers=+... 临时补上。
但请记住:这些都是让你今天能连上的应急绷带,根治永远是升级那台老设备。绷带打多了会忘,弱算法就一直留在那儿了。
2.3 host key 变了:REMOTE HOST IDENTIFICATION HAS CHANGED!
握手走到主机认证那一环,突然蹦出一大段惊悚的警告:
1 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
回想上篇:你的客户端在 known_hosts 里记着这台机器的主机公钥;这次连上来的主机公钥对不上了。SSH 不知道是”服务器自己换了 key”还是”有人在中间冒充”,于是按最坏情况拉响警报、直接拒连。这正是上篇讲的主机认证在起作用——它宁可错杀,也不让你稀里糊涂连上一台身份变了的机器。
绝大多数时候,原因是良性的:服务器重装了系统、或换了主机密钥。但你得先确认这一点(问运维、核对新指纹),别盲目清掉——万一真是中间人,这一步是你唯一的预警。确认无误后,删掉旧记录再重连即可:
1 | ssh-keygen -R host # 删掉 known_hosts 里这台主机的旧行 |
(如果整个机房刚集体重装,用证书的主机认证就能免掉这种满地清 known_hosts 的活儿——参见中篇的 SSH 证书。)
三、登不进:卡在认证那一环
握手已经走过主机认证、切到加密信道,却倒在用户认证上——-v 会显示 Authentications that can continue,然后你的钥匙被一一拒绝,最后是那句最不肯透底的:
1 | Permission denied (publickey). |
3.1 Permission denied (publickey):先查权限,再查别的
同一句报错背后一堆可能,按”最常被忽略、最坑”的顺序排查:
文件权限太松(头号隐形坑)。 sshd 默认开着
StrictModes:登录前它会检查你家目录和~/.ssh的属主与权限,只要太开放,它就当作不安全、直接拒绝——而且对客户端只回一句Permission denied,真正原因(Authentication refused: bad ownership or modes for ...)只写在服务器日志里。所以这类坑,光看客户端永远猜不到,必须翻服务端日志。正确权限:1
2
3chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_ed25519 # 私钥还有个容易漏的:家目录本身不能被同组或其他人写(
chmod g-w,o-w ~),否则同样被StrictModes否掉。**公钥没真正进到服务器的
authorized_keys**,或路径不对(自定义了AuthorizedKeysFile)。私钥没加载:如果用了 agent,确认钥匙在里头(
ssh-add -l,回想中篇);或干脆-i显式指定私钥。服务端没开公钥认证(
PubkeyAuthentication no),或你这个账号被AllowUsers/AllowGroups挡在外面(回想中篇加固那节——你自己设的白名单别把自己漏了)。
排查时,客户端 -v 看”它到底递没递这把钥匙、服务器接没接”,服务端日志看”它为什么拒”。两头一对,真相立现。
3.2 明明配了密钥,却还在反复问密码
这其实是上一条的”温柔版”:你的公钥认证悄悄失败了,sshd 按 Authentications that can continue 里的下一个方式回退到了密码。所以现象是”问密码”,病根还是”公钥没被接受”——十有八九又是上面那个权限问题,或公钥没进对地方。
别急着输密码,先加 -v 看公钥那一步发生了什么:看到 Offering public key 之后服务器没 accept、甚至出现 send_pubkey_test: no mutual signature algorithm,那又是 ssh-rsa 签名被禁那一类(见上一节)。把”为什么回退到密码”查清楚,比输那个密码重要得多。
3.3 Too many authentication failures
1 | Received disconnect ... Too many authentication failures |
这条常让人莫名其妙——明明有对的钥匙,怎么还说我试太多次?根因往往是:你的 agent 里装了一大把钥匙,ssh 会挨个拿去试,还没轮到那把对的,就先撞满了服务器的 MaxAuthTries(默认 6),被踹下线。
治法是让它别乱试,只用你指定的那把:
1 | ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 user@host |
IdentitiesOnly=yes 的意思是”只用我 -i 指定的钥匙,别把 agent 里那一堆都拿来挨个试”。常连的主机,把这条连同 IdentityFile 写进 ~/.ssh/config 一劳永逸(这也是中篇 agent 那节提过的搭配)。
3.4 登录卡半天才给提示符:慢登录
慢登录最折磨人的地方在于——**它不是”登不进”,是”登得进、但要等”**。认证最终会成功,提示符也会出来,只是中间莫名卡十几秒到半分钟。正因为最后成功了,很多人就忍了,其实它几乎总是两个固定的”超时黑洞”之一,而且 ssh -v 能精确告诉你卡在哪。
判断方法:连接时加 -v,盯住哪两行日志之间出现长时间停顿。 卡顿就发生在停顿之前那一步——日志会一直刷,刷到某一行突然不动了,等十几秒才继续,那一行就是元凶。
黑洞一:反向 DNS 查询(最常见)。 10 到 30 秒的登录延迟,几乎都是反向 DNS 查询造成的。服务端收到你的连接后,默认会拿你的客户端 IP 反查主机名(PTR 记录),再用这个主机名正向查一次、验证能不能解析回同一个 IP。一旦 DNS 服务器慢、不可达,或者根本没有 PTR 记录,连接就会一直挂着,直到查询超时才继续。控制这个行为的开关是 sshd_config 里的 **UseDNS**(默认关闭)。
1 | UseDNS no |
对于不依赖反向 DNS 验证的环境,关掉它不会降低安全性——这正是中篇那条暗线的又一次印证:这个”安全检查”在你的场景里既挡不住真攻击者(IP 可伪造),又天天拖慢你,关掉它是安全和顺手双赢。内核邮件列表里甚至抓到过实锤包:服务端发了两次反向 DNS 请求、中间隔 5 秒——那 10 秒就这么白等掉的。
黑洞二:GSSAPI 认证协商。 如果关了 UseDNS 延迟还在,GSSAPI 认证就是下一个嫌疑犯——它(为 Kerberos 服务)在认证握手时同样会触发 DNS 查询。-v 下它的特征非常好认,你会看到 ssh 在正式问你密码/公钥之前,先去尝试 gssapi 方法、然后失败。
1 | debug1: Next authentication method: gssapi-with-mic |
这两行就是 GSSAPI 在空转——它先试 gssapi 方法,失败了,才轮到 publickey/password。客户端可临时 ssh -o GSSAPIAuthentication=no,服务端永久关闭则在 sshd_config。
1 | GSSAPIAuthentication no |
总结: 把 UseDNS 设为 no、再关掉 GSSAPI 认证,能解决绝大多数 SSH 慢登录;对不依赖反向 DNS 或 Kerberos 的环境,这两项改动都不影响安全。改完照例 reload/restart sshd。
补充:GSSAPI(Kerberos)协商到底是什么?
因为它在 -v 里很显眼、却被绝大多数人无视。
GSSAPI 是 Generic Security Services API 的缩写,是一套通用的安全认证接口标准;Kerberos 是它背后最主流的那套认证体系——就是 Windows 活动目录(AD)域里”一次登录、全域通行”的那套票据机制(单点登录 SSO)。
SSH 支持一种认证方式叫 gssapi-with-mic,意思是:如果你所在的环境有 Kerberos/AD 域,你登 SSH 时可以不输密码、也不用公钥,直接拿你域登录时领到的”票据”(ticket)来证明身份。 这在大型企业域环境里很方便,是真正的杀手锏功能。
但关键在于:绝大多数信创/Linux 服务器并不在 Kerberos 域里。 而 OpenSSH 默认又常把 GSSAPI 认证摆在认证方法列表的前面。于是每次登录,ssh 都会先一本正经地去尝试 Kerberos——找票据、(有时还伴随 DNS 查询找 KDC),折腾一圈发现根本没有域环境,报一句 Unspecified GSS failure,才退回去走你真正在用的公钥或密码。你等的那一会儿,就是它在为一个你压根没用的功能空转。
所以对不在域里的机器,GSSAPIAuthentication no 是无脑该关的——它关掉的不是你在用的能力,而是一个你用不上、却每次都要试一遍的累赘。这和加固那一节”只关自己用不上的功能”是同一个道理:攻击面和等待时间一起少了,日常却纹丝不动。
一句话记住 GSSAPI: 它是给”域环境单点登录”准备的高级通道;你不在域里,它就只是一段每次登录都要空跑、最后必然失败的前奏——关掉它,等于把这段前奏直接跳过。
四、登进去了,但行为不对:连接层的坑
人进来了,握手和认证都没问题,毛病出在上篇第四关之后的”会话/通道”层。
4.1 隧道建好了吗?它在哪、怎么拆

先立一个心智模型,这一节后面所有的坑都站在它上面:隧道不是系统里的持久对象,它就是那个 ssh 进程本身——进程活,隧道活;进程死,隧道亡。系统里没有一条”删除隧道”的命令,因为根本没有独立于进程的”隧道”可删。
由此推出三件日常事:
一、建立。 ssh -L 13306:10.0.0.8:3306 ops@bastion 敲下去,你得到的不是”无返回”,而是一个正常的 bastion 登录 shell——提示符照样出现,隧道只是这次登录”顺带”建起来的。如果你只要隧道、不要 shell,加 -N(不执行远程命令):
1 | ssh -N -L 13306:10.0.0.8:3306 ops@bastion |
这时终端才是真的”挂着不动”——光标停那儿不是卡了,是隧道正在干活。再要丢到后台,加 -f。
二、验证。 隧道建没建成,看入口的监听口在不在——而入口开在哪头,-L/-R 是镜像的:
1 | # -L:入口在我本机,本机查 |
初学者最常见的误判就在 -R 上:本机一查没有 18080,以为隧道没建起来——口子本来就不在你这台机器上。
-R 还有个更阴的坑:它会静默失败。对端那个口已被占用时,你的登录照样成功,只在登录输出里夹一行:
1 | Warning: remote port forwarding failed for listen port 18080 |
刷屏一快根本看不见,人还以为隧道好了。这又是那句话——登录成功会骗你,那行 Warning 不会。-L 反而老实:本地端口被占,当场报错拒绝启动。
三、拆除。 既然隧道=进程,拆隧道=结束进程,对号入座:
- 带 shell 的:在那个会话里
exit(或 Ctrl-D);会话卡死退不出,新起一行敲~.(SSH 逃逸序列,客户端立即断开)。 -N前台挂着的:在那个终端 Ctrl-C。-f -N后台的:pgrep -af 'ssh.*13306'找到 PID,kill之。
拆完同样用上面的 ss 验证口子已释放。一个小现象别慌:kill 之后立刻重建同名隧道,偶尔报端口被占——旧连接还没完全断干净,等一两秒再建即可;这个现象在 -R 上更常见,因为对端口子要等那头的 sshd 察觉连接已断才释放。
(生产里要”永远在线”的隧道,靠的不是手工 ssh 而是 systemd 服务或 autossh 拉活——那是部署话题,超出本篇排障范围,按下不表。)
4.2 -R 远程转发,别人却连不上
这是中篇远程转发那节我留的坑:你用 -R 把一个端口挂到了远端,自己在远端本机能连,别人却连不上。因为 -R 挂出来的监听口,默认只绑在远端的 loopback(127.0.0.1),纯粹是出于安全——不让你随手就把一个端口暴露给整个网络。
参数恒为「入口 : 服务地址 : 服务端口」|L = 入口开在本地,R = 入口开在对端
要对外,得两边都松口:服务端 sshd_config 里 GatewayPorts yes(或 clientspecified),客户端把绑定地址写成 0.0.0.0:
1 | ssh -R 0.0.0.0:8080:localhost:80 user@gateway |
这是有意为之的安全默认,不是 bug。 真要对外暴露,想清楚那个口子谁能碰到。
4.3 还是没想通 -R?口子凭什么开在对面
如果你对 -R 始终隔着一层——“明明是我发起的连接,凭什么口子开在对面?GatewayPorts 又是谁家的参数?”——这一小节把这层窗户纸捅破。
用一个具体场景走一遍。你家里一台电脑跑着网页服务(8080 端口),没有公网 IP;你在云上有台 VPS。想让外面的人经 VPS 访问到家里的 8080,你在家里电脑上敲:
1 | ssh -R 18080:localhost:8080 ops@vps |
注意,这首先就是一次普普通通的 SSH 登录——从家里连到 VPS,方向和你平时登服务器一模一样。-R 只是让你在登录时顺嘴拜托了一件事,等于对 VPS 上的 sshd 说:
“兄弟,我登上来了。麻烦你在你那边开一个 18080 替我守着。以后谁连你的 18080,你就把流量顺着咱俩这条已建好的加密连接倒灌回我这儿,我转给本地的 8080。”
看清楚了:那个 18080 的口子,是 VPS 上的 sshd 应你的请求、开在它自己身上的。 不是你隔空在别人机器上凿了个洞——监听、收流量这些活,全是对端 sshd 在干;你这头的 ssh 进程只负责接住倒灌回来的流量、转给本地服务。这一下,前面两件”怪事”全通了:为什么你本机 ss 查不到 18080(口子根本不在你机器上);为什么验证隧道生死要去对端查(口子的”尸体”也在对端)。
再看 GatewayPorts 是谁家的参数——它是 VPS 上 sshd 的”家规”,住在 VPS 的 /etc/ssh/sshd_config 里,和你客户端的命令行是两个世界。它管的事一句话:“客人拜托我开的口子,我允许朝谁开?”
no(默认):”口子可以开,但只开在我的 127.0.0.1 上——只有登在我这台机器上的人能用,绝不朝公网开。”所以你在命令里写不写0.0.0.0,它都当没听见。这是 sshd 替机器兜底:别人一条命令就能在我身上开公网口子?不行,得我的管理员改配置点头。yes:”客人要开的口子,一律朝全网开。”(你写啥都一样。)clientspecified:”客人说朝哪开就朝哪开。”——只有这一档,你命令里的0.0.0.0:才真正说了算。
所以 -R 的全链条上站着两个门卫:你的命令行是申请(”帮我开个口,最好朝公网”),对端 sshd 的 GatewayPorts 是审批(”按我家规来”)。申请几乎不会失败,但审批可能降级——而且降级不报错,口子悄悄落回 loopback。这正是上一小节那个坑的全部来历,也又一次应了那句话:命令”成功”会骗你,去对端 ss 一眼监听地址,不会。
最后,两分钟实操,胜过读十遍(找两台能 ssh 互通的机器,A 当”家”,B 当”VPS”):
1 | # 在 A 上:随便起个服务,再挂 -R |
跑完这四步,-R 的方向感、口子在哪、GatewayPorts 管什么、隧道的生死,全部落地。
4.4 AuthenticationMethods 的逗号是”与”,不是”或”
想做多因子、要求”公钥 + 密码都过”时,最容易栽在这条的语义上:
1 | AuthenticationMethods publickey,password |
逗号分隔的,是”全都要完成”(与)——上面这行要求你先过公钥、再过密码,两个都成功才放行。很多人误以为是”二选一”,配下去发现明明公钥对了还登不进,就是把”与”看成了”或”。真要”或”(任一即可),得用空格分隔成多个列表:publickey password 才是”公钥或密码”。改这条之前,务必按下一节的铁律留好后路。
4.5 agent 转发:中篇欠下的那笔账
中篇说过,把 agent 转发到别的机器很危险,这里把账算清。ForwardAgent yes(或 ssh -A)会把你本机 agent 的那个 socket(SSH_AUTH_SOCK,记得它吗)暴露到你登上去的那台远端机器。好处是你能从远端再跳下一台、复用本地钥匙不必落地私钥;但代价是——只要那台远端机器被人控制(哪怕只是 root),他就能在你连着的这段时间里,借用你的 agent 替他签名,去登录任何信任你的机器。你的私钥虽然没泄露,但”用私钥签名”这个能力被人借走了,效果一样致命。
所以:agent 转发只对你完全信任的机器开,且开则用、用完即断;能不转发就不转发。要做多级跳转,优先用中篇提过的 ProxyJump(ssh -J 跳板 目标)——它在跳板上只做转发、根本不碰你的 agent,从机制上就没有这个风险。
五、最凶的坑:把自己锁在门外
前面所有坑,最坏是连不上、登不进,重试就好。只有这一类是灾难级的:你在远程改 sshd 配置、一 restart,新配置把你自己挡在了门外——PasswordAuthentication no 但公钥还没配好、AllowUsers 漏了自己、改了端口忘了放行防火墙……而你手上没有别的进机器的路。
记住三条铁律,这类事故几乎可以归零:
改完先验语法再生效。 sshd 有自带的配置体检:
1
sshd -t # 检查 sshd_config 语法,有错会指出行号;没输出就是通过
永远留一条活路。 改认证相关项时,保持当前这个会话不要关,另开一个新终端去登一次,确认能进,再回头关掉旧会话。无论
reload还是restart,真正会要你命的不是”它踢掉你当前的连接”(已建立的会话通常还活着),而是”新配置让你再也建不了新连接“——所以那个能成功的新会话,就是你的验证和保险。分清
reload与restart。systemctl reload sshd让 sshd 重读配置、对已有连接更温和;restart是整个守护进程重启。日常改配置优先reload。但无论哪个,第 2 条都不能省。
万一真锁死了,就只剩带外的路:物理终端、云厂商控制台的 VNC/串口、或带外管理卡(IPMI/BMC)。所以——别在唯一一条 SSH 通道上,赌一次没验证过的配置改动。
六、写在三部曲结尾
回头看这一篇做的全部事情,其实只有一件:拿着上篇那张握手时序图,对照 -v 的输出,看它停在第几步。
- 连不上 —— 握手还没真正开始(TCP、网络、host key、算法协商);
- 登不进 —— 倒在用户认证那一环(权限、公钥、回退、重试超限);
- 行为不对 —— 卡在认证之后的会话与转发层;
- 锁门外 —— 服务端配置把门焊死了。
每一个坑,都能在握手链上找到它对应的那一环。报错文本会骗你——那句 Permission denied 什么都不肯说;但握手不会骗你,-v 把每一步摊开,断点就是病灶。 这,就是这一篇真正想交给你的东西:不是一张报错对照表,而是一套”顺着握手链定位”的排障直觉。
三篇到此合龙。上篇问”它到底是什么”——答案是认证,一次握手、两个方向的相互验明正身;中篇问”怎么把它用好、用安全”——钥匙、agent、隧道、证书、加固;下篇问”它坏了怎么办”——让握手自己开口。这三个问题答完,SSH 就不再是一个你天天敲、却从没真正看清的命令,而是一个你能讲清、用稳、修好的协议。
《协议是怎样炼成的》——SSH 这一炉,到此出炉。