今天的 nginx:latest 还是昨天那个镜像吗
目标
把指向同一个应用的三种镜像写法(摘要固定 / latest 标签 / 内部注册表的版本标签)并排部署来比较,按来源设置 imagePullPolicy 和 pull secret,然后用 ValidatingAdmissionPolicy 执行“只放行允许的注册表或以摘要固定的镜像”这条规则,并观察拒绝。
为什么重要
云原生安全的 4C(Cloud、Cluster、Container、Code)中,里面的两层是 运行什么 的问题。集群锁得再好,如果 Pod 拉取的镜像是被人调了包的,防御就会从内部崩溃。
镜像标签(tag)只是个名牌,随时可以在注册表里重新贴到另一个镜像上。nginx:latest 可能今天和明天是不同的镜像,app:1.2.3 这样的版本标签,如果没有禁止覆盖,也是一样。相反,@sha256:... 摘要是镜像内容的哈希,哪怕只差一个字符,值也会不同。所以供应链安全的第一步,是分辨出“哪些镜像只是靠标签来指向的”。
imagePullPolicy 决定节点是否相信缓存。对带标签的镜像使用 IfNotPresent,各个节点可能会运行不同时间点的镜像。内部注册表的凭据通过 pull secret 给出,把它挂到 ServiceAccount 上,就不必在每个 Pod 里重复。
最后,准入策略只在 创建请求的时刻 才会求值。第 2 步里没有策略时创建的 Deployment,在第 6 步的策略启用之后依然原样保留。但是,如果 mutable(nginx:latest)的 Pod 被删除,ReplicaSet 要创建新 Pod,那个创建请求就会被拒绝——这就是在启用策略之前,要先检查现有工作负载的原因。
步骤
- 创建命名空间
kcsa-supply。 - 创建
pinned(摘要)、mutable(nginx:latest)、internal(registry.example.com/app:1.2.3)三个 Deployment。 - 把每个镜像分类为
digest/tag,写入/root/kcsa-supply/classify.txt。 - 把带标签的镜像改成
imagePullPolicy: Always,把摘要镜像改成IfNotPresent。 - 创建 docker-registry 类型的 Secret
regcred,挂到 ServiceAccountdeployer的 imagePullSecrets 上。 - 创建只放行允许的注册表或摘要固定的策略
require-allowed-image和绑定。 - 把
docker.io/library/evil:1Pod 被拒绝的消息保存到/root/kcsa-supply/denied-image.txt。 - 把检查结果留在
/root/kcsa-supply/report.txt中。
参考
- 这个实验集群里没有容器运行时,不会真的拉取镜像。所以可以使用假的摘要和不存在的注册表,判定依据是 spec 和准入结果。
- 摘要固定只能保证“不变”,不能保证“可信”。在实际工作中,还要同时使用签名验证(cosign 等)和漏洞扫描。
- 如果白名单的前缀里没有包含
/,像registry.example.com.attacker.io/...这样的名字就会通过。 - 官方文档:Images · Validating Admission Policy.
创建要追究来源的工作区域
创建命名空间 kcsa-supply。后面步骤的策略绑定,会用自动附上的 kubernetes.io/metadata.name=kcsa-supply 标签来选中这个命名空间。
所有命名空间都会自动加上以自己的名字为值的 kubernetes.io/metadata.name 标签。把 create 用 --dry-run=client 生成后再 apply,运行多次也安全。
同样是 nginx,来源写法却三种各不相同
在命名空间 kcsa-supply 中创建三个 Deployment(各 replicas 1,selector 与 Pod 标签要对上)。pinned 的镜像是 nginx@sha256:0000000000000000000000000000000000000000000000000000000000000000,mutable 是 nginx:latest,internal 是 registry.example.com/app:1.2.3。每个 Deployment 的第一个容器镜像必须恰好是这个字符串。
镜像引用的形式是 [레지스트리/]저장소[:태그][@sha256:다이제스트](占位符依次为注册表、仓库、标签与摘要)。kubectl create deployment 会根据镜像字符串生成容器名称,对含有 @sha256: 的镜像,名称可能不符合规则而失败。用自己写出容器名称的 YAML 清单来创建更安全。摘要必须是 64 位十六进制才算语法有效。
分辨哪些镜像明天可能会变
把三个 Deployment 的镜像分类为 digest(以摘要固定)或 tag(以标签指向),按 이름=분류(占位符依次为名称与分类)的形式,每行一个(pinned、mutable、internal),写入 /root/kcsa-supply/classify.txt。
镜像标签是注册表一侧随时可以重新指向另一个镜像的名牌,而摘要是镜像内容的哈希,不会变。不只是 latest,1.2.3 这样的版本标签,归根到底也是标签。用 kubectl get deploy -o jsonpath 查看实际的镜像字符串,按有没有 @sha256: 来区分。
不要相信标签,每次都重新确认
修改 Deployment 的容器 imagePullPolicy——使用标签的 mutable 和 internal 设为 Always,以摘要固定的 pinned 设为 IfNotPresent。
镜像标签指向的对象可能改变,所以即使节点缓存里有同名的旧镜像,也要重新向注册表询问。摘要是内容哈希,缓存里有就可以放心直接使用。默认值是:标签为 latest 或没有标签时为 Always,其他为 IfNotPresent,所以 internal 要自己改。用 kubectl patch 修改 spec.template.spec.containers 中对应的容器(按名称匹配)。
把内部注册表的凭据挂到 ServiceAccount 上
在命名空间 kcsa-supply 中创建 docker-registry 类型的 Secret regcred(服务器 registry.example.com,用户 u,密码 p),再创建 ServiceAccount deployer,把 regcred 放进它的 imagePullSecrets。
kubectl create secret 有 docker-registry 子命令,会帮你创建 kubernetes.io/dockerconfigjson 类型的 Secret(服务器、用户、密码的标志)。把 imagePullSecrets 挂到 ServiceAccount 上,以该 SA 启动的每个 Pod 就不必另外写 Secret。创建 SA 之后,用 patch 添加字段。
不放行允许的注册表之外的镜像
创建 ValidatingAdmissionPolicy require-allowed-image 和绑定 require-allowed-image-binding。策略在 Pod CREATE 时,要求所有容器镜像以 registry.example.com/ 开头或包含 @sha256: 才放行(消息为 image must come from registry.example.com or be digest-pinned,failurePolicy: Fail),绑定以 validationActions 为 ["Deny"],只应用到 kubernetes.io/metadata.name: kcsa-supply 命名空间。
CEL 的字符串有 startsWith() 和 contains()。前缀末尾要带上 /,像 registry.example.com.evil.io/... 这样的名字才漏不过去。请记住,策略决定检查的内容,绑定决定适用范围和动作(Deny)。
来自 Docker Hub 的陌生镜像被挡在门口
试着在命名空间 kcsa-supply 中创建一个镜像为 docker.io/library/evil:1 的 Pod,把 API 服务器返回的完整拒绝消息保存到 /root/kcsa-supply/denied-image.txt。
被拒绝时,kubectl 会以非 0 的退出码结束,并把错误写到标准错误。标准错误也要存入文件。刚创建绑定后,可能要过 1–2 秒才生效,所以如果 Pod 已经被创建出来,就把它删掉再重试。也请确认第 2 步的 Deployment 的 Pod 仍然活着——准入只作用于创建请求。
把镜像来源检查的结果以账簿的形式留下
把检查结果写成四行保存到 /root/kcsa-supply/report.txt——pinned=digest、mutable=tag、latest-pullpolicy=Always、untrusted-registry=denied。评分器会把每一行与实际的 Deployment spec,以及用白名单之外的镜像创建 Pod 的结果核对。
把前面步骤里亲自确认的值抄过来就行。latest-pullpolicy 是 mutable 容器实际的 imagePullPolicy 值(大小写原样),untrusted-registry 是第 7 步看到的结果(denied 或 allowed)。