Kubernetes 基础体系 · 第 50/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes 供应链安全:镜像签名、SBOM、扫描、准入和来源验证
Kubernetes 供应链安全,关注的不是“Pod 能不能启动”,而是以下问题能否被证明、持续检查并在错误发生时阻断:
- 发布的镜像是否确实来自受信任的构建流程;
- 部署时引用的内容是否与发布时验证过的内容相同;
- 镜像中包含哪些软件、版本和依赖;
- 已知漏洞是否达到组织规定的阻断阈值;
- 工作负载是否只能使用经过验证的镜像;
- 镜像的源代码、构建过程和构建身份是否可追溯;
- 验证服务不可用、签名缺失或扫描结果过期时,系统应该放行还是拒绝。
这几个问题分别对应镜像摘要、镜像签名、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
可以把摘要看作内容身份:
其中:
- 是镜像 manifest;
- 是密码学哈希函数;
- 是镜像摘要。
如果 manifest 或其引用的层发生变化,摘要就会变化。部署时使用摘要,可以避免“同一个标签在不同时间解析到不同内容”的问题。
但摘要只能证明“拉取到的内容对应这个摘要”,不能证明:
- 谁构建了它;
- 它是否来自指定源代码仓库;
- 是否经过安全扫描;
- 是否包含恶意逻辑;
- 这个摘要是否由组织批准。
因此,摘要是内容完整性和可重复部署的基础,不是完整的来源证明。
2. 签名证明的是“某个身份认可了某个摘要”
镜像签名通常签署镜像摘要,而不是签署一个可变标签:
验证过程使用公钥:
这只能在密码学意义上说明:持有私钥的一方签署了摘要 。
安全含义取决于验证策略。例如:
允许 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本身也应该与镜像摘要绑定并签名。可以把一个供应链对象表示为:
其中:
- :镜像摘要;
- :该摘要对应的软件清单;
- :构建来源证明;
- :对这些声明的签名。
如果 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[容器运行时]
关键路径有三个时间点:
- 构建时:源代码产生镜像、SBOM 和来源证明;
- 发布时:扫描结果和组织规则决定是否允许推送或晋级;
- 部署时: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"
成功时会返回签名载荷和验证信息。这里的关键顺序是:
- 先得到不可变摘要;
- 对摘要签名;
- 部署时验证同一个摘要;
- 不依赖之后标签的解析结果。
如果使用:
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质量至少取决于三层:
其中任一项为零,整体价值都会明显下降。
3. SBOM 是清单,不是漏洞结论
漏洞扫描器将 SBOM 或镜像中的组件映射到漏洞数据库:
匹配可能依赖:
- 包名;
- 生态系统;
- 版本;
- 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. 为什么“扫描通过”不是永久事实
扫描结果依赖漏洞数据库版本:
同一个镜像摘要在时间 扫描可能没有高危漏洞,在 新漏洞公开后可能失败。镜像本身没有变化,结论却改变了。
所以生产策略需要决定:
- 是只在构建时扫描;
- 还是在部署时重新扫描;
- 是否按“发现漏洞后的最长允许运行时间”处理;
- 是否允许没有修复版本的漏洞;
- 是否允许带有明确误报记录的漏洞;
- 是否对基础镜像设置重建周期。
扫描器退出码只表达工具规则,不表达完整的业务风险。一个被标记为高危的漏洞可能无法从实际攻击路径到达;一个没有已知 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,应该注意两个对象:
- Deployment 本身;
- 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 的逻辑可以形式化为:
其中:
Digest(i):镜像引用包含摘要;SignatureValid(i):签名与摘要匹配;IdentityAllowed(i):签名身份符合策略;ProvenanceValid(i):来源证明符合策略;ScanPolicySatisfied(i):扫描结果满足门禁。
Webhook 接收 AdmissionReview,请求中包含待写入对象。它通常执行以下步骤:
- 提取
containers、initContainers和必要时的ephemeralContainers; - 将标签解析成摘要,或直接拒绝标签;
- 查询镜像签名、attestation 和 provenance;
- 验证证书链、签名、身份和声明内容;
- 查询扫描结果或扫描平台;
- 返回
allowed: true或allowed: 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
判定过程如下:
第一步:提取所有镜像
不能只检查 containers,还要根据组织范围检查 initContainers 和 ephemeralContainers。初始化容器同样会执行代码并访问网络或挂载卷。
第二步:检查引用形式
app -> 摘要,继续
init -> 摘要,继续
sidecar -> 标签,失败
此时整个 Pod 的条件是:
即使 app 和 init 的其他证据全部有效,Pod 仍应被拒绝。
第三步:检查签名和身份
如果 app 的签名来自受信任 CI,init 的签名来自开发者个人密钥,则:
app -> 通过
init -> 拒绝
“签名存在”不能替代“签名身份符合策略”。
第四步:检查 SBOM 和扫描状态
如果 app 有签名但没有 SBOM attestation,仍然失败。若 init 的 SBOM 存在但扫描结果已经超过组织允许的有效期,也可以失败。
这说明一个典型的逻辑关系:
其中:
- :摘要约束;
- :签名;
- :来源证明;
- :SBOM;
- :漏洞策略。
不能用任意一个条件替代其他条件。
十、常见误解和失败表现
误解一:使用 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. 策略上线前验证三种状态
至少应验证:
- 有效签名和有效来源:应放行;
- 缺失签名或错误身份:应拒绝;
- 验证服务不可用:结果应符合预期的
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 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes Admission Control:内置插件、Webhook、策略、失败和可用性
- 下一篇:Kubernetes 多租户:Namespace、RBAC、NetworkPolicy、Quota 和隔离极限
- 延伸:Kubernetes 运行时安全:非 root、Capabilities、Seccomp、AppArmor 和只读根
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论