镜像治理在 Kubernetes 集群里经常被当成两件事:一件是“把镜像做小”,另一件是“把标签换成摘要”。但在实际攻防视角下,它们消除的是两类完全不同的风险,而且只有串起来才能被验证。典型场景是:集群里跑着几十个 Deployment,镜像引用写成 app:1.4.2 这类可变标签,镜像本身是带完整包管理器和 /bin/sh 的通用基础镜像。一旦镜像仓库被写入同名标签,或者容器内被拿到一个 shell,前者让“部署的是什么”变得不可证,后者让“能做什么”变得不可控。本文以 Kubernetes 官方安全清单中的镜像条目为对照基线,围绕内容最小化与引用固定两条线,给出评估、改造、准入拦截验证与回滚的完整链路;镜像签名体系搭建和运行时逃逸检测不在讨论范围内。

先把两类风险拆开
把“镜像太大”和“标签可变”混在一起谈,会导致治理动作也混在一起:有人以为瘦身就能防篡改,有人以为固定摘要就不需要关心镜像内容。实际上两者的作用面完全不同,需要分别度量、分别验收。
| 风险面 | 具体表现 | 主要后果 | 治理手段 | 生效层 |
|---|---|---|---|---|
| 镜像内容冗余 | 基础镜像自带 shell、包管理器、编译工具链、curl/wget | 拿到执行入口后横向移动成本低;漏洞面和 CVE 数量被放大 | 多阶段构建、distroless 或 scratch 基础镜像、非 root 运行 | 构建流水线 |
| 引用可变 | 使用 :latest 或可被覆盖的业务标签 | 同名标签被重推后,集群实际运行的字节与审计记录不一致;回滚不可复现 | 改用 image@sha256:... 引用 | 清单与准入控制 |
判断适用条件:只要工作负载使用可变标签、或镜像内保留交互式 shell,就值得做这次改造。如果集群规模很小、发布频率极低,可以先只做摘要固定,再逐步收敛镜像内容;两件事的推进节奏可以不同,但验收标准必须分开写清楚。
第一步:评估存量
改造前先量化基线,否则无法证明效果。需要两个数字:使用可变标签的工作负载比例,以及镜像中是否含有 shell 与包管理器。
盘点清单中的镜像引用:
kubectl get deploy,statefulset,daemonset -A -o json
| jq -r '.items[] | .metadata.namespace + "/" + .metadata.name + " " + (.spec.template.spec.containers[].image)'
| awk '{print} /@sha256:/{d++; next} {t++} END {printf "带摘要: %d 仅标签: %dn", d+0, t+0}'注意排查范围不止 containers,initContainers、ephemeralContainers 以及 CronJob 的 jobTemplate 同样是镜像引用入口,只统计主容器会低估风险。
获取镜像摘要不要依赖运行时字段。kubectl get pod -o jsonpath='{.status.containerStatuses[0].imageID}' 返回的是镜像配置的 ID,与清单中用于拉取的 manifest 摘要不是同一个值。可复现的做法是直接查询仓库:
crane digest registry.example.com/app:1.4.2
# 或
skopeo inspect --format '{{.Digest}}' docker://registry.example.com/app:1.4.2多架构镜像需要注意:镜像索引(manifest list)的摘要与单平台 manifest 的摘要不同。固定索引摘要可以在 amd64/arm64 混合节点上正常拉取;固定成某个平台 manifest 的摘要,换架构就会 ImagePullBackOff。团队内应统一约定固定哪一层,并写进发布规范。
检查镜像内容是否包含 shell,可以本地导出文件系统看一眼:
crane export registry.example.com/app:1.4.2 - | tar -tf - | grep -E '^bin/(sh|bash|ash)$|/(apt|apk|yum)$'有输出说明镜像保留了交互式入口,属于需要收敛的对象。另外可以对照 Kubernetes 官方文档中关于容器镜像与摘要格式的说明,确认引用写法是否合法。
第二步:收敛镜像内容
内容收敛的目标不是“越小越好”,而是“运行时不需要的东西不要留在镜像里”。改造通常在构建流水线完成,不需要改动集群配置,因此影响面可控。
一个多阶段构建的骨架:
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER 65532:65532
ENTRYPOINT ["/app"]几个必须同步处理的连带影响:
- 编译产物需要静态链接,否则去掉 libc 后启动即失败。
- 镜像内没有 shell,健康检查只能用二进制自身实现(应用内暴露 health 端点,或用镜像里的专用探针程序),不能写
sh -c "curl ..."。 - 进程需要自己处理信号,
ENTRYPOINT用 exec 形式而非 shell 形式,避免 PID 1 不转发 SIGTERM 导致优雅退出超时。 - 时区、CA 证书、临时目录等隐式依赖要显式补进最终镜像,否则上线后才在调用外部 HTTPS 接口时报错。
配合的 Pod 级加固建议一并启用:runAsNonRoot: true、readOnlyRootFilesystem: true、allowPrivilegeEscalation: false、丢弃全部 capability 并按需加回。这些属于内容治理的延伸,与镜像瘦身同向,但不要把它们当作运行时逃逸检测的替代品。
第三步:把引用固定到 sha256 摘要
摘要固定解决的是“部署内容可证”的问题。Kubernetes 文档明确说明,当引用同时包含标签和摘要时,拉取只使用摘要;因此可以写成 app:1.4.2@sha256:...,既保留人类可读的版本信息,又保证不可变。这一点在官方镜像文档中有直接描述,适合写进团队规范。
改造方式有两种,按团队成熟度选择:
- 清单里直接写死摘要:可读性差、变更需要改文件,但最直观,适合先在小范围试点。
- 在 CI 中自动解析并回填摘要(如
crane digest或 GKE 文档介绍的 digester 方案,它可作为变更型准入 webhook 或 kpt/kustomize 客户端函数运行):发布流程无需人工介入,推荐作为长期方案。
需要接受的运维代价:镜像更新不再“重推标签即生效”,必须产生一次清单变更。这既是安全性来源,也是发布节奏的约束,需要提前和研发对齐。回滚同样要走清单变更,因此保留原标签引用作为回滚素材是必要的。
第四步:用准入控制兜住漏网引用
清单改造只能覆盖已知工作负载,新提交的 YAML 仍可能写回标签。准入控制的作用是把规范变成强制。Kubernetes 内置的准入机制中,ImagePolicyWebhook 通过 ImageReview 对象评估镜像;实践中更常见的是 Kyverno 或 OPA/Gatekeeper 这类以 validating admission webhook 形式部署的策略引擎。无论选哪种,策略内容都是“镜像引用必须匹配 @sha256: 摘要格式”。

以 Kyverno 为例,先以审计模式观察一段时间再切换为拒绝:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-digest
spec:
validationFailureAction: Audit # 观察期用 Audit,确认误报后改为 Enforce
background: false
rules:
- name: pod-require-digest
match:
any:
- resources:
kinds:
- Pod
validate:
message: "镜像引用必须包含 @sha256: 摘要,不接受可变标签。"
pattern:
spec:
containers:
- image: "*@sha256:*"
=(initContainers):
- image: "*@sha256:*"这里有三个容易踩的坑:
- 只匹配
Pod时,kubectl apply一个 Deployment 仍会返回成功,实际拦截发生在 ReplicaSet 控制器创建 Pod 阶段,表现为 ReplicaSet 事件里的拒绝信息。排查时容易误判为策略没生效。若希望 apply 阶段就失败,需要额外为 Deployment、StatefulSet、DaemonSet、Job、CronJob 增加匹配spec.template.spec.containers的规则。 - 遗漏
ephemeralContainers。kubectl debug注入的临时容器同样走镜像拉取,如果策略不覆盖,就等于留了一条绕过路径,需要显式评估是否放行并限定镜像。 failurePolicy的选择。设为Fail时,策略引擎不可用会导致符合条件的资源创建全部失败;需要评估 webhook 副本数、超时时间与命名空间排除范围。把kube-system整体排除会留下缺口,更稳妥的做法是按具体组件和标签精确排除,并记录豁免原因与有效期。
验证拦截是否真的生效
策略写完不等于生效。验证要覆盖“应拒绝”和“应放行”两个方向,并且尽量用 --dry-run=server 走完整准入链而不真正创建资源。
验证旧标签被拒绝:
kubectl -n demo run probe --image=registry.example.com/app:1.4.2
--restart=Never --dry-run=server -o yaml
# 预期输出包含: admission webhook "validate.kyverno.svc-fail" denied the request:
# 镜像引用必须包含 @sha256: 摘要,不接受可变标签。再验证控制器路径,确认 Deployment 层面是否也被拦住(对应上文的匹配范围问题):
kubectl -n demo apply -f legacy-tag-deploy.yaml
kubectl -n demo describe rs -l app=web | tail -20 # 若 apply 成功,拒绝信息出现在这里验证摘要引用可正常部署:
kubectl -n demo apply -f digest-deploy.yaml
kubectl -n demo rollout status deploy/web --timeout=120s
kubectl -n demo get pod -o jsonpath='{.items[0].spec.containers[0].image}'最后一步很关键:确认集群里实际记录的引用是带摘要的形式,而不是被某个变更型 webhook 又改写回标签。若同时启用了自动回填摘要的 webhook,要确认两者顺序与结果一致,避免“策略拦截标签、webhook 改写标签”互相打架导致行为不可预测。
策略审计结果可以从 PolicyReport 读取,用来发现存量遗漏:
kubectl get policyreport -A -o wide调试需求与回滚路径
去掉 shell 之后最直接的反对意见是“出问题没法进容器看”。这是真实成本,需要给出替代方案而不是让团队自行其是。
首选临时容器,它注入到目标 Pod 的命名空间中,能共享网络与进程视图,但不进入原容器文件系统:
kubectl -n demo debug -it pod/web-7d9c8f6b5-x2v4k
--image=registry.example.com/debug-tools@sha256:<digest>
--target=app前提是集群版本支持临时容器(1.23 起稳定),并且该调试镜像本身通过准入策略——所以调试镜像也必须固定摘要,并纳入受控清单,否则会成为绕过策略的口子。临时容器不能用于静态 Pod,节点级排查需要用 kubectl debug node/... 另走一条路径。
拉取失败是最常见的改造副作用,定位顺序建议固定下来:
kubectl describe pod看事件是ImagePullBackOff还是ErrImagePull,区分“摘要不存在”与“认证失败”。- 用
crane manifest registry.example.com/app@sha256:<digest>直接向仓库确认该摘要是否存在、是否是索引摘要。 - 检查
imagePullSecrets与仓库权限,摘要固定不会改变认证方式,但更换仓库或命名空间时容易漏配。 - 检查节点架构与摘要层级是否匹配;若使用镜像缓存或同步代理,确认代理侧已同步该摘要而非仅同步标签。
回滚方案按影响面从小到大排列:
- 单个工作负载回滚:
kubectl rollout undo,但这只回滚到上一版 ReplicaSet,若上一版同样被策略拦截则无效。真正可靠的是把清单引用改回原来的标签并重新发布,因此改造前必须把原引用完整记录在版本库中。 - 策略侧临时豁免:给特定命名空间打标签,并在策略中通过
namespaceSelector排除,或对特定工作负载使用例外规则。豁免必须限时、限范围、留记录,并在到期后复核;长期豁免等于没有策略。 - 兜底路径:保留策略的 Audit 模式开关,紧急情况下先降级为审计以恢复发布能力,同时记录降级窗口内创建的负载,事后补齐摘要引用。这条路径要写进应急预案,而不是临时决定。
复盘:需要长期观察的指标
改造完成后,把以下几项纳入日常观察,才能判断治理是否持续有效:
- 策略拒绝次数与拒绝原因分布,突增通常意味着有人沿用了旧模板或 CI 镜像未更新。
ImagePullBackOff事件数量,尤其是改造后一周内的变化,用于发现摘要层级或仓库同步问题。- 准入 webhook 的延迟与错误率,以及策略引擎自身的可用副本数;这是
failurePolicy: Fail下的单点风险。 - 镜像体积与镜像内是否重新出现 shell 的漂移检测,可在流水线中加一条断言,防止基础镜像被悄悄换回通用镜像。
- 摘要引用的覆盖率,按命名空间和工作负载类型统计,避免新业务重新退回标签。
这条链路的价值在于可验证:内容最小化把“拿到执行入口之后能做什么”压缩到最小,摘要固定把“部署的到底是什么”变成可复核的事实,准入控制则保证这两条约束不会因为一次匆忙的提交而失效。三者缺一,治理就会退化成一份写完就归档的检查清单。
- flag
- it2021
- it2021.com
- Kubernetes
- shell
- tag
- test
- Webhook





1F
多架构镜像固定摘要确实容易踩坑,多谢提醒