你在本机执行 ssh -A bastion,登上跳板机,再从那里连接内网服务器。两段登录都很顺,跳板机磁盘上也找不到你的私钥。这个做法看起来相当克制:敏感文件留在电脑里,中间机器只负责带路。
麻烦藏在“私钥没有复制”这句话后面。转发过去的虽然不是私钥文件,却是一条通往本机认证代理的通道。只要会话还在,远端进程就可能请你的代理完成签名。钥匙没有交出去,能让钥匙开门的窗口却开在了跳板机上。
OpenSSH 10.5 在 2026 年 8 月 11 日发布,其中一项安全修复正好碰到这条边界:锁定 agent 与用于识别转发 agent 的会话绑定扩展发生交互时,本应只允许本地使用的操作可能被远程执行。这次更新没有把所有 agent 转发都判成错误,它提醒我们重新回答一个更实际的问题:这台中间机器为什么必须借用我的身份能力,借到哪里才算够?
私钥没落地,身份能力还是被带过去了
SSH Agent 可以先理解成一个替你保管密钥并完成签名的本地进程。连接服务器时,SSH 客户端把认证请求交给它,agent 用已加载的密钥签名,私钥内容本身不需要离开本机。输入一次密钥口令后可以连续登录,这正是它好用的地方。
打开 ForwardAgent 或使用 ssh -A 后,远端会话里出现 SSH_AUTH_SOCK。它指向一个转发过来的套接字,可以把签名请求一路送回本机。OpenBSD 的 ssh_config 手册写得很直白:能绕过远端套接字文件权限的人拿不到密钥材料,但能够使用 agent 中的身份执行认证操作。
这个区别很容易被口语抹平。“私钥没上服务器”是事实,“跳板机不能使用我的身份”却不成立。若跳板机被入侵、同账号下有恶意进程,或者你在那台机器上运行了不可信脚本,暴露面都跟着转发会话一起存在。断开连接会关闭窗口,但在线的十分钟也足够完成一次不该发生的登录。
还要分清 agent 里“有几把钥匙”和当前任务“需要哪一把钥匙”。开发电脑常同时加载个人 Git、测试机和生产机身份。转发通道默认面对的是 agent 当前持有的身份集合,并不会根据你口头上的任务自动筛选。原计划只在跳板机上拉一次代码,实际暴露的可能还有能登录其他服务器的密钥,这就是盘点 ssh-add -l 的意义。
OpenSSH 10.5 修的是一条容易被忽略的边界
10.5 的发布说明提到,ssh-agent 的锁定状态与 session-bind@openssh.com 扩展之间存在交互问题。会话绑定用于识别转发链路;此前绑定请求在 agent 锁定时被拒绝,结果是一些原计划限制为本地使用的操作仍可能在远端完成,包括添加 PKCS#11 令牌,以及使用带目的地限制的密钥。
这里不适合自行扩大结论。官方说明没有说“任何使用 ssh -A 的人都会泄露私钥”,也没有把每一种客户端实现都放进同一个影响范围。能够确认的是:10.5 包含这项安全修复;仍在使用 OpenSSH agent 转发、agent 锁定或目的地约束的人,应先确认本机版本与发行版补丁状态,再重新测试自己的链路。
升级也不能替代配置收口。修复让既有保护按设计工作,ForwardAgent yes 带来的远端签名能力依然存在。把版本升级当作“以后可以放心全局转发”,反而会错过这次更新最有价值的提醒。
版本确认也有一个常见误区:ssh -V 显示的是你当前终端调用到的程序,图形客户端可能使用另一套 SSH 实现,Linux 发行版也可能在旧版本号上回补安全补丁。最可靠的判断来自客户端发布说明、软件包安全公告和实际链路测试。看见一个版本字符串就宣布所有入口都已修复,证据还不够。
先确认你到底有没有打开转发
不少人记得自己偶尔敲过 -A,却忘了 ~/.ssh/config、系统配置或通配 Host 段里可能已经长期启用。排查时别只看眼前那条命令,可以先让 SSH 输出目标主机最终生效的配置:
ssh -G target.example.com | grep -i '^forwardagent'
ssh-add -l
第一条回答“连接这个目标时是否会转发”,第二条回答“当前 agent 里装了哪些身份”。登录跳板机后再看 printf '%s\n' "$SSH_AUTH_SOCK",有路径通常说明当前会话拿到了 agent 通道。没有输出不代表整套 SSH 密钥管理已经安全,它只说明这条会话没有可见的转发套接字。
若结果和配置文件不一致,再检查命令行参数、Include 文件与更靠前的 Host 段。OpenSSH 对每个参数通常采用第一个获得的值,因此配置顺序会改变最终结果。调试连接时可以加 -v 观察客户端选择了哪份身份和跳转链,但日志可能包含主机名、用户名与内部地址,发给别人前要脱敏。
我会特别查一种配置:在 Host * 下写 ForwardAgent yes。它省掉了逐台设置,却把开发机、测试机、临时云主机和生产跳板机放进同一个信任等级。真正需要转发的目标通常很少,通配开启等于让例外变成默认。
只为穿过跳板机,通常不需要把 agent 交出去
很多 ssh-agent 转发 是从一个误判开始的:访问内网目标必须经过跳板机,于是觉得认证代理也必须先进入跳板机。若需求只是让网络连接穿过中间主机,ProxyJump 往往已经够用。SSH 在本机协调每一跳,跳板机负责转发字节,不需要给中间 shell 一个可用的 agent 套接字。
Host bastion
HostName bastion.example.com
User devops
ForwardAgent no
Host app-prod
HostName 10.20.0.15
User deploy
ProxyJump bastion
ForwardAgent no
图形化 SSH 客户端也能采用同样思路。以 Xterminal 为例,可以分别保存跳板机和目标连接,再在目标连接里配置跳板机链路;每台连接使用自己的认证信息。它减少了手写配置的负担,但不会替你决定凭据是否该复用。若团队必须经过带审计的堡垒机、交互认证或硬件令牌,仍要服从现有访问策略,不能为了少一次确认擅自改链路。
这里的收益不仅是少开一个 agent 通道。目标连接与跳板机连接分开后,失败也更容易定位:第一跳认证失败、内网目标认证失败和中间网络不通会留下不同信号。将两台机器共用同一把密钥虽然省配置,却又把身份边界合并了;有条件时应按环境或角色拆分凭据,让撤销某台机器权限时不影响整条连接清单。
真要在远端继续登录,就给钥匙加目的地
有些工作确实要求在跳板机上执行第二段 SSH,例如远端构建任务需要访问私有 Git 仓库,或旧环境只能从运维入口机发起管理连接。此时一刀切关闭转发会让流程停摆。可行的方向是缩短有效时间、按次确认,并为加载到 agent 的密钥设置目的地约束。
ssh-add 还提供确认使用和有效期选项。需要人工在场的临时任务,可以要求每次使用身份前确认;只用半小时的维护窗口,可以让密钥到期后自动从 agent 移除。这些措施各自解决一段问题:确认降低后台静默使用的机会,有效期缩短遗忘窗口,目的地约束限制能去的主机。它们组合后仍要保留最小权限,不能把多层保护当成扩大授权的理由。
OpenSSH 的 ssh-add -h 可以限制密钥从本机到某个目标,或允许它经指定源主机再到指定目标。例如约束形如 bastion.example.com>git.example.com,表示转发到 bastion 的 agent 只可继续认证 git 目标。手册也给了边界:各跳需要支持这套约束;主机依靠 known_hosts 中的主机密钥识别;拿到远端套接字的攻击者仍可能继续转发它,只是最终只能用于被允许的目的地。
所以目的地约束是减小爆炸半径,不是给不可信跳板机发健康证明。旧客户端、旧服务端、主机密钥缺失或别名解析不一致时,约束可能直接导致认证失败。部署前要在非生产链路验证,失败时宁可回到单独部署公钥、短期证书或硬件确认,也不要临时改回全局 ForwardAgent yes。
一次改配置的完整过程
假设现状是在 Host * 全局转发,而真实需求只有“经 bastion 访问 app-prod”和“偶尔在 bastion 拉取私有仓库”。我会先保存 ssh -G、ssh-add -l 与当前配置片段,确保改完能知道差异来自哪里。随后把全局值改为 no,给 app-prod 配置 ProxyJump bastion,直接测试从本机连接目标。
这一步通过后,再单独处理远端拉仓库。为它准备一把权限更窄的 SSH 密钥,只在需要的工作时段加载;支持目的地约束的环境里,把本机到 bastion、bastion 到 Git 目标的路径分别加入约束,并用真实主机密钥验证。连接跳板机后先运行 ssh-add -l 确认远端能看到预期身份,再访问允许的 Git 目标,最后尝试一个未授权目标,后者必须失败。
完成任务便从 agent 删除临时身份并断开会话。测试只验证“允许的能通”还不够,拒绝路径才证明限制真的在工作。若未授权目标也能登录,立刻停止使用这组配置,检查实际使用的 agent、已加载的其他密钥、Host 别名和目标服务器是否还有别的认证方式。
记录结果时不需要保存完整调试日志,一张简短表述就够:客户端版本来源、目标 Host 的 forwardagent 最终值、agent 指纹列表、允许目标成功、未允许目标失败。下一次升级客户端或更换跳板机后按同样顺序复测,才能知道安全边界有没有因环境变化被悄悄放宽。
哪些失败分支不能靠配置美化
第一类是版本不一致。你的本机显示 OpenSSH 10.5,并不能证明图形客户端内置的 SSH 实现、远端 ssh 客户端和服务器都支持相同扩展。产品没有明确说明支持 destination constraints 时,不要根据界面相似度自行推断。
第二类是业务把跳板机当成长期工作站。大量脚本、构建和发布都在上面运行,转发窗口会接触更多进程,信任面远大于只做网络中转。此时应考虑独立的部署身份、短期 SSH 证书或集中执行系统,让每个任务拿到恰好够用的权限。
第三类是应急时随手放宽。目的地约束因为 known_hosts 不完整而失败,最省事的动作往往是重新加载一把无限制密钥。这个动作会让排障暂时前进,也会把原本清楚的失败信号抹掉。先补齐主机身份、核对别名和链路,比在压力下扩大授权可靠得多。
还有一种反例值得单独说:本机已经失守。agent 转发的限制主要约束远端怎样借用身份,无法修复恶意程序直接控制你本地账号的问题。端点锁屏、密钥口令、硬件确认和及时清理 agent 仍然重要。把所有注意力都放在跳板机,会漏掉身份能力真正驻留的那台电脑。
我会怎样判断这次转发值不值得
我的判断顺序很简单:只需要穿过跳板机,就用 ProxyJump 或客户端的跳板机链路;远端必须继续认证,才考虑临时开启 ssh-agent 转发;开启后再问能否换成独立身份、按次确认、短有效期或目的地约束。只要其中一项无法验证,我就把这条链路视为仍有欠账。
OpenSSH 10.5 值得尽快纳入客户端更新评估,因为它修复了真实的 agent 边界问题。但版本号只能修补实现,不能替你回答“哪台机器可以借我的身份”。跳板机磁盘上没有 SSH 密钥,仍不等于它没有认证能力。把网络通路和身份通路分开看,才是这次更新留给普通开发者最可复用的经验。
