今天回来,要帮同学远程运维一下之前的服务器(他忘记续费了),忽然发现运维脚本执行后没反应(往常1分钟一台跑完的).
问题现象
在 macOS 上 SSH 登录一台 Ubuntu 服务器,连接永久卡死。ssh -v 的调试输出停在:
1 | debug1: Authentications that can continue: publickey |
之后再无任何输出,进程既不报错也不超时,就这样一直挂着。
更诡异的是:
ssh-add -l同样卡死- 重启电脑后问题依旧
- 换用系统自带的
/usr/bin/ssh(9.x)和 Homebrew 的 OpenSSH 10.2 都卡
排查思路:逐层缩小范围
第一层:定位卡死的环节
-v 日志停在 Next authentication method: publickey,下一步本应是 Offering public key ...。在这两步之间,ssh 客户端会做一件事:向 ssh-agent 查询可用密钥。agent 不响应,客户端就无限等待。
初步怀疑 agent。检查环境变量:
1 | $ echo $SSH_AUTH_SOCK |
这是 macOS 系统 launchd 管理的标准 agent socket,路径本身正常。
第二层:绕过 agent 做对照实验
关键的分流测试——让 ssh 完全不碰 agent:
1 | ssh -o IdentityAgent=none -o IdentitiesOnly=yes -i ~/.ssh/id_rsa root@<server> |
立即成功登录。 这一步把问题 100% 锁定在 ssh-agent 环节:服务器没问题、网络没问题、密钥没问题,就是 agent 死了。
小插曲:先用
id_ed25519测试时返回Permission denied (publickey)——虽然被拒,但”立即返回”本身就是有效信息(说明绕过 agent 后流程通畅),换成服务器上实际授权的id_rsa即成功。
第三层:审问 launchd
macOS 上系统 ssh-agent 是 socket 激活式的:launchd 监听 socket,有连接进来才拉起 /usr/bin/ssh-agent -l。查看服务状态:
1 | $ launchctl print gui/$(id -u)/com.openssh.ssh-agent |
铁证:runs = 61,last exit code = 255。 launchd 一直在拉起 agent,但它每次启动后立即以 255 退出,已经反复崩了 61 次。客户端卡死的机制也清晰了:
ssh/ssh-add连接 socket- launchd 收下连接,拉起 agent
- agent 秒退,连接悬空
- 客户端永远等不到响应
- launchd 有 10 秒的重启节流,于是日志里每 10 秒一次”Successfully spawned → service inactive”的循环
用统一日志确认了这个循环:
1 | log show --last 2m --predicate 'process == "ssh-agent" OR eventMessage CONTAINS "ssh-agent"' --info --debug |
每次生命周期只有 5 毫秒:spawn → 查询用户信息(getpwuid)→ 退出。没有留下任何错误日志——launchd 模式下 stderr 被丢弃了。
第四层:让它当面崩给我看
既然 launchd 模式下看不到报错,那就手动跑,把 stderr 抓出来:
1 | $ /usr/bin/ssh-agent; echo "exit: $?" |
真相大白。 新版 ssh-agent 会在 ~/.ssh/agent/ 目录下创建 socket,而这个目录的权限坏了,导致 bind 失败,fatal() 退出 255。
根因:一个缺失的 x 位
看权限:
1 | $ ls -ld ~/.ssh/agent |
属主没问题,坏在权限位:drw-------——目录缺了 execute(x)位。
对目录而言,x 是”进入/穿越”权限。没有它:
- 可以
ls列出文件名(靠r位) - 但无法
stat目录内任何文件 - 无法在目录内创建新文件(socket 的 bind 也就必然失败)
这种坏法的典型成因:某次对 ~/.ssh 递归执行了 chmod 600。600 对私钥文件是正确的,但对目录是致命的——目录必须有 x 才能正常使用。
这也完美解释了所有诡异现象:
| 现象 | 解释 |
|---|---|
| 重启无效 | 权限持久化在磁盘上,与进程/会话无关 |
| 两个版本的 ssh 客户端都卡 | 问题在 agent 端,客户端无辜 |
| agent 静默死亡 | launchd 丢弃了 stderr,错误信息无处可看 |
| socket 存在且可连接 | socket 由 launchd 持有,与 agent 是否活着无关 |
修复:一条命令
1 | chmod 700 ~/.ssh/agent |
验证:
1 | $ ssh-add -l # 立即返回,agent 复活 |
经验总结
“卡死”和”报错”是两种线索。 卡死意味着某一方在无限等待,顺着协议流程找”下一步本应发生什么”,就能定位是在等谁。
对照实验是最快的手术刀。
-o IdentityAgent=none一条命令就把嫌疑范围从”客户端/网络/服务器/agent”缩到只剩 agent。socket 激活服务的报错会被吞掉。 launchd 拉起的进程 stderr 默认丢弃,
launchctl print里的runs和last exit code是最重要的健康指标;拿不到日志时,手动在终端复现启动是终极手段。chmod 600不要递归用在目录上。~/.ssh的标准权限:目录700,私钥600,公钥/config/known_hosts644或600。修权限时用chmod 700 目录+chmod 600 私钥分开处理,或者:1
2find ~/.ssh -type d -exec chmod 700 {} \;
find ~/.ssh -type f -name 'id_*' ! -name '*.pub' -exec chmod 600 {} \;exit code 255 在 OpenSSH 语境里就是
fatal()。 看到它,去找初始化阶段的失败点(socket、权限、配置解析),而不是运行时逻辑。
一个丢失的 x 位,让 ssh-agent 无声地崩溃了 61 次,拖着两个版本的 ssh 客户端一起陪葬,还扛过了一次重启。定位它花了四层排查,修复它只要九个字符。

