LabHub
学习 学习路径 课程

KCA — Kyverno 认证助理

签名证明的不是「安全」,而是「出自这里」

在 LabHub 中继续学习

一句话总结

镜像签名并不表示镜像是安全的,而是表示它来自某条特定流水线。充满漏洞的镜像同样可以被签名,这不是签名失效,而正是签名的定义。verifyImages 规则用于在 admission 阶段强制验证这项来源证明。

概念图: 一句话总结 · 为什么需要它 · 它如何工作 · 实际现场中的表现

为什么需要它

镜像存在于镜像仓库,并不能保证任何事情。它可能由某人在笔记本电脑上构建并手工推送,标签也可能稍后被其他内容覆盖。无论 CI 流水线中的扫描关卡多么严密,只要绕过流水线推送的镜像仍能在集群中运行,这些关卡就更像建议,而非强制要求。

签名可以堵住这个缺口。只使用流水线掌握的密钥(或流水线的 OIDC 身份)为镜像签名,集群则只接受带有该签名的镜像。这样,扫描关卡才会真正得到强制执行。扫描结果正是在这里转化为约束力。

它如何工作

verifyImages 规则会从 Pod 规范中取出所有镜像引用,只选择与 imageReferences 模式匹配的引用,并向镜像仓库查询签名。这里容易忽略的一点是:该查询会成为一次离开集群的网络调用。启用策略后,镜像仓库不再只是下载镜像的路径,也成为决定 Pod 能否创建的路径。

验证者通过 attestors 表达,支持静态公钥(keys.publicKeys)、KMS 和 keyless 三种方式。keyless 使用 Fulcio 签发的短期证书进行签名,并在 Rekor 透明日志中留下记录,其中 subjectissuer 是关键字段。issuer 表示所用的 OIDC 提供者,subject 表示该提供者体系中的具体身份。对于 GitHub Actions,subject 甚至包含工作流文件路径与引用标签,因此,只要更改发布工作流文件名或标签规则,所有验证就会立即被阻止。这种精确性正是设计目标,却也带来了运维负担。

attestors.count 是必须了解的陷阱。若不指定它,entries 中的所有条目都必须通过。新增一把密钥后突然全部失败,多半就是这个原因。若同时放入旧密钥与新密钥并把 count 设为 1,使用任一密钥签名的镜像都能通过,从而无中断地度过轮换期;若省略 count,则只有同时由两把密钥签名的镜像才能通过,结果完全相反。

attestation 是附加在签名之上的声明。SLSA provenance 记录“该镜像由哪个构建器、从什么源代码生成”,CycloneDX 或 SPDX SBOM 则记录“其中包含哪些组件”。可以使用 conditions 检查这些内容,例如要求 SBOM 中某个库的版本不得等于已知漏洞版本。如果只验证签名,就没有检查由谁构建,因此最好同时设置 provenance 条件。

还需要整理四个标志。required 强制所有匹配的镜像都必须完成验证;verifyDigest 强制使用 digest 本身;skipImageReferences 是从匹配中排除的模式列表;mutateDigest 会把标签改写为 digest。最后一项对流水线影响很大。启用后,Pod 规范中保留的将不再是标签,而是以 @sha256: 开头的固定引用。因此,即使镜像仓库中的同名标签被覆盖,扩容时新创建的 Pod 仍会使用最初固定的镜像。这正是所需的不可变性;但原先依赖推送标签、再删除 Pod 以获取新镜像的流水线,从此会悄无声息地不再产生任何效果。

缓存也会影响性能。镜像验证结果会保存到 TTL 缓存中,这属于安装级设置,而非策略字段。默认启用,最多保存 1000 个键,TTL 为 60 分钟。在包含 20 个副本的滚动发布中,真正支付镜像仓库往返成本的只有第一次,因此第二次测量会比第一次快得多,而 TTL 到期后的首次部署会格外缓慢。只有关闭缓存后测量 admission 延迟,才能得到更接近镜像仓库宕机时最坏情况的数值。

实际现场中的表现

作者的家庭实验室使用 Harbor 镜像仓库,地址为 MetalLB 地址池分配的 10.0.0.202。使用内部镜像仓库后必然会遇到一个问题:凭据路径有两条。Pod 能够正常拉取镜像,与 Kyverno 能够查询签名是完全不同的路径。Pod 使用自己的 imagePullSecrets,Kyverno 使用自己的凭据。如果 Kyverno 无法读取私有镜像仓库,即使签名完好无损,看起来也会像根本不存在。

扫描实践也得出了同样的结论。某个镜像中共发现 1,247 项漏洞,其中 Critical 有 9 项;只保留已有修复版本的问题后剩 4 项,再与确认已被真实利用的清单比对后只剩 1 项。而清除这 1,247 项漏洞的方法并不是逐个打补丁,而是替换基础镜像。应用实际链接的共享库只有 8 个,但镜像中却安装了 432 个软件包。改用 distroless 后,操作系统软件包漏洞降为 0,只剩应用 npm 依赖中的 2 项。签名与扫描应当这样衔接:通过扫描精简镜像,为通过检查的镜像添加签名,再让集群只接受带签名的镜像,三者构成完整的一套流程。

还有一点:如果只把镜像复制到镜像仓库镜像站,却不同时迁移签名制品,这项策略就会毫无例外地全部失败。这是把镜像引入隔离网络时最常见的情况。在通过 repository 另行指定签名位置,或修正镜像同步流水线之前,不应启用 Enforce。

下一项实验将做什么

我们会在 /root/kca-verify/ 中编写同时使用静态密钥与 keyless 的 verifyImages 规则、SLSA provenance 与 SBOM attestation 规则,以及允许镜像仓库的 validate 规则。随后把镜像仓库凭据 Secret、挂载该凭据的 ServiceAccount,以及固定到 digest 的 Deployment 实际部署到集群中。