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

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
在 TLS 握手里捎带的、已签名并缓存的 OCSP 应答。

OCSP Staple 免去一段往返校验

把核验交给服务器能省掉一整段往返与一次独立请求;就算没有 Staple,客户端也仍可兜底去查。 服务器只取一次 OCSP stapling 是让「验证」这件事不拖慢握手:客户端本要通过一次额外往返,去 CA 的那台应答服务器(OCSP)查询「这张证书有没有被吊销」。开启 stapling 后,这个查询改成由服务端自己做,然后把查询结果(CA 签过名的那条)放进 TLS 握手一起发过去。浏览器就不用再单独发一次查询了,握手因此更快,也少了一层对 OCSP 提供方是否在线的依赖。 客户端免去一次查询 Staple 的机制是把「每次握手由客户端查」改成「每隔一段时间由服务端查」:服务端以缓存一份带 CA 签名的 OCSP 响应,在窗口内对所有客户端重复“贴”出同一条。每次握手几乎零成本。但 OCSP 响应本身会过期,一般以分钟或天为单位计量;太久、或被 CA 拒签、或验证不过的 staple,会让服务端退回旧路径——干脆不贴。打开的开关在 nginx 里很直白:在 nginx 里把 ssl_stapling 打开,并配置 ssl_stapling_verify 与完整证书链(中间证书)即可。 是 Staple,不是绝对保证 要摆正一个重要认识:stapling 是针对 OCSP 的优化,不是安全边界。它并没有把「吊销检查」做得比不发全程更硬,反而只是把检查挪到了服务器上并缓存。staleness 不影响握手成败——一条过期 staple 更可能让检查退回请求 CA,而不是让 TLS 失败。所以,信任应该建立在 CA 签名与整条证书链之上,而不是 stapling 本身。它真正可度量的收益是省下那一次往返,也消除了 OCSP 提供商宕机时客户端被拖慢的那一层影响。能开就开,几乎没有任何理由不开。 ...

十二月 6, 2027 · 3 分钟 · XB Field Notes
进程不能写根文件系统或切换到零号用户,但可以使用显式允许写入的工作卷。

安全上下文需要运行时证据来验证

Kubernetes 的 securityContext 配置进程身份及部分操作系统约束,可以缩小被攻陷应用允许执行的操作,但本身不会建立完整安全边界。应先确定应用实际需要什么操作,再配置对应字段,并在最终容器中同时测试允许与拒绝的行为。只有一份看起来严格的清单,还不能说明运行中的进程已经按预期受到限制,也不能代替应用本身的权限设计。 把设置放在正确层级 Pod 级配置可以提供用户和组身份、共享卷的组权限行为,以及 seccomp 配置。容器级字段可以覆盖适用的默认值,并设置 capabilities、权限提升和只读根文件系统。需要核对字段真实 API 范围,把熟悉的名称写在错误位置,并不会获得等价约束。相同的 securityContext 名称之下,不代表所有字段都能互换放置。 runAsUser 选择数值用户身份,runAsNonRoot 则要求容器符合非 root 策略。这些设置补充镜像元数据,不能仅凭一个用户名看起来普通,就信任实际权限。卷权限也必须允许所选身份完成必要工作。下面的 emptyDir 通过 fsGroup 提供组访问,使应用能够写入指定工作目录,而不必因此获得对整个根文件系统的写权限。 Linux capabilities 把传统上广泛关联于 root 的权限拆成不同能力。删除 ALL 会移除容器配置中的这些能力。普通容器默认并不继承全部 Linux 能力,而容器内以 root 运行,也不等于已经成功逃逸到宿主机。应审阅真实有效权限,避免用这两种过度简化替代证据。减少权限很有价值,但其效果需要准确描述。 坏掉的样子 allowPrivilegeEscalation false 会为进程设置 no_new_privs,限制通过执行程序获取新权限,例如 set-user-ID 或文件能力引起的身份与权限提升。它不会撤销进程已经拥有的全部权限,也不代替业务授权。应检查实际进程标志和有效能力,不能把每一次被拒绝的操作都归因于这个字段,否则可能误解其他身份或文件权限设置的作用。 readOnlyRootFilesystem 让容器根文件系统只读,其他挂载仍可能可写,包括明确提供的工作卷。这并不是全面禁止执行代码:解释器仍可以读取并运行可写卷中的程序。它也不会阻止网络请求,或撤销对已经挂载数据的读取。只读存储约束的是一类操作,不能直接证明攻击无法继续,更不能把“不能改根目录”扩写成“不能写入任何内容”。 ...

十一月 20, 2027 · 3 分钟 · XB Field Notes
观察、决策、行动三个方框,旁边强调 CT 记录签发而不代表授权。

证书透明度是一本台账,不是机密

证书透明度 CT 让公开 TLS 证书的签发变得可观察,但它不判断域名持有者是否授权了某次签发,不代替证书链验证,也不保证世界上所有证书都已经进入日志。一张错误签发的证书也可能带有有效的透明度证据。真正的价值是留下公共线索,供监控方和受影响的持有者调查。 承诺不等于包含证明 SCT 即签名证书时间戳,是日志对已提交证书或预证书作出的签名承诺:在最大合并延迟 MMD 以内把它纳入树中。SCT 不是另一张证书,也不是证明条目已经存在于树中的包含证明。它的时间戳记录日志作出声明的时刻,不一定就是证书实际签发的精确时刻。 默克尔包含证明把某个已经序列化的叶节点,连接到特定大小的树及其根哈希。验证者需要通过相应的已签名日志声明和可信日志公钥认证这个根。如果无条件接受不可信证明一起附带的根,攻击者完全可以自己构造另一棵树;计算结果相同,也没有建立任何有用的外部事实。 一致性证明回答的是另一个问题:后来的树是否在保持既有前缀不变的情况下扩展了早先的树。一次包含证明不能说明全部历史。签名检查点、一致性验证,以及独立观察者之间对不同视图的比较,有助于发现日志向不同人讲述不同历史。哈希让修改可以被这些检查发现,不是物理上禁止运营者改写数据。 Chrome 对公开受信任的 TLS 证书执行自己的 CT 策略,考虑 SCT 的交付方式、日志状态、数量与运营者多样性。这不是每次打开页面都到公共日志里搜索,也不是判断持有者是否申请过证书。RFC 9162 定义了第二版协议并废止 RFC 6962,但规范发布并不能直接证明某个已部署日志实际支持哪一版。 坏掉的样子 SCT 的签名即使有效,如果对应日志不被接受、数量不足或交付方式不符合目标客户端策略,仍然可能无法通过检查。反过来,符合 CT 也修复不了过期、不受信任或名称不匹配的证书。普通 TLS 验证、CT 合规和签发是否获准,是需要分别确认的三个决定。 域名监控也需要结合业务背景解释。新记录可能来自获准接入的 CDN、通配符证书或正常续签;升级告警之前应核对签发者、名称、指纹与内部签发清单。无法解释的签发应进入调查和纠正流程,不能假定“已经写进日志”就代表它被阻止或被确认合法。 公开日志可能暴露证书中的名称,因此不应仅为做实验就提交私有证书或敏感内部命名。监控覆盖不全、数据过旧,也不能证明某次签发不存在。运营上的监控要声明范围和更新时间,不能把一次空搜索结果当成所有日志、所有时间都没有相关证书的保证。 快速验证 保存为 ct_probe.go,使用 Go 1.23 或更新版本运行 go run ct_probe.go。程序离线使用 RFC 6962 的 SHA-256 默克尔构造,输入是三个合成字符串,不是真实证书,也不是已经按 CT 协议序列化的叶节点。它只验证这个固定三叶树中下标为一的中间叶节点,不是通用验证器。 ...

十一月 3, 2027 · 2 分钟 · XB Field Notes
把握手状态编进序号,让服务器越过队列直接应答。

SYN Cookie 在队列溢出时作答

监听套接字维护的是两个队列,不是一个。半开连接先待在 SYN 队列里,直到客户端的最后一个 ACK 到达;握手完成的连接随后进入 accept 队列,等着应用调用 accept()。两者各有各的上限,而究竟是哪一个溢出,决定了客户端体验到什么。 SYN Cookie 是为第一个队列准备的。它满了以后,服务器不再分配状态,转而把稍后需要的东西编进序号里,于是握手在零内存占用下完成。这确实是一次救援,同时也是一次降级:被救下来的那条连接,比正常情况下本该建立的那条更差。 两个队列,两种症状 一个 SYN 会分配一个 request socket,把连接推进 SYN_RECV 并回一个 SYN-ACK。客户端的 ACK 再把它移进 accept 队列、状态变成 ESTABLISHED。SYN 队列由 tcp_max_syn_backlog 按监听套接字限流——最小值 128,随内存放大,每个表项大约占 304 字节。accept 队列的上限来自 somaxconn(当前内核默认 4096,Linux 5.4 之前是 128),再与应用程序传给 listen() 的 backlog 共同起作用。 两种溢出长得完全不一样。SYN 队列溢出正是 syncookie 为之而生的场景:连接照样能完成。accept 队列溢出更糟也更安静——在 tcp_abort_on_overflow 取默认值 0 的情况下,服务器直接忽略那个最后的 ACK,于是 connect() 返回成功,客户端把请求发出去,然后再也等不到任何回应。实测:backlog 设为 1、客户端 400 个,400 个全都以为自己连上了,服务器实际只能接受 3 个,另外 397 个在两端都没有任何报错的情况下丢掉了。 ...

十月 30, 2027 · 2 分钟 · XB Field Notes
观察、决策、行动三个方框,旁边强调新的共享密钥仍需身份认证。

TLS ECDHE 保证前向保密

前向保密限制的是长期认证密钥日后泄露造成的影响。对于经过正确认证的临时 Diffie-Hellman 交换,攻击者仅取得长期认证私钥,不能据此恢复先前的会话机密。前提是临时机密没有一起被获取;它不会让已经被攻破的端点、导出的密钥日志或保存下来的明文突然变得安全。 两类密钥承担不同职责 ECDHE 中,双方分别生成临时私钥并交换公开份额。每一方把本地私钥与对方公开份额组合,得到相同的共享机密。旁观者虽然能看到公开份额,但在算法的安全假设成立时,缺少重现计算所需的私有值。新鲜的交换材料并不是双方在网络上公开商量出的一段秘密口令。 TLS 也不会把原始 ECDH 输出直接当成应用加密密钥。密钥调度还会派生不同的握手与应用流量机密,绑定握手上下文,并区分方向和用途。临时交换只提供其中一项关键材料,不能用“双方算出相同字节”替代整个传输协议,也不能略过后续派生和认证步骤。 证书按照信任规则把身份绑定到公钥,实际签署握手材料的是对应的私钥,不是证书这个数据对象自己在签名。TLS 1.3 的证书认证路径通过 CertificateVerify 认证握手记录。如果没有身份认证,主动中间人可以替换公开份额,分别和两端建立自己的共享机密;仅仅看到加密正常,并不能排除这种攻击。 RSA 或 Ed25519 认证密钥可以与临时交换并存,签名算法和密钥交换组是不同选择。TLS 1.3 还定义了仅使用 PSK 的恢复模式,后者不具备 DHE 带来的这项前向保密;PSK 配合 DHE 则会加入新的交换材料。可选 early data 的保证更弱,因此不能单凭 TLS 版本或证书算法标签,就推断所有阶段的流量都有相同保护。 坏掉的样子 长期重复使用同一个交换私钥,会削弱原本希望建立的泄露边界。随机数不足、临时私有值暴露、崩溃转储或 TLS 密钥日志,都可能泄露前向保密假设中不能被攻击者取得的材料。日后泄露流量机密,与日后泄露证书签名私钥,不是同一个威胁场景,不能混用结论。 会话进行时攻破端点,可能直接取得明文。长期认证私钥失窃后,能够拦截未来连接的攻击者也可能冒充服务器。前向保密保护的是特定的历史录制流量场景,并不会撤销泄露凭据,也不会修复仍在持续的入侵;这些问题仍需要独立的凭据轮换与事件处置。 机密销毁更是实现层面的问题,不能仅靠一句“用完就丢弃”来保证。把 Go 变量设为 nil,不证明内存、交换空间或诊断材料中的所有副本都已消失。应避免把机密写进日志,并把凭据失窃的处置与历史流量是否仍受保护的判断分开,不能用概念上的前向保密替代真实的数据暴露调查。 ...

十月 27, 2027 · 2 分钟 · XB Field Notes
内核依据设备噪声为加密安全随机数发生器供种。

内核把设备噪声转成密钥

即便一台很安静的服务器,在真正需要随机数之前也早就完成了播种,所以今天还会出问题的随机性事件几乎都不是「熵不够」,而是「状态被重放」:镜像里烤死的种子、被恢复的快照、回滚到上一次重播种之前的状态。内核并不朝你一滴一滴地滴熵;它把一点点设备噪声浓缩成一个生成器,然后无限地吐密钥流。 这个反转正是为什么围绕熵的大部分运维经验已经失效。盯着 entropy_avail、为取密钥去读 /dev/random、装一个补熵守护进程,这三件事针对的都是内核早已不再存在的稀缺;而真正会毁掉密钥的那件事——两台机器拿到同一个种子——在这些习惯里一个都看不出来。 熵池是输入,生成器是产物 内核把物理噪声混进来:中断时序抖动、设备延迟、平台自带的硬件随机数发生器,以及基于 CPU 的 jitterentropy;再用 BLAKE2s 把它们抽取进熵池。熵池给一组 ChaCha20 CRNG 播种,真正产出字节的是这些 CRNG。播种之后收集仍在继续,生成器也会被周期性重播种,但你已经吐出去的每一个字节,其强度由它出生时的那个状态决定,之后再发生什么都改不了。 一旦初始化完成,输出就是密钥流,而不是有限资源。entropy_avail 稳定在 256 位,也就是一个 ChaCha20 密钥的长度,而且你读它它也不会掉;读走 1 MiB 不消耗任何池内配额。random(4) 至今仍把 entropy_avail 描述成 0 到 4096 之间的取值、把 poolsize 描述成 Linux 2.6 之后固定 4096 位——正是这类文档让一条过期的告警一直活着。 /dev/random 不再是「更谨慎」的选择 从 Linux 5.6 起,/dev/random 在早期启动之外不再阻塞,其上的 O_NONBLOCK 也被忽略。那个独立的阻塞池已经消失,剩下的只是同一个生成器顶着历史名字。getrandom(2)(Linux 3.17 引入)默认从 urandom 源取数,只在那个源尚未初始化时阻塞;GRND_NONBLOCK 把这种阻塞变成 EAGAIN,GRND_RANDOM 则改为选择 random 源,也就是那个仍然可能无限期阻塞的源。 ...

十月 22, 2027 · 1 分钟 · XB Field Notes
附着在容器镜像上的摘要与加密签名。

签名镜像可以证来源

容器镜像的引用是一个可变指针,摘要则不可变。签名把摘要和一把密钥绑在一起,于是校验方能在运行之前先问:这些字节出自谁手。这正是 registry 与清单层回答不了的问题——它们只能告诉你字节从哪里来,不能告诉你谁做的。 张力在于:签名只有在被校验时才是一项控制,而校验只有在信任根被管理好时才有意义。签名只是流水线里的几行;决定哪些密钥算数、一把密钥退役之后怎么办,才是真正的工程。 摘要是身份,标签是指针 摘要是对精确清单字节的哈希,镜像一变摘要就变。对摘要签名,就是把签名绑在内容上而不是名字上。用 EC P-256 密钥对、真实的 SHA-256 摘要和一份 cosign 形状的载荷,openssl dgst -verify 返回成功;把产物翻转一个字节,摘要随之改变,同一次校验返回失败。 标签的行为不同。三个标签指向同一个已签名摘要,全都能通过校验,因为标签从来不在被签名的载荷里。这正是设计:标签是指向摘要的指针,改指向是常规的 registry 操作。把标签挪到一个从未被签名的摘要上,对解析后摘要的校验就会失败——但前提是校验方先去解析标签,而不是直接相信你手上那个可变的引用。 对标签签名绑不住任何东西 如果签的是字符串 app:v2 而不是摘要,校验就会永远成功,因为这个字符串永远不变。把那个标签指向一个完全不同的镜像,签名照样通过:它对一个字符串说了真话,而这个字符串对字节什么都没说。所以签名必须签在摘要上,校验必须先解析标签再比对。 信任根是另一半。签名只证明「持有私钥的人产出了它」;公钥,或者一张由 Fulcio 签发、并配有一条 Rekor 日志条目的证书,才把它变成「这个身份构建了它」。分发和轮换这个锚点就是安全边界,而它完全在签名之外。 校验才是控制,签名不是 流水线签完名就直接部署、不做校验,买到的只是装饰。实测对比很直白:被篡改的产物在部署步骤做校验时被挡住;同一条产物在部署步骤只记录来源信息时顺利上线——而那份来源记录从此写着被篡改的摘要。强制点应该在准入,或者部署前的一道闸门,让未签名的镜像根本到不了节点。准入控制器在这里的价值,是它不依赖流水线的自觉:策略在对象进入集群的那一刻求值,跳过签名不是「忘了做」,而是直接失败。 撤销是签名内部完全没有答案的那部分。签名没有有效期、没有吊销列表:在它的密钥下永远有效。轮换密钥并不能撤销任何东西——旧签名在新密钥下被拒,在旧密钥下依旧被接受——所以任何持有已退役密钥的人都能继续产出可通过校验的产物。解法是配短时效证书和透明日志的信任根,再加上钉住身份的策略,而不只是检查「存在某个签名」。 坏掉的样子 最经典的是策略引擎从未检查过的未签名镜像。流水线签名,部署步骤按标签拉取并运行,而这个缺口在有人往同一标签推入不同字节之前一直不可见。审计记录从此为一份没人校验过的镜像声称来源,这比完全没有来源记录更糟。 第二种是签名能通过、但签名所用的密钥你已经不再信任。因为签名内部没有撤销机制,泄露或已退役的密钥会继续签出可接受的产物,而爆炸半径是那把密钥签过的每一个产物,且是回溯性的。在策略里钉住签名身份,让证书保持短时效,并把签名记进透明日志,这样一把意料之外的密钥会以一条日志条目出现,而不是一次无声通过。 快速验证 校验摘要,再确认标签仍然指向它: cosign verify --key cosign.pub registry.example.com/app@sha256:9ab5a134... cosign tree registry.example.com/app@sha256:9ab5a134... skopeo inspect --format '{{.Digest}}' docker://registry.example.com/app:latest cosign verify 必须打印出被校验的身份并以 0 退出,cosign tree 应列出挂在该摘要上的签名与证明。skopeo inspect 必须打印出你刚校验过的同一个摘要。错误的结果是:verify 报找不到匹配的签名,或者 verify 成功而 skopeo 报出另一个摘要——那意味着标签现在指向从未被签名的字节。 ...

十月 21, 2027 · 1 分钟 · XB Field Notes