容器运行时一旦拉取不可信镜像,后续网络策略、RBAC、运行时防护都只能做补救。镜像准入控制是把安全校验前置到 Kubernetes API 请求落库之前的关键手段。它的目标并不是替代 CI 扫描,而是在生产边界上设卡。设置这个卡点后,即使开发者绕过 CI 流程直接提交 Pod,也会被 API Server 拒绝。本文展开的策略包括镜像仓库白名单、禁止 latest、强制签名、漏洞级别阻断等,并给出对应的 Webhook 与 OPA 配置。

一、准入控制链路的触发时机与失败策略
在 Kubernetes 中创建 Pod 时,请求会先经过认证、鉴权,然后进入准入控制器。镜像策略通常放在 ValidatingWebhook 或自定义准入插件中,因为此时不需要修改对象,只需要给出一致性判断。Webhook 接收 AdmissionReview 请求,返回 allowed true 或 false。如果返回 false,API Server 会直接拒绝该 Pod 创建请求。
失败策略需要单独设计。failurePolicy 设置为 Fail 时,只要 webhook 服务不可用,所有 Pod 创建都会被阻断。这虽然严格,但可能让整个集群不可用。因此生产环境常把失败策略设为 Fail,同时用多副本和 PDB 保证 webhook 可用;如果策略属于审计性质,可以先设 Ignore 观察一段时间。另一个容易忽略的是 timeoutSeconds,它决定了 webhook 响应超时时间。镜像扫描如果耗时长,不能直接在 webhook 内同步扫描,否则请求容易超时。
真正推荐的方式是将镜像扫描结果预先生成到镜像元数据或外部缓存中,webhook 只查缓存或签名验证结果。签名验证使用短时网络请求即可完成,适合放在同步 webhook 中。漏洞扫描则需要异步执行,在 CI 或仓库侧完成。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: image-policy-validation
webhooks:
- name: check-images.ipipp.com
clientConfig:
service:
name: image-policy-webhook
namespace: security
path: /validate-image
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Fail
sideEffects: None
admissionReviewVersions: ["v1"]
timeoutSeconds: 5
二、镜像仓库白名单与标签策略
不少生产事故的起点并不是镜像里有恶意代码,而是开发者不小心用了公共仓库中的同名镜像,或者把测试环境里 latest 标签的镜像带到了生产。latest 是一个可移动标签,今天指向的版本和明天可能完全不同。即使镜像内容本身没有问题,这种不确定性也会破坏可重复部署。因此第一条准入策略通常是禁止 latest 标签,同时只允许来自指定仓库前缀的镜像。
仓库白名单的核心是解析镜像字段,而不是做字符串包含判断。比如 image 字段可能写成 registry.ipipp.com/team/app:1.2.3,也可能带 digest,还可能来自默认 docker.io。如果只检查是否包含某个域名,攻击者可以使用 registry.ipipp.com.evil.com 绕过。严格的做法是解析完整镜像引用,确认 host 部分精确匹配白名单。
下面这段 Rego 策略通过 startswith 判断仓库前缀,并显式拒绝 latest。它放在 Gatekeeper 的 ConstraintTemplate 中,可以约束所有 Pod 和 Deployment 等上层资源,因为上层资源最终都会生成 Pod。
package k8sallowedimages
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
image := container.image
not startswith(image, "registry.ipipp.com/")
msg := sprintf("镜像 %s 不在仓库白名单中", [image])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
image := container.image
endswith(image, ":latest")
msg := sprintf("禁止使用 latest 标签: %s", [image])
}
三、强制镜像签名与内容信任
仓库白名单只能保证镜像来自某个受控位置,不能保证这个位置上的镜像没有被篡改。仓库账号泄露、CI 流水线被入侵、镜像推送过程中的中间人攻击都可能导致镜像内容变化。镜像签名解决的是内容完整性和来源认证问题。Sigstore 提供的 cosign 工具可以对镜像摘要进行签名,验证时只要镜像摘要与签名匹配,就说明内容没有被改动。
在准入阶段验证签名,需要 webhook 解析 Pod 中的镜像引用,调用 cosign verify 或通过签名存储服务检查。如果镜像没有签名,或者签名密钥不匹配,直接拒绝。这样即使攻击者拿到了仓库写权限,只要没有签名私钥,仍无法把恶意镜像带进集群。签名策略还需要考虑镜像 tag 与 digest 的区别,建议强制使用 digest 而不是 tag,因为 tag 可移动,签名也容易错位。
以下命令展示在 CI 或 webhook 中执行签名验证的方式。生产环境中通常会把公钥挂载到 webhook 容器内,验证过程不依赖外部服务。
cosign verify \ --key /keys/cosign.pub \ registry.ipipp.com/team/app@sha256:9f6c... if [ $? -ne 0 ]; then echo "镜像签名验证失败" exit 1 fi
四、漏洞扫描结果的准入判断
签名再严格,也不能保证镜像里的依赖没有已知漏洞。基础镜像中的 OpenSSL、glibc、Java 运行时都可能存在高危 CVE。漏洞扫描通常由 Trivy、Grype 或云厂商扫描服务完成,输出 JSON 报告。准入控制需要消费这些报告,而不是在请求链路上重新扫描。常见架构是 CI 推送镜像后执行扫描,把结果写入数据库或 OCI 注解,Webhook 查询结果并判断是否超过阈值。
漏洞等级是策略核心。一般将 CRITICAL 和 HIGH 定义为阻断级,MEDIUM 可以告警,LOW 忽略。这个阈值不能一刀切,例如基础镜像中的某个低危漏洞无法修复且不影响业务,仍然可以放行;但互联网暴露服务如果包含 CRITICAL RCE 漏洞,必须阻断。策略中不仅要看等级,还要看 CVSS 评分、可利用性和是否存在固定版本。
下面是一个在 CI 中使用 Trivy 的示例,扫描失败时阻断镜像推送。推送失败后,Pod 自然无法引用该镜像,这是第一层控制。准入 webhook 再做第二层兜底,防止有人绕过 CI 直接推送未扫描镜像。
trivy image \ --severity HIGH,CRITICAL \ --exit-code 1 \ --ignore-unfixed \ registry.ipipp.com/team/app:1.2.3 if [ $? -ne 0 ]; then echo "发现高危漏洞,禁止推送" exit 1 fi
五、策略分层与审计落地
镜像准入控制不是单一规则,而是多层策略的组合。仓库白名单负责限定来源,标签策略负责消除漂移,签名验证负责确认完整性,漏洞阈值负责控制风险。每一层可以独立演进,但需要在同一条 webhook 链路上统一执行。策略过多可能出现规则冲突,例如开发环境允许某公共镜像,生产环境不允许,这要求策略引擎支持按命名空间、标签和资源类型做作用域隔离。
OPA/Gatekeeper 通过 Constraint 和 ConstraintTemplate 解耦策略与执行,适合团队规模较大的场景。Kyverno 则以 Kubernetes 原生策略形式呈现,学习成本更低。无论选择哪种工具,都建议把拒绝原因写清楚,方便开发者快速修正镜像引用。下面是一个 Kyverno 策略片段,它拒绝未带摘要的镜像,并在返回信息中提示需要执行 cosign resolve。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-digest
spec:
validationFailureAction: Enforce
background: false
rules:
- name: check-digest
match:
any:
- resources:
kinds:
- Pod
validate:
message: "镜像必须使用 digest 而不是 tag"
pattern:
spec:
containers:
- image: "*@sha256:*"
策略上线后不能一劳永逸。至少要定期检查被拒绝请求的类型,判断是策略过严还是团队确实存在风险行为。同时要演练 webhook 服务故障时的表现,确认 failurePolicy 设置合理。镜像治理的最终目标不是给开发者添堵,而是把安全基线变成默认行为。当受信仓库、签名、扫描和 digest 都成为唯一入口时,不可信镜像自然就没有落地空间。