先搞清楚进来的是什么
一句话总结
供应链安全的核心问题不是“这个镜像安全吗”,而是“当前在集群中运行的那些字节究竟是什么、 来自哪里,以及是否遭到过调包”。
为什么需要它
像 myapp:v1.2 这样的标签是可变指针。你可以向同一个标签推送另一个镜像,
latest 更是如此。如果 Pod 规约中只写了标签,就没人能证明节点当前运行的究竟是哪些字节。
即使执行回滚,同一个标签也可能已经指向了别的内容。
摘要(@sha256:...)是内容的哈希值,因此能从根源上消除这个问题。
同样的逻辑也适用于语言包。某个项目直接声明的依赖只有 37 个, 实际安装的包却有 1,123 个,其中 486 个会随生产运行时一起部署。 经过审查和选择的是 37 个,决定信任的却是 1,123 个。锁文件的完整性哈希并不保证“这个 软件包是安全的”,只保证“这个软件包与我上次看到的相同”。 这两句话之间的差别,几乎就是供应链安全的全部。
工作原理
控制措施分为四层。
| 层级 | 回答什么问题 | 工具与字段 |
|---|---|---|
| 固定 | 当前运行的字节是什么 | 镜像摘要、imagePullPolicy |
| 来源 | 它来自哪里 | 允许的镜像仓库列表、imagePullSecrets |
| 证明 | 它是否由我们的流水线构建 | 签名(cosign)、SBOM、来源证明 |
| 强制 | 如何阻止不符合要求的镜像 | 准入策略(VAP、Kyverno、Gatekeeper) |
imagePullPolicy 经常被误解。Always 会每次都向镜像仓库检查清单,
IfNotPresent 则只在节点上不存在镜像时才拉取。使用标签时,Always 看起来更安全,
但这也意味着每次都要重新面对标签可能发生变化的问题。用摘要固定后,
内容按定义就不会变化,因此设置为 IfNotPresent 更合理,而且镜像仓库故障也不会
蔓延成部署故障。
还需要澄清对扫描器的误解。镜像扫描器只做两步:解开镜像层,
生成已安装软件包清单,再将清单与漏洞数据库比对。因此,通过
curl 下载后复制进去的二进制文件,或直接从源码构建的库,根本不会被识别。
这是扫描结果干净并不等于安全的第一个原因。
最后一层是准入控制。扫描结果和签名本身没有强制力。要阻止绕过流水线、 手工推送的镜像进入集群,只能依靠准入策略。在此之前,扫描门禁更接近一种 可以绕过的建议。
现场会遇到的情况
刚接入扫描报告时,通常会看到这样的数字:仅一个 payments:1.4.2 镜像就有
1,247 个问题(LOW 812、MEDIUM 289、HIGH 137、CRITICAL 9)。如果直接把它扔进团队频道,
什么也不会发生。加上一个 --ignore-unfixed 后,数量会降到 4 个(HIGH 3、CRITICAL 1)。
安全性没有任何改善,但剩下的都是当前可以处理的项目。再与 CISA KEV 清单
对照,确认已被实际利用的只有 1 个。今天要处理的就是这一个。
而消除其余 1,200 多个问题的办法并不是逐项打补丁,而是更换基础镜像。
把同一个应用从 node:22(432 个软件包)迁移到 distroless 基础镜像(19 个软件包)后,
可修复的 CRITICAL 从 9 个降到了 0。Shell 和包管理器消失并不是附带效果,
而是关键所在,因为这意味着镜像中没有入侵者可利用的工具。
代价是无法再用 kubectl exec 进入容器,因此调试方式要改为附加临时容器。
在隔离网络中,镜像仓库控制同时也是可用性问题。containerd 1.x 从
sandbox_image 键读取 pause 镜像地址,到了 2.x,该配置移到了 pinned_images 下的
sandbox 键。原本已切换到内部镜像仓库的集群升级到 2.x 后,旧键会被静默忽略,
并回退到默认的公共镜像仓库。结果是该节点上一个 Pod 都无法启动。
由于出问题的不是某个工作负载,而是整个节点,因此很容易被误判为网络故障。
控制镜像的三个层面
供应链领域的问题常以“禁止使用这个镜像”的形式出现。可以实施阻断的位置有三个, 不同问题考查的层面也不同。
第一,容器运行时。 通过节点配置,只允许特定镜像仓库或验证签名。 它会作用于整个集群,但必须在每个节点上配置。
第二,准入控制(admission)。 在集群内通过策略实施阻断。这是考试中
最常出现的位置。ImagePolicyWebhook 是专门向外部服务询问镜像是否允许的
机制,而策略引擎(Kyverno、Gatekeeper)可以设置更通用的条件。
# ImagePolicyWebhook 을 켤 때 함께 필요한 것
# --admission-control-config-file 로 설정 파일을 주고,
# 그 파일이 kubeconfig 를 가리키고, 둘 다 정적 파드에 마운트되어야 한다.
设置 defaultAllow: false 后,无法连接 Webhook 时会拒绝所有请求。这样更安全,
但 Webhook 一旦宕机,集群就无法部署任何资源。考试题通常会考查这个值。
第三,镜像本身。 即签名和 SBOM。签名验证回答“由谁构建”, 扫描回答“里面有什么”。二者不能相互替代—— 既可能存在已签名但有漏洞的镜像,也可能存在干净却来源不明的镜像。
使用摘要而不是标签来固定镜像。 题目经常要求通过策略禁止 :latest。
标签会移动,因此经过验证的内容和实际运行的内容可能不同。
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"
"}{end}' | sort -u | grep -vE '@sha256:'
静态分析也属于供应链。 题目还会涉及在部署前检查清单的工具 (kubesec、kube-linter、trivy config)。要点是:在资源进入集群之前阻止问题, 是成本最低的控制层。
下一实验要做什么
首先用摘要固定镜像,建立允许的镜像仓库列表,并把用于私有镜像仓库认证的 Secret 绑定到 ServiceAccount。签名和 SBOM 验证策略将以清单文件编写。接下来的实验中, 你将亲手创建一个可实际应用于集群的 ValidatingAdmissionPolicy,使用 CEL 表达式 拒绝“只使用标签的镜像”。