笔记 by 车东

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

配套三件事缺一不可:

  1. 私钥设口令:ssh-keygen -p -f ~/.ssh/id_ed25519_current
  2. ~/.ssh/config 加 Host * 段的 AddKeysToAgent yes + UseKeychain yes
  3. ~/.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. 重启后流程

  1. launchd agent socket 路径已变 → .zshrc 自动恢复(第 4 节)
  2. agent 空 → 第一次 ssh 时提示口令,输入一次即自动入 agent(AddKeysToAgent yes)
  3. 若钥匙串条目已在:连提示都不会有;若未落盘(第 3 节怪癖):重跑一次 ssh-add --apple-use-keychain