容器镜像从构建到最终运行,中间要经过镜像仓库、网络传输、节点拉取等多个环节,任何一环都可能成为攻击者注入恶意代码的入口。镜像签名与验证机制正是为了解决这个问题而生:发布者用私钥对镜像进行签名,运行环境用公钥验证签名,一旦镜像内容被篡改,验证就会失败,从而阻断被污染的镜像进入生产环境。本文将从原理、主流工具和落地实践三个层面,完整拆解这套可信分发机制。

一、镜像签名的底层原理:为什么必须做密码学签名
很多团队对镜像安全的理解停留在扫描漏洞层面,但扫描只能发现已知问题,无法回答一个更根本的问题:这个镜像到底是不是我们构建出来的那个?镜像在传输过程中可能被中间人替换,镜像仓库被攻破后镜像可能被重新打包,甚至内部人员也可能在构建流程外偷偷推送一个同名镜像。这些场景下,漏洞扫描毫无作用。
镜像签名的本质是把镜像的摘要(digest)与发布者身份绑定在一起。镜像的digest是对镜像分层内容做SHA256哈希得到的唯一指纹,只要镜像内容有任何一个字节的变化,digest就会完全不同。签名过程就是发布者用自己的私钥对digest进行加密运算,生成签名数据并存储到镜像仓库或独立存储中。验证方拿到镜像后,先计算镜像的实际digest,再用发布者的公钥解密签名进行比对,两者一致则证明镜像内容未被篡改,且确实出自持有私钥的一方。
这套机制的安全性完全依赖私钥的保管。私钥一旦泄露,攻击者就可以冒充发布者签署恶意镜像。因此成熟的方案都会配套密钥管理策略,比如将私钥存放在KMS、HSM或硬件设备中,避免私钥落地到构建机器的磁盘上。理解这一点非常重要,后面讨论的工具选型时,密钥管理能力是核心的评估维度之一。
二、主流签名方案对比:Docker Content Trust与cosign
Docker官方的Content Trust机制基于Notary项目实现,通过docker trust sign或设置DOCKER_CONTENT_TRUST=1环境变量启用。它把签名元数据存储在镜像仓库中独立的tuf目录下,采用The Update Framework框架管理密钥的轮换与分发。这套方案与Docker生态结合紧密,但存在明显局限:部分云厂商镜像仓库对Notary支持不完整,而且Kubernetes原生并不感知Content Trust的验证状态。
Sigstore项目的cosign是近年来更受青睐的选择。它的设计哲学是签名与镜像仓库解耦,签名数据以OCI Artifacts的形式存储,也可以存放在独立的Rekor透明日志服务中。cosign支持keyless模式,利用短期有效的证书结合OIDC身份(比如GitHub Actions的身份)完成签名,证书过期后签名仍然可以通过透明日志审计追溯。这种方式免去了长期密钥管理的负担,特别适合CI/CD流水线场景。两种方案的对比可以总结如下:
| 维度 | Content Trust | cosign |
|---|---|---|
| 签名存储 | 镜像仓库内Notary目录 | OCI Artifact或Rekor日志 |
| 密钥模式 | 需维护根密钥与委托密钥 | 支持keyless短期证书 |
| K8s集成 | 需额外适配 | 与Kyverno等策略引擎配合成熟 |
| 社区活跃度 | Notary v1已停止演进 | 持续更新,CNCF毕业项目 |
从趋势看,新的项目基本都会选择cosign。Notary v1的维护已经停滞,虽然Notary v2正在开发中,但生态兼容性还远不如Sigstore成熟。如果是存量系统已经使用Content Trust,可以逐步迁移;新项目直接采用cosign是更稳妥的选择。
三、实战:用cosign完成镜像签名与验证
先看签名的基本操作。安装cosign后,第一步生成密钥对:
# 生成密钥对,私钥建议设置口令保护 cosign generate-key-pair # 对镜像签名(使用本地私钥) cosign sign --key cosign.key myregistry.ippipp.com/myapp:v1.0
签名完成后可以验证:
# 使用公钥验证镜像签名 cosign verify --key cosign.pub myregistry.ippipp.com/myapp:v1.0 # 验证时会输出签名的详细内容 # 包含镜像digest、签名者、时间戳等关键信息
在CI流水线中更推荐keyless模式。以GitHub Actions为例,job的身份会被自动用于获取短期证书,整个流程不需要管理任何长期私钥:
# keyless模式签名,CI环境中无需保存私钥
cosign sign myregistry.ippipp.com/myapp:${{ github.sha }}注意一个细节:签名针对的是镜像digest而不是tag。tag是可变的,同一个tag可以指向不同的镜像内容,如果只对tag签名,攻击者可以在签名后重新推送同名tag指向恶意镜像。正确的做法是流水线中先解析出digest,然后基于digest进行签名和部署,Kubernetes的Pod spec中也应该尽量使用image@sha256:...的形式而非可变的tag。
四、集群侧强制验证:用Kyverno拦截未签名镜像
仅在客户端验证是不够的,必须在集群准入控制层面强制执行,否则任何绕过流水线的部署都能让签名机制形同虚设。Kyverno提供了verifyImages规则,可以在镜像拉取前校验签名,验证失败的请求直接拒绝。
以下是一个完整的ClusterPolicy配置,要求default命名空间中的所有镜像必须由指定公钥签名:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.ippipp.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
这里填入cosign公钥内容
-----END PUBLIC KEY-----这个策略的关键配置点有几个。validationFailureAction: Enforce表示验证失败直接拒绝创建,如果设置为Audit则只记录日志不拦截,适合灰度上线阶段观察影响面。imageReferences限定策略只作用于特定仓库的镜像,避免对外部公共镜像一刀切。attestors中声明的公钥就是验证的信任锚,集群管理员务必妥善保管公钥的真实性,可以通过内部配置管理工具分发。
上线这类强管控策略前,建议先以Audit模式运行一段时间,收集哪些工作负载使用了未签名镜像,推动业务团队完成签名改造后再切换Enforce。同时要考虑豁免机制,比如给特定命名空间或标签开白名单,避免阻塞紧急修复:
# 策略中增加排除规则,避免误伤
rules:
- name: exclude-emergency
match:
any:
- resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- emergency-hotfix另外需要注意准入验证的性能开销。每次创建Pod都会触发一次对镜像仓库的签名查询,如果仓库响应慢会拉长Pod创建耗时。生产环境应确保Kyverno与镜像仓库之间网络通畅,必要时启用Kyverno的签名缓存功能,对已验证过的digest缓存结果。
五、构建端到端的可信镜像分发链条
签名与验证只是整个供应链安全体系中的一个环节,要让机制真正发挥作用,需要把它嵌入完整的流程闭环。在构建阶段,CI流水线完成镜像构建后立即基于digest签名,签名失败则流水线中断,不允许未签名镜像推送到业务可用的仓库。在分发阶段,所有部署清单统一引用digest而非tag,确保运行的内容与签名内容严格一致。在运行阶段,准入策略强制验证签名,配合Pod Security Admission限制特权容器,多层防御叠加。
密钥管理是链条中最脆弱的环节,有几个实践建议值得参考。私钥优先托管到云厂商KMS或自建Vault,cosign原生支持各类KMS后端,签名时私钥不会离开KMS;构建环境使用keyless模式时,要确保OIDC身份配置正确,防止身份被冒用;定期轮换密钥并在策略中维护多公钥列表,实现平滑过渡而不是硬切换。
最后建立审计能力。Sigstore的Rekor服务记录了所有签名行为的不可篡改日志,配合定期审计可以及时发现异常签名。运维侧也可以周期性扫描集群中实际运行的镜像,比对是否都在已签名清单内,形成运行时的持续核验能力。把签名从单点工具升级为体系化流程,容器镜像的安全边界才算真正建立起来。