在 GitOps 交付链路里,风险通常来自两个位置:一是镜像本身携带已知漏洞或来源不可信,二是 Kubernetes 清单把不安全的配置带进集群。很多团队在 CI 里跑了 Trivy、KubeSec 和 Cosign,却仍然让问题制品进入生产,原因是 CI 的校验只对流水线负责,不对集群负责。只要有人绕过流水线、直接改镜像 tag,或者 GitOps 控制器同步了一份未经验证的清单,CI 的“通过”就失效了。本文把这三类检查组织成一条部署前的门禁链:CI 负责尽早失败并留下可复核的结果,准入层负责最终强制。适用前提是镜像通过 OCI 仓库分发、清单通过 Git 管理、集群具备 validating admission webhook 能力;不适用于完全离线且无法运行准入控制器的环境。

门禁链的分层:哪些检查放在 CI,哪些必须放到准入层
一个常见的误区是把 Cosign 验证只放在 CI。CI 里 cosign verify 通过,只证明“流水线认为这个镜像合格”;如果集群不强制校验签名和摘要,攻击者或误操作仍然可以把任意镜像部署进去。反过来,把所有检查都压到准入层也不现实:准入 webhook 位于关键路径上,实时拉取漏洞库或做深度静态分析会显著增加 Pod 创建延迟,甚至拖垮 apiserver。
比较稳妥的分工如下:
| 检查项 | CI 阶段 | 准入阶段 | 作用 |
|---|---|---|---|
| 镜像漏洞扫描 | Trivy 按严重级别阻断 | 校验扫描证明是否存在,或只做兜底 | CI 负责快速失败,准入负责防止绕过 |
| 清单配置校验 | KubeSec 静态评分与建议 | Pod Security Admission 或策略兜底 | CI 给出修改建议,准入阻止明显越权配置 |
| 镜像签名 | Cosign 签名并附加证明 | 验证签名身份与摘要 | 准入层是最终强制执行点 |
| 镜像引用 | 写回 digest | 拒绝可变 tag | 保证部署的是被验证过的同一个制品 |
这张表的核心意思是:CI 的输出必须是“可验证的证据”,而不是一句“已通过”。证据要绑定到镜像摘要(digest),而不是绑定到 latest 或某个可变的 tag。否则同一 tag 被重新推送后,之前的扫描和签名结果就与当前内容脱钩了。
准备阶段:先把“可验证”变成前提
固定制品身份,拒绝可变引用
在写任何策略之前,先确认 Git 清单里的镜像引用是否使用 digest。典型形式是 registry.example.com/app/api@sha256:...。如果仍然使用 :1.4.2 这样的 tag,后续的签名验证和扫描证明都缺少稳定的绑定对象。CI 构建完成后应输出 digest,并由清单更新流程写回 Git;Flux 的 image automation 或 Argo CD Image Updater 可以参与这一步,但更新逻辑必须只写 digest,或者至少保证准入层拒绝非 digest 引用。
明确信任根与权限边界
签名方案有两种常见选择:长期私钥和 keyless。长期私钥适合自建 KMS 或 HSM 的场景,但私钥保管、轮换和访问审计必须到位;keyless 依赖 OIDC 身份和短期证书,CI 工作负载的身份直接成为验证依据,能减少长期密钥泄露面,但对 OIDC issuer 和 identity 的匹配规则要求更严格。无论哪种方式,都要明确三件事:谁有权修改签名身份配置、谁有权修改准入策略、谁有权读取或使用签名材料。这三类权限不应集中在同一个角色上。
策略仓库与工具版本
把准入策略本身也放进 Git,用与业务清单相同的评审流程管理。记录 Trivy、Cosign、Kyverno 或 Gatekeeper 的版本,并在升级前用测试集群验证策略行为。策略引擎的字段和默认行为会随版本变化,直接复制网络上的片段而不核对版本文档,是导致“策略看起来生效、实际没拦截”的常见原因。
实施:把三段检查串成一条链
CI 侧:Trivy 与 KubeSec 的快速失败
CI 的任务不是给出完整安全结论,而是用明确的阈值把明显不合格的制品挡在发布之前。镜像扫描可以按 HIGH 和 CRITICAL 级别设置退出码,让流水线在发现高危漏洞时直接失败:
trivy image "$IMAGE"
--severity HIGH,CRITICAL
--exit-code 1
--format table清单校验可以用 KubeSec 对 Deployment、Pod 等 YAML 做静态扫描,关注 privileged、hostPath、以 root 运行、缺少 resource limits 等配置。KubeSec 输出评分和建议,阈值应结合团队基线设定,而不是照搬某个固定分数:
kubesec scan apps/api/deployment.yaml这一步的变更影响是:CI 会变慢,且漏洞库更新后可能出现原本能过的构建突然失败。因此需要把 Trivy 数据库更新时间、扫描耗时和失败原因记录到流水线日志中,避免把“数据库刚更新”误判成“代码刚引入漏洞”。
签名与证明:让结果跟着 digest 走
扫描通过后,对镜像摘要签名,并把 SBOM 或扫描结果作为证明附加到同一个 digest 上。这样准入层验签时,验证的是“这个摘要对应的制品”,而不是某个 tag。使用 Cosign 的典型命令如下:
cosign sign --yes "$IMAGE_DIGEST"
trivy image --format spdx-json --output sbom.spdx.json "$IMAGE_DIGEST"
cosign attest --predicate sbom.spdx.json --type spdxjson --yes "$IMAGE_DIGEST"如果采用 keyless 模式,签名身份由 CI 的 OIDC 身份决定,后续验证需要匹配 identity 和 issuer。把这些参数写进准入策略,而不是只写在 CI 脚本里,才能真正形成约束。
准入侧:用策略强制摘要与签名
准入层要回答两个问题:这个镜像是不是用 digest 引用的?它的签名是否来自受信任的构建身份?以 Kyverno 为例,策略可以先用 Audit 模式上线,观察一段时间后再切到 Enforce:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-supply-chain
spec:
validationFailureAction: Audit
background: false
rules:
- name: verify-image-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "registry.example.com/app/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----这段片段只表达结构,具体字段需要按所用 Kyverno 版本核对。对 digest 的要求可以另用一条 validate 规则实现,或者在签名验证规则中同时检查镜像引用格式。策略上线前要评估 failurePolicy:设为 Fail 更安全,但准入控制器不可用时可能阻断正常发布;设为 Ignore 则会在故障时放行未验证的工作负载。这个取舍必须结合集群的可用性要求明确记录,而不是默认沿用示例。
验证:用受控失败用例证明拦截真的生效
策略写进 Git 不等于拦截生效。验证要在隔离的测试命名空间或测试仓库中进行,准备一组“必须被拒绝”的用例和一组“必须被放行”的用例,逐条执行并记录结果。
| 用例 | 预期结果 | 观察点 |
|---|---|---|
| 未签名镜像 | 拒绝 | admission webhook 返回的拒绝原因 |
| 签名存在但 digest 不匹配 | 拒绝 | 验签失败信息是否指向摘要不匹配 |
| 使用可变 tag 引用 | 拒绝 | 策略是否命中 digest 规则 |
| 含 CRITICAL 漏洞的镜像 | CI 阻断 | Trivy 退出码与报告 |
| privileged 或 hostPath 清单 | CI 低分或准入拒绝 | KubeSec 建议与准入错误 |
| 合规镜像与合规清单 | 放行 | 创建成功且无策略告警 |
执行时不要只看 kubectl apply 的返回,还要看策略引擎的报告和 webhook 日志。拒绝原因必须能让开发者定位问题:是签名身份不匹配、摘要不对,还是清单字段越权。如果拒绝信息只写“policy violated”,排查成本会很高,最终会促使团队用例外绕开策略。

变更与回滚:策略和密钥调整时如何不阻断发布
策略变更先 Audit,再 Enforce
新策略或收紧阈值时,先在 Audit 模式下运行,观察策略报告中的命中对象和拒绝原因是否符合预期。确认误报范围可控后,再通过 Git 提交把 validationFailureAction 改为 Enforce。这样做的代价是多一次发布节奏,但能避免策略一上线就把正常发布全部挡住。回滚同样走 Git revert,恢复上一版策略,而不是直接在集群里手工修改资源。
密钥轮换采用双密钥过渡
签名密钥轮换或身份配置变更时,不要让旧签名立刻失效。比较稳妥的做法是让准入策略在一段时间内同时接受旧公钥和新公钥,等所有在用镜像完成重新签名或自然滚动更新后,再移除旧公钥。keyless 模式下,身份匹配规则的变化也要按同样思路过渡:先放宽到同时匹配新旧 identity,再收紧。轮换窗口内要监控验签失败率,避免把“旧镜像尚未重建”误判为攻击。
故障恢复路径
准入层故障时的恢复动作要提前写清楚:如果 webhook 不可用导致发布受阻,是临时把策略切回 Audit,还是调整 failurePolicy?前者会放行未验证工作负载,后者可能继续阻断。无论选哪种,都应限定时间窗口、记录操作人,并在恢复后补做验证。策略仓库中保留最近若干个可用版本,确保回滚目标明确。
复盘:门禁链的观察点与边界
上线后需要持续观察几个信号:准入拒绝率是否突然升高、webhook 延迟的 P99 是否影响发布、Trivy 数据库最近一次更新时间、验签失败按身份和摘要的分布、以及被拒工作负载的归属团队。拒绝率突增可能是策略过严,也可能是有人绕过了 CI 直接提交清单;两者处理方式完全不同。
最后要明确边界:Trivy 的扫描结果受漏洞库时效和检测规则影响,KubeSec 的静态评分不能覆盖运行时行为,Cosign 只证明签名身份与制品完整性,不证明镜像内容安全。这条门禁链解决的是“进入集群前是否可验证、是否按既定策略被强制”,而不是供应链安全的全部。把单一工具的通过当作完整结论,反而会掩盖真正需要人工审查的风险。
- it2021
- it2021.com
- Kubernetes
- tag
- test
- 漏洞扫描





1F
写回 digest 这一步很关键,不然签名和扫描都白搭