Kubernetes 基础体系 · 第 50/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 供应链安全:镜像签名、SBOM、扫描、准入和来源验证

Kubernetes 供应链安全,关注的不是“Pod 能不能启动”,而是以下问题能否被证明、持续检查并在错误发生时阻断:

  1. 发布的镜像是否确实来自受信任的构建流程;
  2. 部署时引用的内容是否与发布时验证过的内容相同;
  3. 镜像中包含哪些软件、版本和依赖;
  4. 已知漏洞是否达到组织规定的阻断阈值;
  5. 工作负载是否只能使用经过验证的镜像;
  6. 镜像的源代码、构建过程和构建身份是否可追溯;
  7. 验证服务不可用、签名缺失或扫描结果过期时,系统应该放行还是拒绝。

这几个问题分别对应镜像摘要、镜像签名、SBOM、漏洞扫描、Admission 准入和来源证明。它们相互关联,但不能互相替代。


一、先建立供应链中的信任对象

1. 镜像标签不是不可变身份

容器镜像通常通过以下形式引用:

registry.example.com/team/payment:v1.4.2

这里的 v1.4.2 是标签。标签由镜像仓库维护,可以被重新指向另一个镜像。例如:

v1.4.2 -> sha256:aaa...

后来又被覆盖为:

v1.4.2 -> sha256:bbb...

因此,标签表达的是“当前名称”,而不是“不可变内容”。

镜像摘要引用则是:

registry.example.com/team/payment@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

摘要通常是镜像 manifest 的内容摘要。对同一个 OCI manifest 来说:

image_reference = repository + digest

可以把摘要看作内容身份:

I=H(M)I = H(M)

其中:

  • MM 是镜像 manifest;
  • HH 是密码学哈希函数;
  • II 是镜像摘要。

如果 manifest 或其引用的层发生变化,摘要就会变化。部署时使用摘要,可以避免“同一个标签在不同时间解析到不同内容”的问题。

但摘要只能证明“拉取到的内容对应这个摘要”,不能证明:

  • 谁构建了它;
  • 它是否来自指定源代码仓库;
  • 是否经过安全扫描;
  • 是否包含恶意逻辑;
  • 这个摘要是否由组织批准。

因此,摘要是内容完整性和可重复部署的基础,不是完整的来源证明。

2. 签名证明的是“某个身份认可了某个摘要”

镜像签名通常签署镜像摘要,而不是签署一个可变标签:

σ=Signsk(I)\sigma = Sign_{sk}(I)

验证过程使用公钥:

Verifypk(σ,I)=trueVerify_{pk}(\sigma, I) = true

这只能在密码学意义上说明:持有私钥的一方签署了摘要 II

安全含义取决于验证策略。例如:

允许 registry.example.com/team/payment@sha256:...,
前提是:
  签名有效;
  签名身份是 ci@example.com;
  签名证书由指定 OIDC 身份提供商签发;
  签名时间和仓库范围符合策略。

签名不能自动证明镜像安全。一个被攻破的构建系统也可能合法地签署恶意镜像。签名解决的是“发布者身份和内容绑定”问题,不能单独解决“发布者是否可信”和“软件是否无漏洞”问题。

3. SBOM 描述软件组成,不是签名本身

SBOM,即 Software Bill of Materials,软件物料清单,描述一个构建产物中包含哪些组件。

典型条目包括:

组件名称:openssl
版本:3.0.13
包类型:apk / deb / rpm / npm / golang
来源或 PURL:pkg:apk/alpine/openssl@3.0.13-r0
许可证:Apache-2.0

常见格式有:

  • SPDX;
  • CycloneDX。

SBOM回答:

这个镜像里有哪些软件?

它不天然回答:

  • 这些软件是否安全;
  • SBOM 是否完整;
  • SBOM 是否由可信构建系统生成;
  • SBOM 是否对应当前镜像摘要。

因此,SBOM本身也应该与镜像摘要绑定并签名。可以把一个供应链对象表示为:

Artifact=(I,SBOM,Provenance,Signature)Artifact = (I, SBOM, Provenance, Signature)

其中:

  • II:镜像摘要;
  • SBOMSBOM:该摘要对应的软件清单;
  • ProvenanceProvenance:构建来源证明;
  • SignatureSignature:对这些声明的签名。

如果 SBOM 只按文件名保存,例如 payment-sbom.json,却没有绑定摘要,就可能出现“SBOM 属于旧镜像,文件名却被复用”的错配。


二、一个完整供应链流程中的对象和数据流

典型流程如下:

flowchart LR
    S[源代码仓库] --> B[受控构建系统]
    B --> I[OCI 镜像]
    B --> P[构建来源证明]
    I --> D[镜像摘要]
    D --> SG[镜像签名]
    I --> SB[生成 SBOM]
    SB --> AT[SBOM Attestation]
    I --> SC[漏洞扫描]
    D --> R[镜像仓库]
    SG --> R
    AT --> R
    P --> R
    SC --> G[发布门禁]

    R --> K[Kubernetes API Server]
    K --> A[Admission]
    A -->|验证摘要、签名、来源和策略| P2[Pod 对象]
    P2 --> Q[Kubelet]
    Q --> R
    Q --> C[容器运行时]

关键路径有三个时间点:

  1. 构建时:源代码产生镜像、SBOM 和来源证明;
  2. 发布时:扫描结果和组织规则决定是否允许推送或晋级;
  3. 部署时:Kubernetes Admission 决定对象是否可以写入 API Server。

它们的判断对象不同:

阶段 主要问题 典型证据
构建 从什么源代码、什么构建器构建 provenance、构建日志
发布 镜像包含什么、有什么漏洞 SBOM、扫描报告
部署 当前 Pod 使用的内容是否获准 digest、签名、准入策略
运行 容器是否具备过高权限 non-root、Capabilities、Seccomp、AppArmor

发布阶段扫描通过,并不意味着部署阶段一定安全,因为漏洞数据库可能变化,镜像引用也可能被修改。反过来,部署阶段签名验证通过,也不意味着镜像没有高危漏洞。


三、镜像签名:签什么、由谁签、在哪里验证

1. 签名的最小正确流程

以 Cosign 为例,下面演示基于密钥的签名。生产环境也可以使用基于 OIDC 的无密钥签名,但身份绑定和密钥生命周期管理要求不同。

# 生成测试密钥对
cosign generate-key-pair

该命令通常生成:

cosign.key
cosign.pub

cosign.key 是私钥,不能提交到代码仓库,也不能放入普通 CI 日志。然后先构建并推送镜像:

docker build -t registry.example.com/team/payment:v1.4.2 .
docker push registry.example.com/team/payment:v1.4.2

解析该标签对应的摘要:

DIGEST="$(
  crane digest registry.example.com/team/payment:v1.4.2
)"

IMAGE="registry.example.com/team/payment@${DIGEST}"
echo "$IMAGE"

预期输出类似:

registry.example.com/team/payment@sha256:...

接着签署摘要:

COSIGN_PASSWORD='change-me' \
  cosign sign --key cosign.key "$IMAGE"

验证:

cosign verify --key cosign.pub "$IMAGE"

成功时会返回签名载荷和验证信息。这里的关键顺序是:

  1. 先得到不可变摘要;
  2. 对摘要签名;
  3. 部署时验证同一个摘要;
  4. 不依赖之后标签的解析结果。

如果使用:

cosign sign --key cosign.key \
  registry.example.com/team/payment:v1.4.2

工具通常会先解析标签再签署对应摘要。即使这一步可以工作,部署策略仍应要求最终使用摘要,因为标签本身可能随后变化。

2. 签名存储在哪里

OCI 签名通常作为与镜像关联的额外制品存储在镜像仓库中。验证工具根据镜像摘要查找签名,而不是把签名写进镜像层。

因此需要区分:

  • 镜像 manifest;
  • 镜像层;
  • 签名制品;
  • SBOM attestation;
  • provenance attestation。

镜像仓库、代理缓存和 OCI Registry 的兼容性会影响这些关联制品是否能被正常保存和读取。生产部署前应验证目标仓库是否支持所使用工具的签名和 referrer 存储方式,不能只在本地仓库测试通过后直接假设云厂商仓库行为完全一致。

3. 密钥签名和无密钥签名

密钥签名

密钥签名的信任根是公钥:

验证策略:
  公钥必须等于组织保存的 cosign.pub

优点是关系直接,验证依赖少。缺点是:

  • 私钥需要安全保管;
  • 密钥轮换需要更新准入策略;
  • 私钥泄露后必须撤销或停止信任旧密钥;
  • 多个构建环境之间需要明确密钥使用边界。

无密钥签名

无密钥签名并不意味着没有密码学密钥。签名工具通常临时生成密钥对,再使用 OIDC 身份获取短期证书,验证方检查:

  • 证书链是否由指定 CA 或 Fulcio 等服务签发;
  • 证书中的身份是否符合要求;
  • 签名是否与镜像摘要匹配;
  • 可选地检查透明日志中的记录。

验证策略应从“任意有效签名”收紧为“指定身份对指定仓库范围的有效签名”。例如,以下两个条件不是等价的:

存在一个有效签名

和:

存在一个有效签名,且:
  issuer == https://token.actions.githubusercontent.com
  subject == repo:example/payment:ref:refs/heads/main
  repository == registry.example.com/team/payment

前者可能允许任意获得组织签发身份的工作流发布镜像,后者才把身份、来源和目标仓库绑定起来。


四、SBOM:生成、绑定、消费和局限

1. 从镜像生成 SBOM

使用 Syft 可以从本地镜像生成 CycloneDX JSON:

syft registry.example.com/team/payment@sha256:... \
  -o cyclonedx-json > sbom.cdx.json

也可以生成 SPDX:

syft registry.example.com/team/payment@sha256:... \
  -o spdx-json > sbom.spdx.json

这里的输入必须是已经固定的摘要,而不是可变标签。否则在生成 SBOM 和部署镜像之间,标签可能解析到不同内容。

检查结果:

jq '.components | length' sbom.cdx.json

输出是组件数量。这个数字不能直接解释为“安全程度”:扫描器可能识别出更多或更少的组件,静态链接、distroless 镜像、语言包管理器和二进制文件都会影响识别结果。

2. 把 SBOM 作为 attestation 绑定到镜像

cosign attest \
  --key cosign.key \
  --type cyclonedx \
  --predicate sbom.cdx.json \
  "$IMAGE"

随后可以查看或验证 attestation:

cosign verify-attestation \
  --key cosign.pub \
  --type cyclonedx \
  "$IMAGE"

这里验证的是签名声明的真实性和对象绑定关系,不是验证 SBOM 内容绝对完整。SBOM 生成器可能遗漏:

  • 没有包管理器元数据的静态二进制;
  • 被复制进镜像但未被识别的库;
  • 构建阶段依赖;
  • 动态下载的运行时内容;
  • 应用内部插件或脚本。

因此,SBOM质量至少取决于三层:

SBOM可信度=生成器识别能力×构建流程完整性×对象绑定正确性SBOM可信度 = 生成器识别能力 \times 构建流程完整性 \times 对象绑定正确性

其中任一项为零,整体价值都会明显下降。

3. SBOM 是清单,不是漏洞结论

漏洞扫描器将 SBOM 或镜像中的组件映射到漏洞数据库:

Finding=Match(Component,VulnerabilityDatabase)Finding = Match(Component, VulnerabilityDatabase)

匹配可能依赖:

  • 包名;
  • 生态系统;
  • 版本;
  • CPE、PURL;
  • 发行版修复回补信息。

例如,Linux 发行版可能把安全补丁回补到旧版本号中。简单比较上游版本号可能产生误报。因此扫描器是否理解 Alpine、Debian、Ubuntu、Red Hat 等发行版的修复状态,直接影响结果。


五、漏洞扫描:门禁、时效性和误判

以 Trivy 为例:

trivy image \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  --exit-code 1 \
  "$IMAGE"

参数含义:

  • --severity HIGH,CRITICAL:只把高危和严重漏洞作为输出重点;
  • --ignore-unfixed:忽略暂时没有修复版本的漏洞;
  • --exit-code 1:发现符合条件的问题时返回非零退出码;
  • "$IMAGE":扫描固定摘要,而不是标签。

在 CI 中,非零退出码可以阻断发布:

set -euo pipefail

trivy image \
  --severity HIGH,CRITICAL \
  --exit-code 1 \
  "$IMAGE"

cosign sign --key cosign.key "$IMAGE"

但扫描顺序和策略应明确。常见做法是先扫描,再签名:

构建 -> 固定摘要 -> 扫描 -> 满足门禁 -> 签名 -> 发布

否则可能出现“镜像先签名,后来发现高危漏洞”的流程语义混乱。签名仍然有效,但组织可能不再愿意信任它。

1. 为什么“扫描通过”不是永久事实

扫描结果依赖漏洞数据库版本:

Result(t)=Scan(Image,DB(t))Result(t) = Scan(Image, DB(t))

同一个镜像摘要在时间 t1t_1 扫描可能没有高危漏洞,在 t2t_2 新漏洞公开后可能失败。镜像本身没有变化,结论却改变了。

所以生产策略需要决定:

  • 是只在构建时扫描;
  • 还是在部署时重新扫描;
  • 是否按“发现漏洞后的最长允许运行时间”处理;
  • 是否允许没有修复版本的漏洞;
  • 是否允许带有明确误报记录的漏洞;
  • 是否对基础镜像设置重建周期。

扫描器退出码只表达工具规则,不表达完整的业务风险。一个被标记为高危的漏洞可能无法从实际攻击路径到达;一个没有已知 CVE 的应用也可能包含逻辑漏洞、恶意代码或泄露凭据的行为。

2. “扫描镜像”与“扫描运行时”不同

镜像扫描检查静态内容,无法完整观察:

  • 容器运行后的下载行为;
  • 通过挂载卷加载的代码;
  • 运行时生成的文件;
  • 外部服务交互;
  • 应用逻辑和授权错误。

因此,镜像扫描应与运行时安全策略结合,例如:

apiVersion: v1
kind: Pod
metadata:
  name: payment
spec:
  containers:
  - name: app
    image: registry.example.com/team/payment@sha256:...
    securityContext:
      runAsNonRoot: true
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
      seccompProfile:
        type: RuntimeDefault

这些字段降低容器运行时的权限,但不能证明镜像来源;镜像签名也不能替代这些运行时限制。


六、Kubernetes 准入:验证发生在哪里

1. API Server 的对象生命周期

Kubernetes Admission 发生在 API Server 鉴权之后、对象持久化之前。一个简化流程是:

客户端请求
  -> Authentication:请求者是谁
  -> Authorization:是否允许执行该动作
  -> Mutating Admission:是否修改对象
  -> Validating Admission:是否接受对象
  -> 持久化到 etcd
  -> 控制器创建或更新下游对象
  -> Kubelet 读取 Pod
  -> 拉取镜像并启动容器

Authentication 和 Authorization 解决的是“谁能请求什么 API”;Admission 解决的是“即使请求者有权限,这个对象是否符合集群规则”。

准入拒绝发生在持久化之前:

HTTP 4xx
对象不会写入 API Server
控制器也不会获得该对象

如果是 Deployment,应该注意两个对象:

  1. Deployment 本身;
  2. Deployment 控制器后来创建的 Pod。

只检查 Pod,可能导致 Deployment 创建成功但 Pod 创建持续失败。只检查 Deployment,又可能漏掉直接创建的 Pod。因此生产策略要明确是检查 workload 模板、最终 Pod,还是两者都检查。

2. 内置准入、ValidatingAdmissionPolicy 和 Webhook

Kubernetes 的准入能力包括:

  • 内置 admission plugin;
  • ValidatingAdmissionPolicy
  • MutatingAdmissionWebhook
  • ValidatingAdmissionWebhook
  • 外部策略系统,如 Kyverno、OPA Gatekeeper。

ValidatingAdmissionPolicy 使用 CEL 表达式进行验证,适合检查 Kubernetes 对象中已有的字段。例如可以要求普通容器和初始化容器使用摘要:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-image-digest
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  validations:
  - expression: >-
      object.spec.containers.all(c,
        c.image.matches('^[^@]+@sha256:[a-f0-9]{64}$'))
      &&
      (!has(object.spec.initContainers) ||
        object.spec.initContainers.all(c,
          c.image.matches('^[^@]+@sha256:[a-f0-9]{64}$')))
    message: "all application and init container images must use a digest"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: require-image-digest
spec:
  policyName: require-image-digest
  validationActions:
  - Deny

应用:

kubectl apply -f require-image-digest.yaml

测试一个标签引用:

apiVersion: v1
kind: Pod
metadata:
  name: tag-test
spec:
  containers:
  - name: app
    image: nginx:1.27

预期创建失败,错误信息应包含:

all application and init container images must use a digest

使用摘要后才可能通过:

image: nginx@sha256:<实际摘要>

这个策略有几个边界:

  • 它只检查字符串格式,不解析仓库中的摘要是否真实存在;
  • 它不验证签名;
  • 它不读取 SBOM;
  • 它没有判断镜像是否来自受信任构建系统;
  • 示例没有覆盖 ephemeralContainers
  • 如果只匹配 pods,还需要额外考虑 Deployment、Job、CronJob 等模板。

CEL 适合表达字段级、结构化约束,但镜像签名验证需要读取外部 OCI 制品、验证密码学签名并检查身份,通常应交给 Validating Webhook 或集成了该能力的策略引擎。

3. 通过 Webhook 验证签名和来源

签名验证 Webhook 的逻辑可以形式化为:

Allow(Pod)=iImages(Pod)[Digest(i)SignatureValid(i)IdentityAllowed(i)ProvenanceValid(i)ScanPolicySatisfied(i)]Allow(Pod) = \bigwedge_{i \in Images(Pod)} \left[ Digest(i) \land SignatureValid(i) \land IdentityAllowed(i) \land ProvenanceValid(i) \land ScanPolicySatisfied(i) \right]

其中:

  • Digest(i):镜像引用包含摘要;
  • SignatureValid(i):签名与摘要匹配;
  • IdentityAllowed(i):签名身份符合策略;
  • ProvenanceValid(i):来源证明符合策略;
  • ScanPolicySatisfied(i):扫描结果满足门禁。

Webhook 接收 AdmissionReview,请求中包含待写入对象。它通常执行以下步骤:

  1. 提取 containersinitContainers 和必要时的 ephemeralContainers
  2. 将标签解析成摘要,或直接拒绝标签;
  3. 查询镜像签名、attestation 和 provenance;
  4. 验证证书链、签名、身份和声明内容;
  5. 查询扫描结果或扫描平台;
  6. 返回 allowed: trueallowed: false

验证失败时,拒绝原因必须具体,例如:

image registry.example.com/team/payment@sha256:...
has no valid signature from subject repo:example/payment:ref:refs/heads/main

“验证失败”与“验证服务不可用”需要分别处理。前者是安全结论,通常拒绝;后者是可用性故障,涉及 failurePolicy


七、失败策略:Fail-closed 不是无条件更安全

Webhook 通常有:

failurePolicy: Fail

或者:

failurePolicy: Ignore

含义不同:

  • Fail:Webhook 超时、TLS 错误或不可达时,相关请求失败;
  • Ignore:Webhook 本身失败时跳过该检查。

对安全验证来说:

Fail-closed:
  无法证明安全 -> 拒绝

对集群可用性来说:

Fail-closed:
  验证服务故障 -> 可能无法创建、更新或扩缩容 Pod

因此这是一个安全性与可用性的约束选择,不是简单的“Fail 永远正确”。

需要同时考虑以下故障路径:

1. Webhook 服务不可达

可能原因:

  • Webhook 自身的 Deployment 没有副本;
  • 所有节点同时重启;
  • Service selector 错误;
  • 证书过期;
  • NetworkPolicy 阻断 API Server 到 Webhook 的路径;
  • Webhook 依赖的镜像仓库或透明日志服务不可达。

如果使用 failurePolicy: Fail,新建 Pod 会被拒绝;如果使用 Ignore,可能在没有完成签名验证时放行。

2. 外部镜像仓库不可达

准入服务可能无法查询签名,但 Kubelet 后续也可能无法拉取镜像。这时应区分:

  • 准入阶段验证不了;
  • 运行阶段拉不到镜像。

前者由 Webhook 策略决定,后者通常表现为 Pod 进入:

ErrImagePull
ImagePullBackOff

例如查看原因:

kubectl describe pod payment-7d8f9c7f5d-abcde

常见输出包括:

Failed to pull image
unauthorized: authentication required
manifest unknown
no matching manifest for linux/amd64

签名正确不代表仓库凭据正确,仓库凭据正确也不代表签名正确。

3. 策略更新造成现有工作负载问题

Admission 通常只拦截新的创建或更新,不会自动重新验证已经存在的 Pod。若组织从“允许标签”切换到“必须摘要”,旧 Pod 可能继续运行,新建 Pod 或滚动更新时才失败。

因此策略变更需要明确:

  • 是否只影响新对象;
  • 是否重新扫描存量对象;
  • 是否触发滚动重建;
  • 回滚策略本身是否仍能通过新的准入规则。

可以先用审计或告警模式观察,再切换为拒绝。具体的审计模式由所用策略引擎决定,不能把某个第三方引擎的参数当作 Kubernetes 通用 API。


八、来源验证:签名之外的构建证明

1. 来源证明要描述什么

来源证明,即 provenance,描述构建产物是如何产生的。常见字段包括:

  • 源代码仓库;
  • 提交哈希;
  • 分支或标签;
  • 构建工作流;
  • 构建器身份;
  • 构建时间;
  • 构建参数;
  • 输入材料;
  • 输出镜像摘要。

它要回答的是:

这个镜像摘要是否由允许的构建系统,从允许的源代码和输入产生?

例如策略可以要求:

image digest == I
且 provenance 满足:
  sourceRepository == github.com/example/payment
  commit == 已审核提交
  builder == ci.example.com/release
  workflow == release.yaml

这比单独验证“CI 私钥签了镜像”更强,因为它还约束签名对象的生产过程。

2. 来源证明不能证明源代码本身无恶意

如果攻击者将恶意代码提交到受信任仓库,并且该提交经过合法流程构建,那么来源证明仍可能有效。它证明的是过程和输入,不是输入的业务正确性。

因此信任链通常是:

源代码评审和分支保护
  -> 受控构建器
  -> 构建来源证明
  -> 镜像摘要
  -> 签名
  -> SBOM 和扫描
  -> Kubernetes 准入

每一环只提供局部保证。安全策略如果只验证最后一环,不能声称覆盖整个链条。


九、准入策略的完整判定算例

假设组织策略如下:

允许镜像必须满足:
A. 引用为摘要;
B. 摘要存在;
C. 签名由 release-ci 身份产生;
D. provenance 的源仓库为 example/payment;
E. SBOM attestation 存在;
F. HIGH/CRITICAL 漏洞数量为 0,或有批准的例外。

对一个 Pod 中的三个镜像:

app:
  registry.example.com/team/payment@sha256:111...

init:
  registry.example.com/team/migrate@sha256:222...

sidecar:
  registry.example.com/thirdparty/log-agent:3.2

判定过程如下:

第一步:提取所有镜像

Images(Pod)={app,init,sidecar}Images(Pod) = \{app, init, sidecar\}

不能只检查 containers,还要根据组织范围检查 initContainersephemeralContainers。初始化容器同样会执行代码并访问网络或挂载卷。

第二步:检查引用形式

app     -> 摘要,继续
init    -> 摘要,继续
sidecar -> 标签,失败

此时整个 Pod 的条件是:

A(Pod)=A(app)A(init)A(sidecar)=falseA(Pod) = A(app) \land A(init) \land A(sidecar) = false

即使 appinit 的其他证据全部有效,Pod 仍应被拒绝。

第三步:检查签名和身份

如果 app 的签名来自受信任 CI,init 的签名来自开发者个人密钥,则:

app  -> 通过
init -> 拒绝

“签名存在”不能替代“签名身份符合策略”。

第四步:检查 SBOM 和扫描状态

如果 app 有签名但没有 SBOM attestation,仍然失败。若 init 的 SBOM 存在但扫描结果已经超过组织允许的有效期,也可以失败。

这说明一个典型的逻辑关系:

Allow=ASPBVAllow = A \land S \land P \land B \land V

其中:

  • AA:摘要约束;
  • SS:签名;
  • PP:来源证明;
  • BB:SBOM;
  • VV:漏洞策略。

不能用任意一个条件替代其他条件。


十、常见误解和失败表现

误解一:使用 imagePullPolicy: Always 就能防止标签被篡改

Always 只要求 Kubelet 每次启动时重新解析标签。它可能让系统更快获得标签的新内容,恰好增加了不可重复部署的可能性。

它不能证明新解析出的镜像经过签名或扫描。真正控制内容身份的是摘要,控制发布身份的是签名和来源策略。

误解二:镜像摘要已经等于镜像签名

摘要可以由任何人计算:

sha256sum some-file

摘要本身没有发布者身份。签名才把摘要和私钥持有者绑定起来,但仍需要验证方决定信任哪个身份。

误解三:漏洞扫描为零表示镜像安全

“零漏洞”通常只表示:

在某个时间点、使用某个数据库、按某个识别器和阈值,没有匹配到结果

它不表示没有未知漏洞、恶意逻辑、配置错误或运行时风险。

误解四:签名验证默认由 Kubernetes 或 containerd 完成

标准 Kubernetes 工作流中,Kubelet 和容器运行时通常负责:

  • 根据镜像引用拉取镜像;
  • 使用仓库认证信息;
  • 校验传输和内容摘要;
  • 交给运行时启动。

它们不会因为镜像“没有 Cosign 签名”就自动拒绝。签名策略必须通过 Admission、镜像策略组件、运行时集成或其他组织控制面实现。

误解五:只检查 Deployment 就足够

用户可以直接创建 Pod,也可能存在 Job、CronJob、StatefulSet、DaemonSet 或控制器生成的其他 Pod。策略必须覆盖实际入口,并测试变更和子资源行为。

误解六:只验证签名不验证仓库范围

如果策略只写成“签名有效”,攻击者可能将自己签名的镜像放到受信任仓库,或者使用不应被该工作负载信任的身份签名。策略应同时限制:

  • registry 和 repository;
  • 签名公钥或证书身份;
  • OIDC issuer;
  • 源代码仓库;
  • 构建器;
  • 允许的工作流。

十一、诊断顺序:先定位哪个环节失败

1. Pod 创建直接失败

查看客户端错误:

kubectl apply -f deployment.yaml

如果返回:

Error from server (Forbidden): admission webhook ... denied the request

这是准入拒绝,不是镜像拉取失败。应检查:

kubectl get validatingwebhookconfigurations
kubectl get validatingadmissionpolicies
kubectl describe validatingwebhookconfiguration <name>

再查看策略引擎日志:

kubectl logs -n <policy-namespace> deploy/<policy-deployment>

具体资源名取决于部署方式。不要把 API Server 的拒绝和业务容器日志混在一起排查,因为被拒绝的对象根本没有进入 Pod 生命周期。

2. Pod 已创建但无法启动

查看:

kubectl get pod payment -o wide
kubectl describe pod payment

如果状态为:

ImagePullBackOff
ErrImagePull

重点检查:

  • 镜像仓库地址;
  • digest 是否存在;
  • imagePullSecrets
  • 节点架构;
  • 私有仓库网络访问;
  • 仓库是否保留了签名关联制品。

此时 Admission 可能已经成功,但运行时拉取失败。

3. 签名验证失败

在能够访问仓库的环境中直接验证:

cosign verify --key cosign.pub "$IMAGE"

如果失败,区分:

no matching signatures

和:

signature verification failed

前者通常表示签名不存在或查找不到,后者可能表示签名内容、摘要、公钥或载荷不匹配。若使用无密钥签名,还要检查证书 issuer 和 identity,而不是只看“有无签名”。

4. SBOM 与镜像不匹配

应重新从摘要生成并比较:

syft "$IMAGE" -o cyclonedx-json > sbom-new.json

同时验证 attestation 是否确实绑定到同一摘要。最常见的根因是:

  • 先生成 SBOM,后重新构建镜像;
  • 标签被覆盖;
  • SBOM 文件被人工复制到其他发布目录;
  • attestation 上传到了另一个仓库或错误摘要。

十二、生产取舍:阻断、例外和恢复

1. 例外必须是可审计对象

现实中可能暂时存在:

  • 上游没有修复的漏洞;
  • 第三方镜像无法提供组织要求的 provenance;
  • 紧急回滚镜像尚未完成新流程;
  • 特定命名空间运行遗留系统。

例外不应通过永久关闭 Webhook 实现,而应记录:

对象或镜像摘要
例外原因
批准人
开始时间
过期时间
补救计划

例外最好绑定到摘要,而不是标签。绑定标签会使例外随着标签移动到另一个镜像,失去明确边界。

2. 策略上线前验证三种状态

至少应验证:

  1. 有效签名和有效来源:应放行;
  2. 缺失签名或错误身份:应拒绝;
  3. 验证服务不可用:结果应符合预期的 failurePolicy

还要测试:

  • Deployment 滚动更新;
  • 节点重启后的重新拉取;
  • 控制器自动扩容;
  • 私有仓库凭据轮换;
  • 策略 Webhook 自身升级;
  • API Server 无法访问外部验证服务时的超时行为。

3. 回滚路径必须提前设计

如果所有新 Pod 都要求新签名策略,而策略服务本身故障,回滚 Deployment 也可能被拒绝。生产上至少应准备:

  • 多副本和反亲和的 Webhook;
  • Webhook 依赖的证书和密钥轮换方案;
  • 仓库、透明日志或 attestation 存储的可用性方案;
  • 明确的紧急变更权限;
  • 受审计的临时例外,而不是直接删除所有准入配置。

删除准入配置可能快速恢复写入,但同时会让未验证镜像进入集群。恢复动作必须知道自己关闭了哪一个安全保证。


十三、Kubernetes 版本和实现边界

本文使用的 Kubernetes API 示例是:

  • admissionregistration.k8s.io/v1
  • ValidatingAdmissionPolicy
  • ValidatingAdmissionPolicyBinding
  • ValidatingWebhookConfiguration 等稳定 API。

ValidatingAdmissionPolicy 适用于使用 CEL 检查 Kubernetes 对象字段,但其能力边界不等于外部签名验证器。不同 Kubernetes 版本对 CEL 可用变量、字段访问和策略特性可能存在差异,应以目标集群版本的官方文档和 OpenAPI 定义验证。

Webhook、Kyverno、Gatekeeper、Cosign、Syft、Trivy 不属于同一个 Kubernetes 内置组件集合:

  • Kubernetes 提供准入扩展点;
  • 策略引擎实现具体匹配和验证逻辑;
  • Cosign 等工具负责签名与 attestation;
  • SBOM 工具负责清单生成;
  • 扫描器负责漏洞匹配;
  • 镜像仓库负责保存镜像和关联制品。

云厂商托管 Kubernetes 可能限制 API Server 到用户命名空间 Webhook 的网络路径,也可能对镜像仓库、身份提供商、透明日志或节点运行时做定制。生产部署需要单独验证这些外部依赖。


供应链安全的核心不是部署一个扫描器或打开一个 Webhook,而是把不同证据绑定到同一个不可变镜像摘要,并在正确的生命周期阶段验证:

摘要确认内容
签名确认发布身份
SBOM描述组成
扫描评估已知漏洞
provenance描述构建来源
Admission决定能否进入集群
运行时策略限制启动后的权限

当这些判断都明确绑定到镜像摘要、签名身份、来源声明和失败策略时,Kubernetes 才能从“接受一个镜像字符串”提升为“只接受经过组织验证的构建产物”。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。