ssh-add "Could not open a connection to your authentication agent":macOS 私钥口令 + ssh-agent/钥匙串配置手册
一次 SSH 密钥轮换(旧钥换新钥 + 加口令 + 全节点部署)踩坑后的沉淀,主机信息已隐去。
TLDR
ssh-add 报 “Could not open a connection to your authentication agent”,九成是 SSH_AUTH_SOCK 没指向 launchd agent 的 socket——agent 本身在跑,只是 shell 找不到它:
# 找到 launchd agent 的 socket(路径每次重启会变)
launchctl print gui/$(id -u) | grep SSH_AUTH_SOCK
# 带上 socket 加载钥匙并存入钥匙串
SSH_AUTH_SOCK=<上一步的路径> ssh-add --apple-use-keychain ~/.ssh/id_ed25519_current
配套三件事缺一不可:
- 私钥设口令:
ssh-keygen -p -f ~/.ssh/id_ed25519_current ~/.ssh/config加Host *段的AddKeysToAgent yes+UseKeychain yes~/.zshrc加 socket 自动恢复段(见下文第 4 节),否则新终端里 pdsh 等批量工具全部认证失败
0. 原则
- 每把私钥必须设口令(passphrase)。裸私钥一旦被任何进程/人读到,等于门锁被人拿走钥匙坯。
- 口令的记忆成本由 agent + 钥匙串 消化:交互时输一次,之后所有场景(终端、脚本、pdsh、cron)自动签名。
- 最小暴露:每台机器的
~/.ssh/config只显式指定当下该用的 IdentityFile。
1. 给私钥加/改口令
ssh-keygen -p -f ~/.ssh/id_ed25519_current
# 输入旧口令(无则直接回车) → 输两次新口令
验证口令已生效(能无口令派生公钥说明还是裸钥):
ssh-keygen -y -P "" -f ~/.ssh/id_ed25519_current && echo "仍无口令" || echo "已有口令"
2. ~/.ssh/config 模板
Host node-a
HostName 100.x.y.z
User someuser
IdentityFile ~/.ssh/id_ed25519_current
# 所有节点通用:自动入 agent + 从钥匙串取口令
Host *
AddKeysToAgent yes
UseKeychain yes
⚠️ 坑 1:IdentityFile 会"替换"而非"追加"默认钥匙列表。
一旦某 Host 配了 IdentityFile,ssh 只尝试列出的钥匙,默认的 ~/.ssh/id_ed25519 等不再尝试。
所以换钥期间若 config 只写了新钥,旧钥 fallback 不会发生——这是大量"明明有备用钥却 Permission denied"的根源。
⚠️ 坑 2:公钥 offer 不需要口令,签名才需要。
所以 BatchMode 下会看到 ssh 正常 offer 新钥,然后卡在签名上失败,报错却是笼统的 Permission denied。用 ssh -v 看 Server accepts key 之后有没有走下去。
3. 加载进 agent + 钥匙串
launchd 的 ssh-agent socket 路径每次重启会变,动态获取:
launchctl print gui/$(id -u) | grep SSH_AUTH_SOCK
# 形如 /private/tmp/com.apple.launchd.XXXXXXXX/Listeners
加载(把 socket 路径代入):
SSH_AUTH_SOCK=<上一步的路径> ssh-add --apple-use-keychain ~/.ssh/id_ed25519_current
验证:
SSH_AUTH_SOCK=<路径> ssh-add -l # 应列出钥匙指纹
⚠️ macOS 怪癖:Identity added 提示成功,但钥匙串条目可能并未真正写入。
验证钥匙串:
security find-generic-password -s "SSH:你的key注释"
查无此项时重启后需重跑一次 ssh-add(口令会提示一次)。UseKeychain yes 已就位,条目一旦落盘即全自动。
4. 让 shell 稳定看到 agent(~/.zshrc)
部分终端环境不继承 launchd 设的 SSH_AUTH_SOCK,导致 ssh-add 报
“Could not open a connection to your authentication agent”、pdsh 等批量工具全部认证失败。
在 ~/.zshrc 末尾加:
# 从 launchd 恢复 ssh-agent socket(仅在当前 socket 无效时)
if [[ ! -S "${SSH_AUTH_SOCK:-}" ]]; then
_ls="$(launchctl print gui/$(id -u) 2>/dev/null | awk -F'=> ' '/SSH_AUTH_SOCK/ {print $2; exit}')"
[[ -n "$_ls" ]] && export SSH_AUTH_SOCK="$_ls"
unset _ls
fi
5. 部署 / 清理 authorized_keys
部署公钥到目标节点:
ssh target-host 'echo "ssh-ed25519 AAAA...你的新钥...注释" >> ~/.ssh/authorized_keys'
清理旧钥(不要用 sed):
ssh target-host "grep -vF '旧钥b64串' ~/.ssh/authorized_keys > ~/.ssh/ak.tmp && cat ~/.ssh/ak.tmp > ~/.ssh/authorized_keys && rm -f ~/.ssh/ak.tmp"
⚠️ 坑 3:sed '#pattern#d' 是静默空操作。 sed 脚本以 # 开头会被当注释,退出码还是 0。
# 只能作 s 命令的分隔符(s#a#b#),不能用在裸地址上。用 grep -vF + cat tmp > 原位覆写(保留权限)。
6. 验证方法论(最容易自欺的一步)
⚠️ 坑 4:远程命令里的固定标签不能证明用的是哪把钥。
ssh -i 旧钥 host 'echo OLD_OK' 在 agent 可达时可能被新钥完成认证,输出的 “OLD_OK” 毫无意义。
正确做法是控制变量:
# 真·仅旧钥:切断 agent + 只允许显式钥匙
ssh -o BatchMode=yes -o IdentitiesOnly=yes -o IdentityAgent=none \
-o IdentityFile=~/.ssh/id_ed25519_old host 'echo ok'
# 期望: Permission denied(旧钥已从节点删除)
# 仅新钥(走 agent)
ssh -o BatchMode=yes -o IdentitiesOnly=yes -o IdentityAgent=none \
-o IdentityFile=~/.ssh/id_ed25519_current host 'echo ok'
# 期望: ok
轮换验收 = 两项都符合预期,缺一不可。
⚠️ 坑 5:IdentitiesOnly=yes 只挡 agent 里"多余"的钥匙,挡不住 config 里 IdentityFile 指定的钥匙借 agent 签名。
config 里写了新钥 IdentityFile 时,“旧钥测试"仍可能被新钥完成——必须配合 IdentityAgent=none。
7. 已知坑速查
| 症状 | 原因 | 处置 |
|---|---|---|
| 同一命令时好时坏 Permission denied | OpenSSH 9.x 服务端 PerSourcePenalties:反复失败认证后源 IP 被临时惩罚 | 等 1–2 分钟重试;排查为何大量失败认证 |
ssh-add 报 agent 连接失败 |
SSH_AUTH_SOCK 未设/指向死 socket |
见第 4 节 |
| 换钥后 pdsh/批量工具全挂 | config IdentityFile 替换了默认列表 + 新钥签不了名 | 见坑 1、坑 2;agent 里加载新钥 |
| 删行不生效但退出码 0 | sed '#…#d' 注释陷阱 |
见坑 3 |
| 验证"成功"但换台机器又失败 | 标签陷阱,实际是另一把钥认证的 | 见坑 4 |
8. 重启后流程
- launchd agent socket 路径已变 →
.zshrc自动恢复(第 4 节) - agent 空 → 第一次 ssh 时提示口令,输入一次即自动入 agent(
AddKeysToAgent yes) - 若钥匙串条目已在:连提示都不会有;若未落盘(第 3 节怪癖):重跑一次
ssh-add --apple-use-keychain