把镜像验签推进容器运行时:用 NRI 补上供应链验证的最后一跳

2026-07-30 19 预计阅读时间: 1 分钟
来源: cncf.io AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

Kyverno、OPA Gatekeeper 和 Sigstore Policy Controller 等工具通常在 Kubernetes API 层拦截 Pod 创建,并检查镜像签名与证明材料。这道准入关卡很重要,但它验证的是控制面当时看到的声明。Node Resource Interface(NRI)把策略执行点推进到节点和容器运行时附近,使验证器有机会围绕实际将要启动的容器镜像做决定。

API 准入与运行时验证解决的不是同一个问题

准入 Webhook 擅长集中治理:它能检查 PodSpec、拒绝不合规字段,并在工作负载进入集群前给出清晰反馈。然而,从 API 接受 Pod 到节点真正启动容器,中间还经过调度、镜像解析、拉取和 CRI 调用。

这段路径带来几个需要关注的边界:

  • PodSpec 可能使用可变标签,例如 registry.example.com/payments/api:latest。策略看到的是标签,节点最终运行的是标签当时解析出的摘要。
  • API 层策略未必掌握节点本地缓存、运行时解析结果以及最终镜像身份。
  • 并非所有容器都一定来自常规 Kubernetes Pod 创建路径。具体风险取决于节点权限模型和运行时配置。
  • 准入成功只表示工作负载在那个时间点满足控制面策略,不能自动证明节点启动时仍满足相同条件。

因此,运行时验证不是准入策略的替代品。更合理的结构是两层控制:API 层尽早拒绝明显违规的声明,NRI 插件在容器生命周期的关键位置核对最终镜像,并在验证失败时阻止启动。

NRI 把策略放到更接近容器启动的位置

NRI 为容器运行时与外部插件之间提供接口。插件可以接收容器生命周期相关事件,并依据策略参与运行时决策。用于供应链验证时,一个插件通常需要完成以下流程:

  1. 从运行时事件中取得容器及镜像信息。
  2. 将标签解析为不可变摘要,或者直接读取运行时提供的摘要。
  3. 使用受信公钥、证书身份或 keyless 身份规则验证签名。
  4. 检查 provenance、SBOM 等证明材料是否满足策略。
  5. 缓存成功结果,并在策略、签名或证明材料变化时失效缓存。
  6. 验证失败或超时时,根据 fail-open 或 fail-closed 配置决定是否阻止容器创建。

这里最关键的工程约束是“按摘要验证”。如果插件验证 app:stable,随后运行时又独立解析一次标签,两次解析可能得到不同内容。验证器应把策略判断绑定到 registry/repository@sha256:...,并确保放行的正是该摘要。

可以这样实践:先验证不可变镜像引用

下面是一个可直接改造的最小验证脚本。它不是完整 NRI 插件,而是插件可以调用的验证核心:输入必须是带摘要的镜像引用,然后使用 Cosign 验证签名。运行前安装 cosign,并将 COSIGN_PUBLIC_KEY 指向团队的公钥文件。

#!/usr/bin/env bash
set -euo pipefail

image_ref="${1:?usage: verify-image.sh <registry/repo@sha256:digest>}"
public_key="${COSIGN_PUBLIC_KEY:-./cosign.pub}"

if [[ "${image_ref}" != *@sha256:* ]]; then
  echo "refusing mutable image reference: ${image_ref}" >&2
  exit 2
fi

cosign verify \
  --key "${public_key}" \
  "${image_ref}" >/dev/null

echo "verified: ${image_ref}"

保存为 verify-image.sh 并添加执行权限后,可以这样运行:

chmod +x verify-image.sh
COSIGN_PUBLIC_KEY=/etc/supply-chain/cosign.pub \
  ./verify-image.sh registry.example.com/payments/api@sha256:REPLACE_WITH_DIGEST

若采用 keyless 签名,不应只检查“存在某个有效签名”。还要约束签名者身份和 OIDC 颁发者。可以这样实践:

cosign verify \
  --certificate-identity-regexp '^https://github.com/acme/payments/.github/workflows/release.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  registry.example.com/payments/api@sha256:REPLACE_WITH_DIGEST

NRI 插件的策略配置可以采用下面这种结构。字段是便于说明集成方式的示例,并非 NRI 标准配置格式;实际名称需要与所选插件实现对齐。

apiVersion: supply-chain.example.io/v1alpha1
kind: RuntimeVerificationPolicy
metadata:
  name: production-images
spec:
  mode: enforce
  failurePolicy: FailClosed
  verificationTimeout: 5s
  requireDigest: true
  registries:
    - registry.example.com
  signatures:
    keyless:
      oidcIssuer: https://token.actions.githubusercontent.com
      identityRegexp: >-
        ^https://github.com/acme/.+/.github/workflows/release.yml@refs/tags/.+$
  attestations:
    requiredPredicateTypes:
      - https://slsa.dev/provenance/v1
  cache:
    successTtl: 10m
    maxEntries: 10000

插件收到容器创建事件后,应从事件中提取实际摘要,用该摘要执行验证,并只缓存“摘要 + 策略版本”的结果。不要仅按标签缓存,也不要让一次历史成功永久放行同一仓库的后续内容。

生产部署要处理延迟、故障与可观测性

把验证放进容器启动路径会扩大安全覆盖面,也会引入新的可用性依赖。签名存储、OCI Registry、透明日志或身份服务变慢时,容器启动可能随之变慢甚至失败。

上线前至少要明确以下决策:

  • 失败策略:生产命名空间通常倾向 fail-closed;应急或低风险环境可以经过审批后 fail-open,但必须产生告警。
  • 超时预算:验证超时需要小于平台允许的容器启动延迟,并避免无限重试放大 Registry 故障。
  • 缓存键:至少包含镜像摘要、策略版本和信任根版本。密钥轮换或策略收紧后,旧结果必须失效。
  • 旁路保护:限制节点上的 CRI socket、containerd socket 和 NRI 插件目录权限,否则高权限用户可能绕过或替换验证器。
  • 审计信息:记录工作负载身份、节点、镜像摘要、签名者、证明类型、策略版本和拒绝原因,但不要把敏感令牌写入日志。
  • 插件故障:验证器崩溃、升级或与运行时失联时,行为必须可预测,并通过指标区分策略拒绝与基础设施故障。

采用建议:保留准入层,再增加运行时终检

落地时可以先让 NRI 验证器运行在 audit 模式,比较 PodSpec 中的镜像、运行时解析出的摘要以及现有准入结论。确认误报率、验证延迟和 Registry 压力后,再按命名空间或镜像仓库逐步启用 enforce。

最终检查表可以压缩为五项:所有生产镜像是否使用摘要;签名身份是否被精确约束;证明材料是否验证内容而非只验证存在性;缓存能否随策略和信任根失效;节点权限是否足以防止绕过 NRI。API 准入负责尽早反馈,NRI 负责在启动前核对实际制品,两者叠加才形成更完整的供应链执行链路。


相关推荐