客户端签名时去询问的内存密钥库。

ssh agent 传递密钥而不暴露

agent 转发并不等于 agent。转发是请远端主机上的 sshd 自己开一个套接字,并把那边的 SSH_AUTH_SOCK 指过去,于是那台机器上的程序可以回头找你家里的 agent 签名。远程自始至终没有拿到密钥,它也根本不需要拿到:它拿到的是这份能力的完整使用权。 这是穿过堡垒机抵达内网机器的常规做法,同时也意味着路径上每一跳都分到了这份能力。多数伸手去拿 -A 的人,真正想要的是 ProxyJump——它只转发 TCP,把 agent 留在本机。 两者容易被混为一谈,是因为它们用的是同一套协议、同一个 SSH_AUTH_SOCK 变量,区别只在于那只套接字落在哪台机器上。agent 留在本机,它就是你的私钥保险箱;套接字出现在远端,它就是一张那台机器上任何程序都能使用的通行证。 远程拿到的是套接字,不是密钥 ssh_config(5) 对这笔交易毫不含糊。ForwardAgent 接受 yes、no(默认值)、一个明确的套接字路径,或某个环境变量的名字;手册警告说,能够在远端绕过该 agent 套接字文件权限的用户,可以经由这条转发连接访问你本地的 agent。更要紧的是紧接着的那句:攻击者无法从 agent 取得密钥材料,但可以对密钥执行操作,从而用已装载的身份完成认证。 本文的实测正好呈现了这个形状。一个只拿到 SSH_AUTH_SOCK、既没有密钥文件也没有口令的第三方进程,列出了身份并拿到了一枚 88 字节的签名。这就是转发交给某一跳的全部能力。 转发是逐跳选择的 能签名的机器集合,恰好就是那些以启用转发的方式运行 ssh 的跳数。ssh -A bastion 把 agent 暴露给 bastion;从那里再 ssh -A build-box,build-box 也一并拿到;而 ssh build-box 不带 -A,链条就止步于 bastion。没有任何一跳会意外地级联下去,但也没有任何东西会提醒你。 ...

二月 9, 2028 · 1 分钟 · XB Field Notes
一个常驻帮手,用只装在内存里的已解锁密钥来应答签名请求。

ssh-agent 在会话期间替你记住已解密的密钥

ssh-agent 是一个持有私钥、并以公钥认证方式替你应答签名请求的程序。ssh 通过 SSH_AUTH_SOCK 找到它,那是一个 Unix 域套接字的路径;于是密钥只解锁一次,之后每一次连接都借用这份身份。私钥本身从不交给客户端。 这既是一项真实的安全属性,也是一项真实的负债。已解密的密钥不会落到磁盘上,但 agent 是一座活着的签名服务:任何能够到那个套接字的东西都能签名,只要身份还装着就一只能签。 socket 就是那份能力 agent 协议里没有任何一个动词用于导出密钥。请求身份列表(消息 11)返回消息 12,内容是公钥 blob 与注释——本文这次实测里是 51 字节,从来不是私钥材料。请它对一段任意挑战签名(消息 13)返回消息 14,一枚 88 字节的 Ed25519 签名。未定义的 opcode(99)返回消息 5,也就是 SSH_AGENT_FAILURE。把私钥文件改名挪走之后,下一次签名依然成功,这正是要点:材料活在 agent 的内存里。 这份内存靠套接字上的文件权限来保护,除此之外没有更强的屏障。该套接字是 Unix 域套接字,模式 0600,属主是启动 agent 的那个 uid。另一个只拿到 SSH_AUTH_SOCK 这一个环境变量、既没有密钥文件也没有口令的进程,照样列出了身份并拿到了一枚签名。 只解锁一次,然后给期限 ssh-add 是唯一需要口令的一步。它读取密钥文件(该文件应当是 0600;ssh-add 会忽略任何他人可读的身份文件),把解密后的身份交给 agent。此后就由 agent 替你应答。ssh-add -t 8h 设定最长存活时间,单位可以是秒,也可以是 sshd_config(5) 里的时间格式;本文这次实测中,ssh-add -t 2 加完之后身份仍在列表里,三秒后再查已经一个都没有了。 ...

二月 27, 2027 · 1 分钟 · XB Field Notes
一条已记录的 SSH 主机指纹连接到一个密钥符号。

SSH 主机密钥是部署记录的一部分

记录期望的指纹,使用斟酌过的 StrictHostKeyChecking 策略,并把变化当作事故来对待。

七月 28, 2026 · 1 分钟 · XB Field Notes
一把被限定在单一身份与狭窄权限范围内的 SSH 密钥。

SSH 密钥卫生,主要是限制密钥能干什么

SSH 密钥很方便,而方便正是在不知不觉中失控的原因。实践上的关键纪律并不是少生成几把密钥,而是让每把密钥都成为有清晰用途、有限生命周期、并且能在"谁被允许做什么"的文件里留下记录的一个具名身份。 一把密钥应被当作一个身份 一把 SSH 密钥应当能读作一个身份,而不是一句无密码的便利。给它起名:为了它属于谁、为什么存在,而不是为了它恰好躺在那台笔记本;给它一个简短的注释,并在你的工具支持时给私钥配一个口令,让一份只拿到了文件的人无法直接使用。一把「一看就知道属于谁、可以做什么」的密钥,能让后续的运维者不假思索就应用最小权限原则——这正是「上了锁的凭据」和「一堆谁也认不出的无名钥匙」之间的分水岭。 范围才是关键 范围才是密钥的要害。尽量让一把 key 只能碰到它该碰的东西:在服务端,通过账户与 authorized_keys 的设置,只接收该接收的 key,并限制每把 key 具体能做哪些——别一股脑把整台机器的大权交给一个起自自动化的角落。遇到自动化用途,更要按命令、源或目录把它钉死在一个边界内;一把范围被钉死的 key,在泄漏时,泄漏时,损失的也只是「这个被定义好的边界」,而不是一把「每一扇门都能开」的总钥匙。 轮换与审计线索 旧钥匙不会自己走。一套轮换的节奏,才是让清单长久诚实的办法:删掉已经没人再用的 key,把剩余的交实际负责人重新签发,并过一段时间再确认一次——「现在被接受的这些 key,是不是真的对应我要的那些人、那些用途」。authorized_keys 本身,就是把这份「谁允许做什么」变成可复核的文件的实体。这样一个定期、便宜又明确的检查,抵得上一百次在事故后茫然翻找。 坏掉的样子 没有口令的私钥,会让每一个副本都变成一次登录。入职克隆笔记本、把个人配置推到共享仓库、备份软件同步家目录,都会顺带复制 ~/.ssh。只要有人能读到其中一份,就能立刻登录,而服务端日志分辨不出小偷和主人。全程没有任何口令提示,故障是静默的,往往要等日志里出现陌生地址才会被发现。 长期残留的 authorized_keys 条目,是另一种同样安静的失效。工程师离职、合作到期、定时任务下线,公钥却还留在服务器上,因为没人负责清理。几个月后,那一行被遗忘的记录依然有效,而且大多没有 from=、command= 这类限制,等于没有使用边界。只发新钥而不删旧钥的轮换,每转一圈都在扩大攻击面。 第三种失效是算法老旧。ssh-dss 和短长度的 ssh-rsa 在宽松配置的服务端上依然能登录,尽管主流建议早已转向 Ed25519。连接一旦成功就不会告警,而攻击者截获的每次握手,都能在弱算法上拿到大得多的杠杆。正确做法是生成新类型密钥并删掉旧行,而不是让两种长期并存。 第四种是代理转发劫持。用 ssh -A 登录时,本地代理套接字会暴露到远端主机,有足够权限的人可在会话期间让你的代理替他签名。私钥文件纹丝未动,登录却以你的名义扩散到更多主机。症状是:横向移动发生了,现场却找不到被盗密钥,只剩转发打开期间产生的一串签名。 快速验证 把下面的脚本保存为 ssh_audit.py,运行 python3 ssh_audit.py。它只用 Python 标准库,只读当前用户 ~/.ssh,不做任何修改;没有密钥文件的机器报告 SKIP 而非报错,新机器也能干净跑完。 ...

七月 6, 2026 · 3 分钟 · XB Field Notes