镜像从构建流水线进入集群,中间会经过镜像仓库、拉取凭据、准入控制、kubelet 缓存等多个环节;只要有一个环节允许未经授权的摘要进入,前面的签名工作就难以发挥作用。常见风险位置包括:使用可变 tag 覆盖已发布镜像、仓库写入权限过宽、准入控制器只覆盖部分命名空间、以及平台组件与注入的边车容器被策略遗漏。本文面向已有基本 CI/CD 与 Kubernetes 集群的团队,说明如何在部署前把签名与完整性校验落到准入阶段,并给出灰度验证、影响评估和可执行回滚路径。需要先明确一个边界:签名验证只证明镜像来源与内容未被篡改,不证明镜像没有漏洞,也不能替代镜像扫描、RBAC 收紧和发布流程约束。

准备:先确认验证对象与信任根
准入阶段的签名校验不是孤立配置,它依赖三样前置条件:可验证的签名、明确的信任身份、以及能够读取镜像元数据的准入组件。
上游 Kubernetes 提供 ImagePolicyWebhook 作为对接外部镜像策略服务的扩展点,但签名解析与策略判断通常由外部准入控制器完成,例如基于 Sigstore 校验的实现或云厂商提供的 validation hook。不同实现的能力差异很大:有的只校验镜像签名,有的同时校验证明(attestation)与 SBOM;有的支持密钥模式,有的依赖 OIDC 身份与证书链。因此第一步不是选工具,而是把“谁能签、签什么、验什么”写成可审计的信任声明。
准备阶段建议完成以下盘点:
- 镜像来源清单:区分自建镜像、上游基础镜像、平台组件镜像和第三方 Operator 镜像,按仓库与路径分组。
- 签名主体:确定由发布流水线中的哪个工作负载身份签名,密钥或证书如何托管、如何轮换、私钥泄露时如何吊销。
- 验证粒度:策略应绑定不可变摘要(digest),而不是 tag。tag 可被重新指向,摘要不会。
- 准入组件边界:确认准入控制器覆盖哪些命名空间、是否拦截
Pod与更上层的Deployment、对控制器创建的 Pod 是否同样生效。
可以用以下命令先做一次现状盘点,输出结果可作为后续影响评估的基线:
kubectl get pods -A
-o jsonpath='{range .items[*]}{.metadata.namespace}{"t"}{.spec.containers[*].image}{"n"}{end}'
| sort -u
同时对关键镜像手工验证一次签名,确认信任根可用:
cosign verify --key cosign.pub
registry.example.com/apps/api@sha256:<digest>
这一步的价值在于把“策略是否配置正确”和“签名是否真的存在”分开排查。如果大量存量镜像根本没有签名,直接进入阻断模式会造成大面积发布失败,应先补签或先缩小策略作用域。
准入策略设计:从窄作用域和审计模式开始
策略设计的第一原则是作用域要窄。首个策略只覆盖组织能够控制、且已完成签名的镜像路径,例如 registry.example.com/apps/*,而不是一上来要求全集群所有镜像必须签名。平台组件、CNI、监控代理、注入的边车容器往往来自不同仓库,签名体系也不同,混在一条规则里会让策略难以解释,也难以回滚。
第二条原则是先审计、后阻断。以 Kyverno 为例,validationFailureAction: Audit 只记录违规而不拒绝请求,配合 PolicyReport 可以量化“如果现在开启阻断,会有多少工作负载失败”。这与直接在生产开启 Enforce 的差别,是把一次高风险变更拆成了可观测的两步。其他准入实现通常也提供 dry-run、warn 或 audit 等等价模式,名称不同,思路一致。
一个最小化的策略示意如下,仅用于说明结构,具体字段需按所选准入控制器的版本文档核对:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-app-image-signatures
spec:
validationFailureAction: Audit # 先审计,验证后再切换为 Enforce
background: false
rules:
- name: verify-apps
match:
any:
- resources:
kinds: [Pod]
namespaces: [app-test] # 灰度阶段只覆盖测试命名空间
verifyImages:
- imageReferences:
- "registry.example.com/apps/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
<公钥内容>
-----END PUBLIC KEY-----
设计时需要明确几个容易踩坑的点:
- 匹配层级:只匹配
Pod时,Deployment、CronJob、Job创建 Pod 仍会经过准入,但失败信息会出现在控制器事件里,排障路径更长。是否同时匹配工作负载资源,取决于团队希望错误在哪一层暴露。 - 系统命名空间:不要用全局排除
kube-system的方式“解决”报错,这会留下稳定的绕过路径。更稳妥的做法是为平台组件单独定义信任来源,或在其镜像完成签名前用带到期时间的例外名单管理。 - 边车与初始化容器:服务网格注入、日志采集边车、initContainer 都在校验范围内,盘点时必须把它们算进去,否则灰度阶段会出现“业务镜像已签名、注入容器未签名”的阻断。
- 摘要固定:若准入实现支持把 tag 改写为 digest,应评估其对现有发布流程的影响;改写能降低 tag 漂移风险,但也会改变工作负载对象的实际内容。
- 失败模式:准入 webhook 的
failurePolicy决定了校验服务不可用时是放行还是拒绝。Fail更安全但会放大故障面,Ignore可用性更好但存在绕过窗口,需要结合集群规模与 SLO 决定,并写入变更说明。
灰度验证:在测试命名空间同时验证放行与阻断
灰度不是只看“部署成功”,而是要证明两类结果都符合预期:合法签名镜像被放行,未签名或签名身份不符的镜像被拒绝。建议在独立测试命名空间中准备四个用例,逐条记录预期与实际结果。
| 用例 | 镜像状态 | 预期结果 |
|---|---|---|
| A | 已签名,摘要与签名一致 | 放行 |
| B | 完全未签名 | 拒绝 |
| C | 有签名,但签名身份不属于信任列表 | 拒绝 |
| D | tag 指向的摘要已被替换 | 拒绝,或按摘要校验失败 |
用例 B 和 C 可以用临时 Pod 验证,观察拒绝信息是否包含足够的排障线索:
kubectl run unsigned-check
--image=registry.example.com/apps/unsigned:test
-n app-test --restart=Never
如果策略处于 Audit 模式,上面这条命令会创建成功,但违规记录会进入 PolicyReport;切换到 Enforce 后同样的命令应返回拒绝。两次结果都要留档,它们共同构成“策略确实生效”的证据。

除手工用例外,还应覆盖真实发布路径:用一次完整的 CI 构建产物走完部署,确认流水线签名、仓库推送、准入校验三段没有断点。特别要检查流水线是否在签名后重新打了 tag 或触发了镜像复制,这类操作可能让签名与最终摘要脱钩。
监控观察点建议在灰度期间保持开启:
- apiserver 审计日志中与准入拒绝相关的事件数量与来源命名空间;
- 准入控制器自身的拒绝计数、校验延迟与错误率;
- PolicyReport 中违规资源的分布,按命名空间和镜像仓库聚合;
- 发布失败率与回滚次数,确认没有把策略问题误判为应用故障。
影响评估:把存量工作负载算清楚
准入策略的爆炸半径通常不在新部署,而在存量工作负载的重启、扩缩容和滚动更新。评估时应区分三类影响:
- 可立即满足策略的工作负载:镜像已签名且摘要匹配,切换
Enforce后行为不变。 - 需要补签或改造的工作负载:镜像可追溯但签名缺失,需要在流水线补签并重新发布;这类工作量往往被低估。
- 无法满足策略的工作负载:来源不可控的第三方镜像、已停止维护的旧版本、带私有补丁的镜像。这类需要明确处置方式:替换、隔离到例外命名空间、或限期豁免。
评估时还要注意控制器行为差异:Deployment 滚动更新会创建新 Pod,若新 Pod 被拒绝,旧副本是否保留取决于 maxUnavailable 与控制器状态;StatefulSet 与 DaemonSet 的失败表现又不同。对关键业务,建议在灰度前用一次副本扩缩容演练确认失败时的实际状态,而不是假设它会“自动保持旧版本”。
权限边界也需要同步收紧。签名校验只约束“部署什么”,并不约束“谁能推送”。如果仓库写入权限过宽,攻击者仍可能推送一个用合法密钥签名的恶意镜像,或用被泄露的签名身份签名。因此需要配合:仓库的不可变 tag 策略、CI 身份的最小权限、签名密钥的访问审计,以及镜像扫描作为独立门禁。
回滚安排:分级降级,保留证据
回滚方案要在变更前写好,而不是在阻断生效后临时决定。建议按影响程度设计三级路径,并明确每级的触发条件与执行人。
第一级:策略降级为审计模式。 将 validationFailureAction 从 Enforce 改回 Audit,或把灰度命名空间移出匹配范围。这是恢复最快、副作用最小的操作,适合“拒绝量超出预期但准入服务本身正常”的场景。降级后应保留 PolicyReport 采集,避免失去观测。
第二级:收窄规则作用域。 如果问题集中在某类镜像(例如注入边车或平台组件),可临时把该路径从 imageReferences 中移除,而不是关闭整条策略。收窄时要记录移除的路径、原因和恢复时间点。
第三级:停用策略或准入配置。 仅在准入控制器本身异常、导致 apiserver 大量请求失败时使用。直接删除策略对象与删除 webhook 配置的影响不同:前者通常可由控制器回收关联配置,后者可能留下孤儿资源或让其他策略一并失效。执行前应备份策略 YAML 与 webhook 配置,执行后立即验证 Pod 创建、扩缩容与发布是否恢复。
回滚后的验证同样需要留痕:
# 确认策略当前模式
kubectl get clusterpolicy verify-app-image-signatures -o yaml | grep validationFailureAction
# 确认准入配置与拒绝情况
kubectl get validatingwebhookconfigurations
kubectl get events -A --field-selector reason=FailedCreate
应急豁免必须可控:为豁免对象打上明确的标签或注解,记录负责人、原因和到期时间,并纳入定期复核。长期存在的豁免名单会逐渐演变成事实上的绕过通道,其风险与完全不开启校验相差不大。
复盘:把校验接入更大的供应链控制
部署前镜像签名校验的价值,在于把“镜像来源”从约定变成了准入时的强制检查。但它只是软件供应链控制中的一环:镜像扫描负责已知漏洞,SBOM 与证明负责成分可追溯,RBAC 与仓库策略负责谁能写入,运行时防护负责部署后的异常行为,发布流程负责变更可审计。任何一环缺失,其他环节的强度都会被稀释。
复盘时可以用一份简短清单核对:策略覆盖的命名空间与镜像路径是否与信任声明一致;审计模式下的违规是否已清零或有明确处置;豁免项是否有到期时间;回滚路径是否在演练中真实执行过;准入控制器不可用时的行为是否符合预期。Kubernetes 官方文档中关于验证签名制品的任务说明可作为核对签名验证命令与产物的参考(Verify Signed Kubernetes Artifacts),但具体策略字段与失败模式仍需以所选准入实现的文档为准,不宜把某一种配置当作通用标准。
- it2021
- it2021.com
- Kubernetes
- tag
- test
- Webhook

1F
先审计再阻断这个顺序很稳