TT Lab
开始
学习 学习路径 课程

策略即代码

镜像引用策略 — 标签、摘要,以及叠在上面的东西

在 TT Lab 中继续学习

一句话总结

标签(tag)是可以移动的名称标记,摘要(digest)是内容的哈希,无法移动,而镜像策略几乎全部都是这一句话的推论。

为什么需要它

在部署事故中,有一类最难重现:昨天运行良好的东西,今天的表现不同了。清单没变,代码也没变,可就是不同。当原因出在镜像上时,通常是因为同一个标签指向了不同的内容。

Kubernetes 官方文档简短地点明了这一性质:标签可以被移动,指向另一个镜像,而摘要是固定的。摘要是镜像内容的哈希,不可变。所以 app:v1.2.3 不是承诺,而是惯例。注册表并不阻止把新镜像推送到同一个标签上(除非是启用了不可变标签设置的注册表)。

latest 是这个问题的极端。如果不写标签,Kubernetes 就把它视为 latest。此外还有一个事实:如果省略标签,或者用 :latest,又没有写 imagePullPolicy,这个值会自动变成 Always。也就是说,Pod 每次重启,都会去拉取那一刻注册表给出的内容。昨天和今天不同,原因就在这里。

工作原理

镜像引用由三部分构成。

registry.example.com/team/app:v1.2.3@sha256:1ff6c18f...
└── 레지스트리 ──┘└ 이름 ┘└ 태그 ┘└─── 다이제스트 ───┘

如果三者都写了,会怎样?文档给出了准确的回答。同时写了标签和摘要时,拉取时只使用摘要。所以就有了实际工作中使用的形式——保留标签供人阅读,而真实身份用摘要固定下来。

imagePullPolicy 的默认值也是由引用的形状决定的。

引用 没有写 imagePullPolicy 时
写了摘要 IfNotPresent
标签为 :latest Always
没有标签 Always
其他标签 IfNotPresent

摘要保证什么,不保证什么。这里的误解很多。

保证的只有一件事:内容的一致性。同一个摘要拉取两次,得到的是相同的字节。无论从哪个注册表拉取,无论几年之后再拉取,都是一样的。

不保证的有三件事。

补上后两点的,是签名(谁来担保)和来源证明(是怎样构建的)。SLSA 的 provenance 规范定义了后者的标准格式——把构建者、源码仓库与提交、构建参数,以签名文档的形式留下来。固定摘要是这些上层结构站立的地基。因为要签名的对象,最终指向的就是摘要。本实验环境中没有 cosign 之类的签名工具,所以签名和来源证明只作为概念来讲解。

施加策略的两个位置。

*内侧(准入)。*查看 Pod 规格中的镜像字符串。允许的注册表前缀、禁止 latest、要求摘要。这里有一个重要的陷阱。镜像字符串检查是字符串检查,所以很容易被绕过。如果把 registry.example.com 放进允许列表,只做前缀检查,registry.example.com.evil.net/x 就会通过。必须准确切出主机边界(/ 前面的部分)再做比较。另外,容器并不只存在于 containers 中——漏掉 initContainers 和 ephemeralContainers 的策略很常见。

*外侧(构建和仓库)。*Dockerfile 的 FROM、清单和 Helm Chart 中的镜像值、构建产物的镜像列表。准入是最后一道防线,而这里是人可以修复的位置。如果仓库里有 FROM alpine:3.20,每次构建都可能引入不同的基础镜像。把同样的规则挂在两侧,开发者在 PR 中就能知道,而集群则拦住万一漏出去的。

在现场相遇的样子

第一,标签被覆盖的那天。CI 把 app:v1.4.0 推送了两次。集群中的 Pod 没有变化,但那天重启的 Pod,启动时用的是新的内容。于是在同一个 Deployment 中,每个 Pod 运行的是不同的代码,而原因只有比较摘要才能看到。用 kubectl get pod -o jsonpath 取出 status.containerStatuses[].imageID,就能得到实际拉取的摘要。

第二,回滚不了的回滚。只用标签部署的团队,决定“退回到上一个版本”,结果那个标签已经被覆盖了。要退回的目标消失了。如果是用摘要部署的,退回的地址就还在。

第三,固定摘要的代价。固定不是免费的。基础镜像的安全补丁不会自动跟进,所以必须同时有定期升级所固定的值的流程。没有这个流程,几个月之后就会变成“重现得很完美,但全都有漏洞”的状态。固定与更新是一对。

第四,允许列表的漏洞。用前缀比较编写的策略,放过了名称相似的外部主机,这样的事故屡屡发生。编写规则时,一定要同时放入必须通过的样本和必须被拦住的样本,尤其要加入名称相似的攻击样本。

参考文档

下一项实验要做什么

亲手拆解镜像引用,并用策略检查。从离线镜像包中取出清单和摘要,确认标签和摘要各自指向什么,再把同样的内容复制为另一个标签,看到摘要保持不变。然后编写允许的注册表、禁止 latest、要求摘要这三条规则,放入用名称相似的主机绕过前缀比较的样本,以及只有 initContainers 中存在违规的样本,亲自找出规则的漏洞并补上。