前言:在一次生产环境的 SSH 密钥轮换中,我遇到了一个极其令人困惑的问题:在 Jenkins 服务器上手动执行 ssh -i 命令测试新密钥到目标服务器,直接报 Permission denied;将同样的命令放入 Jenkins 任务脚本中,同样失败。 在排除了文件权限、换行符、known_hosts 等所有常见原因后,最终发现是 ssh-agent 缓存的老密钥在"作祟",而解决方法是在 ~/.ssh/config 中为堡垒机单独配置 IdentitiesOnly yes。
本文记录了完整的排查过程,希望能帮助遇到类似"幽灵"故障的运维人员快速定位问题。
一、现象:手动测试失败,Jenkins 任务也失败
在 Jenkins 服务器上执行以下命令手动测试新密钥:
bash
ssh -i ~/.ssh/id_ed25519_new -J user@bastion_ip:22222 target_ip "echo '连通'"
结果: 报 Permission denied。
再将同样的命令放入 Jenkins 任务脚本中:
bash
scp -i ~/.ssh/id_ed25519_new -o 'ProxyJump user@bastion_ip:22222' ...
结果: Jenkins 控制台同样输出 Permission denied。
手动与脚本表现一致------说明问题不在 Jenkins 任务执行环境,而在基础的 SSH 连接链路本身。
二、排查:绕过的弯路
- 检查服务器端
authorized_keys:确认新公钥已正确添加,权限为600,文件末尾有换行符。 - 重置
known_hosts:ssh-keygen -R bastion_ip,无效。 - 在脚本中增加参数 :
-o StrictHostKeyChecking=no,依然报Permission denied。 - 清空
ssh-agent缓存 :ssh-add -D,依然无效。
这些操作均无法解决问题,说明问题不在表面,而在 SSH 认证的深层机制。
三、真相:SSH 的密钥优先级顺序
经过反复验证,发现 SSH 在认证时的密钥优先级是:
- 最高优先级 :
ssh-agent中缓存的密钥。 - 中间优先级:命令行
-i参数指定的密钥。 - 最低优先级:
~/.ssh/id_rsa等默认密钥。
在 Jenkins 节点上,后台进程悄悄地启动了 ssh-agent,并将旧的 id_rsa 密钥加载了进去。
当我们在手动测试或 Jenkins 脚本中执行 ssh -i ~/.ssh/id_ed25519_new ... 时,SSH 客户端会优先问 ssh-agent:"你有能用的钥匙吗?"
ssh-agent 回答:"有,我这里有 id_rsa。"
于是 SSH 尝试用 旧密钥 id_rsa 去连接堡垒机。此时,堡垒机上的老公钥已被删除,认证失败,连接被服务端直接切断。切断后,SSH 根本没机会再去尝试你 -i 指定的新密钥。
这就是为什么手动测试和 Jenkins 任务都失败的根本原因。
四、终极解决方案:配置 ~/.ssh/config
在 Jenkins 节点上,修改 ~/.ssh/config 文件,为堡垒机添加强制隔离配置:
bash
Host bastion_ip
IdentitiesOnly yes
IdentityFile ~/.ssh/id_ed25519_new
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
核心参数解析:
IdentitiesOnly yes:强制 SSH 客户端只使用IdentityFile指定的密钥 ,完全忽略ssh-agent中缓存的所有密钥。这是解决"-i参数失效"的关键。IdentityFile:指定要使用的新密钥文件。StrictHostKeyChecking no&UserKnownHostsFile /dev/null:跳过主机指纹校验,避免 SSH 在自动化环境中卡住。
五、验证与成果
添加配置后,再次执行手动测试命令(不再需要 -i 参数):
bash
ssh -J user@bastion_ip:22222 target_ip "echo '连通'"
结果: 成功输出 连通。
直接重跑 Jenkins 任务------构建成功,密钥轮换完成。
六、经验总结:下次轮换密钥该怎么做?
- 不要依赖脚本里的
-i参数 :在 Jenkins 这种有ssh-agent的环境里,单独指定-i往往不可靠,因为ssh-agent的优先级更高。 - 善用
~/.ssh/config:为跳板机或目标服务器单独配置IdentitiesOnly yes和IdentityFile,可以做到"一劳永逸"。 - 清理服务器端的旧公钥:确保新公钥是唯一可用的。
- 后续轮换 :下次更新密钥时,你只需要修改
~/.ssh/config里的IdentityFile路径,所有 Jenkins 任务会自动切换到新密钥,无需修改任何 Jenkins 脚本。
希望这篇博客能帮你快速定位这类 Jenkins 密钥更新后的连接失败问题。如果你也遇到过类似的"幽灵"报错,不妨试试在 ~/.ssh/config 里加上 IdentitiesOnly yes,或许能节省数小时的不必要排查。