标签一个字没改,起来的却不是昨天那个
目标
培养按形状区分镜像引用的眼光,把要求固定摘要的规则先以警告的方式挂到准入上,再只在很窄的位置提升为拦截,编写把标签换成摘要的锁文件和解析器,并把同样的规则也施加到集群之外的构建产物上。
为什么重要
标签是可以移动的名称标记,摘要是内容的哈希,无法移动。镜像策略几乎全部都是这一句话的推论。注册表并不阻止把新镜像推送到同一个标签上,所以只用标签部署的团队,既无法重现“昨天和今天不同”的事故,又会遇到“要回滚的对象已经消失”的回滚。因此规则的第一行不是禁止 latest,而是固定摘要。不过,固定所保证的只有内容一致性这一件事——那个内容是否安全、是谁构建的、是怎样构建的,这些都不在哈希里,而漏洞扫描、签名和来源证明,分别在另外的维度上补上这些位置。编写规则的一方也有两个陷阱。容器列表不是 spec.containers 一个,而是包括 initContainers 和 ephemeralContainers 共三个,而且镜像引用不只存在于 Pod 中,工作负载的 Pod 模板里也有,只是路径深了一层。漏掉这两点的策略,即使启用了也会被绕过。此外,固定与更新流程是一对。锁文件就是那个流程所在的位置。
步骤
-
在
/root/imgpolicy中工作。在/root/imgpolicy/manifests/中创建四个清单——web.yaml是 Podprobe-web(initContainersmigrate为registry.internal/migrate:2.1,containersweb为registry.internal/api:1.4.0),cache.yaml是 Podprobe-cache(containerscache为registry.internal/sidecar@sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266),batch.yaml是 Podprobe-batch(containersbatch为没有标签的registry.internal/runtime,tail为busybox:latest),api.yaml是 Deploymentprobe-api(模板容器api为registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc)。然后创建/root/imgpolicy/classify.sh <디렉터리>(占位符为目录)。从该目录的.yaml和.yml中取出所有镜像引用,去重并排序,每行以<참조> <모양>(占位符依次为引用与形状)的形式输出。形状恰好是digest、latest、tag、notag这四个词之一。最后用./classify.sh manifests > /root/imgpolicy/01-shapes.txt保存结果。 -
先执行
export KUBECONFIG=/root/.kube/config和kubectl config use-context kwok-lab。在/root/imgpolicy/policy.yaml中编写admissionregistration.k8s.io/v1的 ValidatingAdmissionPolicyrequire-image-digest——matchConstraints.resourceRules匹配核心组("")v1pods的CREATE和UPDATE,变量floating是object.spec.containers的镜像中没有@sha256:的那些的列表,validation 是size(variables.floating) == 0,messageExpression 包含这个列表。创建命名空间img-warn并加上标签image-policy=warn。在/root/imgpolicy/binding-warn.yaml中编写 ValidatingAdmissionPolicyBindingimage-digest-warn——policyName为require-image-digest,validationActions为["Warn", "Audit"],matchResources.namespaceSelector.matchLabels为image-policy: warn。创建测试用 Pod/root/imgpolicy/pod-tag.yaml——Podprobe-tag,initContainersmigrate为registry.internal/migrate:2.1,containersweb为registry.internal/api:1.4.0。应用策略和绑定之后,用--dry-run=server把pod-tag.yaml发送到img-warn,把输出连同标准错误一起保存到/root/imgpolicy/02-warn.txt。即使出现警告,Pod 也必须能够创建。 -
先创建
/root/imgpolicy/pod-init.yaml——Podprobe-init,initContainersmigrate为registry.internal/migrate:2.1(未固定),containersapp为registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc。用--dry-run=server把它发送到img-warn,会发现没有出现警告。然后修改/root/imgpolicy/policy.yaml——让变量allImages成为把spec.containers、spec.initContainers和spec.ephemeralContainers全部连接起来的镜像列表(对可能不存在的字段,先用has(...)检查),floating只保留其中没有@sha256:的。并在matchConstraints的resources中再加上pods/ephemeralcontainers。另外在/root/imgpolicy/pod-pinned.yaml中创建 Podprobe-pinned(containersapp为registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc),并在img-warn中真正创建它(kubectl apply -n img-warn -f pod-pinned.yaml)。临时容器在创建 Pod 时无法附加,所以要发送到子资源:kubectl get -n img-warn pod probe-pinned -o json | jq '.spec.ephemeralContainers = [{"name":"dbg","image":"busybox:latest"}]' | kubectl replace --raw "/api/v1/namespaces/img-warn/pods/probe-pinned/ephemeralcontainers?dryRun=All" -f -
应用修改后的策略之后,(1) 把 pod-init.yaml 重新发送到 img-warn,(2) 发送上面的临时容器请求,把两份输出连同标准错误一起保存到 /root/imgpolicy/03-lists.txt。这次两者都必须出现警告。
4. 再创建两个命名空间——给 img-prod 加上标签 image-policy=enforce,而 img-legacy 不加任何标签(在范围之外)。警告用的绑定不要删除,保持原样。在 /root/imgpolicy/binding-deny.yaml 中编写第二个绑定 image-digest-deny——同一个 policyName,validationActions 为 ["Deny"],选择器为 image-policy: enforce。接着在 /root/imgpolicy/policy.yaml 的 matchConstraints 中,加入针对 apps 组 v1 的 deployments 的 CREATE 和 UPDATE 的规则,并设置变量 podSpec,让它在 has(object.spec.template) 时选择 object.spec.template.spec,否则选择 object.spec。allImages 现在改为从 variables.podSpec 的三个列表中取出。在 /root/imgpolicy/dep-tag.yaml 中创建 Deployment probe-dep(模板容器 api 为 registry.internal/api:1.4.0),依次用 --dry-run=server 发送到 img-prod 和 img-legacy,并把两份输出保存到 /root/imgpolicy/04-deny.txt。在 img-prod 中必须被拒绝,而在 img-legacy 中必须照常创建。
5. 在 /root/imgpolicy/images.lock 中,用一个 JSON 对象写出锁文件。键是带标签的引用,值是摘要——registry.internal/api:1.4.0 对应 sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc,registry.internal/migrate:2.1 对应 sha256:3e7c7af932c7b5e39c7812b321bbbd291da943717e96860e5976ac609ef84030,registry.internal/runtime:3.20 对应 sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c,registry.internal/sidecar:1.0 对应 sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266。然后创建解析器 /root/imgpolicy/resolve.sh <매니페스트> [잠금파일](占位符依次为清单与锁文件)。省略锁文件参数时,使用 /root/imgpolicy/images.lock。它读取清单,把标签引用替换为 <이름>@<다이제스트>(占位符依次为名称与摘要),并把整个清单输出到标准输出,已经用摘要固定的行则原样透传。锁文件中没有的引用,不要编造摘要,而是向标准错误每行输出一条 LOCK MISS <참조>(占位符为引用),并以退出码 3 结束(此时标准输出中什么都不输出)。创建之后,用 ./resolve.sh manifests/web.yaml > /root/imgpolicy/resolved/web.yaml 保存解析结果,把 ./resolve.sh manifests/batch.yaml 的标准错误保存到 /root/imgpolicy/05-miss.txt,然后在该文件末尾追加一行 EXIT <종료코드>(占位符为退出码)。
6. 编写 /root/imgpolicy/Dockerfile——第一阶段是 FROM registry.internal/builder:3.2 AS build,第二阶段是 FROM registry.internal/runtime@sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c。在 /root/imgpolicy/build.json 中写入构建产物列表。有三个键——build(字符串 api-2026-09-17)、base_images(按所写的顺序原样放入 Dockerfile 中 FROM 引用的字符串数组)、artifacts(带有 name 和 image 的对象数组:api 为 registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc,worker 为 registry.internal/worker:2.0)。在 /root/imgpolicy/policies/image-refs.yaml 中编写 apiVersion: json.kyverno.io/v1alpha1、kind: ValidatingPolicy 的策略。有两条规则——base-pinned 要求 base_images 中没有 @sha256: 的数量为 0,artifact-pinned 要求 artifacts[].image 中没有 @sha256: 的数量为 0。然后创建门禁 /root/imgpolicy/jsongate.sh <페이로드> [정책](占位符依次为载荷与策略;省略策略参数时使用 /root/imgpolicy/policies/image-refs.yaml)。解析 kyverno json scan 给人读的输出,每条违规输出一行 FAIL <그 줄>(占位符为该行),最后输出 RESULT failed=<개수>(占位符为数量),有违规则以退出码 1 结束,没有则为 0。如果一条判定行(PASSED、FAILED、ERROR:)都没有,不要视为通过,而是以退出码 2 结束。最后创建 /root/imgpolicy/06-gate.txt——第一行是记录原样运行策略时的退出码的 SCAN EXIT <코드>(占位符为退出码),下面是 ./jsongate.sh build.json 的输出,最后一行是 GATE EXIT <코드>(占位符为退出码)。
7. 摘要所保证的只有内容一致性这一件事。它不保证的三件事,由其他维度的机制来补上。在 /root/imgpolicy/limits.tsv 中恰好写三行。每行是用一个制表符分隔的两栏,第一栏是 content-safety、author、build-path 各出现一次,第二栏是补上该位置的机制,从 scan、signature、provenance 中选一个合适的(第一栏的顺序按这个顺序书写)。然后在 /root/imgpolicy/revoked.txt 中每行写一个已吊销的摘要——现在只有一行。registry.internal/sidecar:1.0 所指向的摘要已被发现存在漏洞,在 /root/imgpolicy/images.lock 中找到那个值并原样写入(不写 @ 之前的名称,只写以 sha256: 开头的值)。
8. 创建 /root/imgpolicy/audit.sh <매니페스트디렉터리> [폐기목록](占位符依次为清单目录与吊销列表)。省略吊销列表参数时,使用 /root/imgpolicy/revoked.txt。从该目录的 .yaml 和 .yml 中取出所有镜像引用,对每个没有用摘要固定的引用,输出一行 FLOATING <파일이름> <참조>(占位符依次为文件名与引用);已固定、但该摘要在吊销列表中的,输出一行 REVOKED <파일이름> <참조>(这些行要排序后输出)。最后一行恰好是 RESULT floating=<수> revoked=<수> ready=<yes|no>(占位符均为数量),只有两者都为 0 时才是 ready=yes。退出码是:ready=yes 时为 0,否则为 1。创建之后,把 ./audit.sh manifests 的输出保存到 /root/imgpolicy/audit.txt,并在该文件末尾追加一行 EXIT <종료코드>(占位符为退出码)。第 1 步中创建的那批清单就是对象。
参考
- 先执行
export KUBECONFIG=/root/.kube/config和kubectl config use-context kwok-lab。这是 Pod 内 kwok 启动的真实 kube-apiserver v1.30.4,所有产物都放在/root/imgpolicy之下。 - kwok 不会真正运行 Pod。本实验所看的只有准入阶段,所以用
kubectl create -f <파일> --dry-run=server(占位符为文件)就足够了——它会照常经过准入,但不会留下对象。 - 没有能实际下载镜像的运行时。所以标签与摘要的对应关系,要用你制作的锁文件(标签 -> 摘要表)来处理。它与实际工作中的镜像锁文件形态相同。
- 这个镜像中没有
cosign、skopeo、conftest、opa,也没有网络。所以无法实际运行签名验证和来源证明,第 7 步只从概念和维度的区分来讲解。集群之外的检查用kyverno json scan进行。 - 修改策略和绑定之后,反映会晚一两次往返。如果结果还是和原来一样,请几秒后再发送一次。
- 常见错误:只看
spec.containers。如果漏掉initContainers和ephemeralContainers,策略即使启用,也会被绕过。 - 常见错误:用整个引用中是否有
:来判断有没有标签。registry.internal:5000/team/app会被错误地分类为带标签的引用。 - 常见错误:相信
kyverno json scan的退出码。即使有违规也会以 0 结束——第 6 步要亲自打印出来确认。 - Kubernetes 镜像 · Validating Admission Policy · Kubernetes 中的 CEL · OCI descriptor · OCI distribution spec · SLSA provenance · kyverno-json
同一批清单中有四种形状的镜像引用
在 /root/imgpolicy 中工作。在 /root/imgpolicy/manifests/ 中创建四个清单——web.yaml 是 Pod probe-web(initContainers migrate 为 registry.internal/migrate:2.1,containers web 为 registry.internal/api:1.4.0),cache.yaml 是 Pod probe-cache(containers cache 为 registry.internal/sidecar@sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266),batch.yaml 是 Pod probe-batch(containers batch 为没有标签的 registry.internal/runtime,tail 为 busybox:latest),api.yaml 是 Deployment probe-api(模板容器 api 为 registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc)。然后创建 /root/imgpolicy/classify.sh <디렉터리>(占位符为目录)。从该目录的 .yaml 和 .yml 中取出所有镜像引用,去重并排序,每行以 <참조> <모양>(占位符依次为引用与形状)的形式输出。形状恰好是 digest、latest、tag、notag 这四个词之一。最后用 ./classify.sh manifests > /root/imgpolicy/01-shapes.txt 保存结果。
镜像引用是 <레지스트리>/<이름>[:태그][@sha256:...](韩文占位符依次为注册表、名称、标签)。如果带有摘要,即使同时有标签,拉取时用的也只是摘要,所以按 digest 处理。如果在整个引用中找 : 来判断有没有标签,像 registry.internal:5000/team/app 这种主机带端口的引用,会被错误地看作带标签——要只看最后一个 / 之后的部分来判断。在 shell 中,最后一段可以用 ${ref##*/} 得到。
把要求摘要的规则以警告方式挂上
先执行 export KUBECONFIG=/root/.kube/config 和 kubectl config use-context kwok-lab。在 /root/imgpolicy/policy.yaml 中编写 admissionregistration.k8s.io/v1 的 ValidatingAdmissionPolicy require-image-digest——matchConstraints.resourceRules 匹配核心组("")v1 pods 的 CREATE 和 UPDATE,变量 floating 是 object.spec.containers 的镜像中没有 @sha256: 的那些的列表,validation 是 size(variables.floating) == 0,messageExpression 包含这个列表。创建命名空间 img-warn 并加上标签 image-policy=warn。在 /root/imgpolicy/binding-warn.yaml 中编写 ValidatingAdmissionPolicyBinding image-digest-warn——policyName 为 require-image-digest,validationActions 为 ["Warn", "Audit"],matchResources.namespaceSelector.matchLabels 为 image-policy: warn。创建测试用 Pod /root/imgpolicy/pod-tag.yaml——Pod probe-tag,initContainers migrate 为 registry.internal/migrate:2.1,containers web 为 registry.internal/api:1.4.0。应用策略和绑定之后,用 --dry-run=server 把 pod-tag.yaml 发送到 img-warn,把输出连同标准错误一起保存到 /root/imgpolicy/02-warn.txt。即使出现警告,Pod 也必须能够创建。
策略只决定“判定什么”,“在哪里、以什么强度”由绑定决定。Warn 只通过响应头返回警告,并放行请求。把推行的第一阶段设为 Warn,是为了先统计会撞上什么。CEL 的字符串有 contains、startsWith、endsWith,列表有 map、filter、join。警告是通过标准错误输出的,所以要加上 2>&1,才会一并写入文件。kubectl create -f <파일> --dry-run=server(占位符为文件)会让请求照常通过准入,但不留下对象。拒绝消息和警告,都与真实请求一样出现。
只看一个容器列表,就会被绕过
先创建 /root/imgpolicy/pod-init.yaml——Pod probe-init,initContainers migrate 为 registry.internal/migrate:2.1(未固定),containers app 为 registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc。用 --dry-run=server 把它发送到 img-warn,会发现没有出现警告。然后修改 /root/imgpolicy/policy.yaml——让变量 allImages 成为把 spec.containers、spec.initContainers 和 spec.ephemeralContainers 全部连接起来的镜像列表(对可能不存在的字段,先用 has(...) 检查),floating 只保留其中没有 @sha256: 的。并在 matchConstraints 的 resources 中再加上 pods/ephemeralcontainers。另外在 /root/imgpolicy/pod-pinned.yaml 中创建 Pod probe-pinned(containers app 为 registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc),并在 img-warn 中真正创建它(kubectl apply -n img-warn -f pod-pinned.yaml)。临时容器在创建 Pod 时无法附加,所以要发送到子资源:
kubectl get -n img-warn pod probe-pinned -o json | jq '.spec.ephemeralContainers = [{"name":"dbg","image":"busybox:latest"}]' | kubectl replace --raw "/api/v1/namespaces/img-warn/pods/probe-pinned/ephemeralcontainers?dryRun=All" -f -
应用修改后的策略之后,(1) 把 pod-init.yaml 重新发送到 img-warn,(2) 发送上面的临时容器请求,把两份输出连同标准错误一起保存到 /root/imgpolicy/03-lists.txt。这次两者都必须出现警告。
Pod 中有三个容器列表——containers、initContainers、ephemeralContainers。实际工作中最常见的策略漏洞,就是只看第一个列表。在 CEL 中列表可以用 + 连接,对可能不存在的字段,用 has(x) ? x : [] 形式的三元表达式包裹。ephemeralContainers 不能通过创建 Pod 的 CREATE 来设置(Forbidden: cannot be set on create),只能通过 pods/ephemeralcontainers 子资源进入——所以必须把这个子资源写进策略的 resources,像 kubectl debug 这样的路径才会被同一条规则看到。因为加了 ?dryRun=All,这个请求只接受判定,不会在 Pod 上留下任何东西。
拦截不要大范围启用,只在很窄的位置启用
再创建两个命名空间——给 img-prod 加上标签 image-policy=enforce,而 img-legacy 不加任何标签(在范围之外)。警告用的绑定不要删除,保持原样。在 /root/imgpolicy/binding-deny.yaml 中编写第二个绑定 image-digest-deny——同一个 policyName,validationActions 为 ["Deny"],选择器为 image-policy: enforce。接着在 /root/imgpolicy/policy.yaml 的 matchConstraints 中,加入针对 apps 组 v1 的 deployments 的 CREATE 和 UPDATE 的规则,并设置变量 podSpec,让它在 has(object.spec.template) 时选择 object.spec.template.spec,否则选择 object.spec。allImages 现在改为从 variables.podSpec 的三个列表中取出。在 /root/imgpolicy/dep-tag.yaml 中创建 Deployment probe-dep(模板容器 api 为 registry.internal/api:1.4.0),依次用 --dry-run=server 发送到 img-prod 和 img-legacy,并把两份输出保存到 /root/imgpolicy/04-deny.txt。在 img-prod 中必须被拒绝,而在 img-legacy 中必须照常创建。
一个策略可以挂上多个绑定,每个绑定有自己的范围和自己的强度。所以推行的方式是“先窄后宽地扩大”,而不是“大范围挂上再挖例外”——例外列表时间一长就没有人去删,而窄范围每次扩大都需要做出决定。镜像引用不只存在于 Pod 中。Deployment、StatefulSet、Job 在 Pod 模板里有同样的字符串,只是路径深了一层。只匹配 Pod 的策略,最终也能拦住控制器创建的 Pod,但那时部署已经开始,人很难查找原因。用 CEL 的三元表达式把两种形状放进一个变量,validation 就可以只有一个。
把标签换成摘要写下来,只看它来修复
在 /root/imgpolicy/images.lock 中,用一个 JSON 对象写出锁文件。键是带标签的引用,值是摘要——registry.internal/api:1.4.0 对应 sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc,registry.internal/migrate:2.1 对应 sha256:3e7c7af932c7b5e39c7812b321bbbd291da943717e96860e5976ac609ef84030,registry.internal/runtime:3.20 对应 sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c,registry.internal/sidecar:1.0 对应 sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266。然后创建解析器 /root/imgpolicy/resolve.sh <매니페스트> [잠금파일](占位符依次为清单与锁文件)。省略锁文件参数时,使用 /root/imgpolicy/images.lock。它读取清单,把标签引用替换为 <이름>@<다이제스트>(占位符依次为名称与摘要),并把整个清单输出到标准输出,已经用摘要固定的行则原样透传。锁文件中没有的引用,不要编造摘要,而是向标准错误每行输出一条 LOCK MISS <참조>(占位符为引用),并以退出码 3 结束(此时标准输出中什么都不输出)。创建之后,用 ./resolve.sh manifests/web.yaml > /root/imgpolicy/resolved/web.yaml 保存解析结果,把 ./resolve.sh manifests/batch.yaml 的标准错误保存到 /root/imgpolicy/05-miss.txt,然后在该文件末尾追加一行 EXIT <종료코드>(占位符为退出码)。
锁文件是把“这个标签现在指向什么”用人能读懂的方式写下来的表格。部署用摘要进行,更新则通过修改这张表来完成——固定与更新是一对。用 jq -r --arg k "$ref" '.[$k] // empty' <파일>(占位符为文件),可以安全地查找一个键。要从引用中去掉标签,必须删除最后一个 / 之后的 : 及其后的部分——sed 's/:[^:/]*$//' 不会动主机端口。解析器是否真的读取了锁文件,给它另一个锁文件就会立刻暴露。本实验的评分器就是这样做的。
把同样的规则也施加到集群之外的构建产物上
编写 /root/imgpolicy/Dockerfile——第一阶段是 FROM registry.internal/builder:3.2 AS build,第二阶段是 FROM registry.internal/runtime@sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c。在 /root/imgpolicy/build.json 中写入构建产物列表。有三个键——build(字符串 api-2026-09-17)、base_images(按所写的顺序原样放入 Dockerfile 中 FROM 引用的字符串数组)、artifacts(带有 name 和 image 的对象数组:api 为 registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc,worker 为 registry.internal/worker:2.0)。在 /root/imgpolicy/policies/image-refs.yaml 中编写 apiVersion: json.kyverno.io/v1alpha1、kind: ValidatingPolicy 的策略。有两条规则——base-pinned 要求 base_images 中没有 @sha256: 的数量为 0,artifact-pinned 要求 artifacts[].image 中没有 @sha256: 的数量为 0。然后创建门禁 /root/imgpolicy/jsongate.sh <페이로드> [정책](占位符依次为载荷与策略;省略策略参数时使用 /root/imgpolicy/policies/image-refs.yaml)。解析 kyverno json scan 给人读的输出,每条违规输出一行 FAIL <그 줄>(占位符为该行),最后输出 RESULT failed=<개수>(占位符为数量),有违规则以退出码 1 结束,没有则为 0。如果一条判定行(PASSED、FAILED、ERROR:)都没有,不要视为通过,而是以退出码 2 结束。最后创建 /root/imgpolicy/06-gate.txt——第一行是记录原样运行策略时的退出码的 SCAN EXIT <코드>(占位符为退出码),下面是 ./jsongate.sh build.json 的输出,最后一行是 GATE EXIT <코드>(占位符为退出码)。
扫描是 KYVERNO_EXPERIMENTAL=true kyverno json scan --payload <JSON> --policy <YAML>——如果漏掉环境变量,这是实验性功能,命令本身会被拒绝。这个工具即使找到违规,退出码也是 0,--output json 中也不会记录违规。所以用 if kyverno json scan ...; then 写成的门禁总是会通过——这一步要亲自打印出来确认的就是这一点。spec.rules[].assert.all[].check 的键是用括号包起来的 JMESPath 表达式,值是预期值。在 JMESPath 的过滤器中,“不包含的”是通过把 contains 的结果与假值比较来选出的——JSON 字面量用反引号包裹,所以字符串和假值都需要反引号。如果想单独测试表达式,kyverno jp query -i <파일> '<식>'(占位符依次为文件与表达式)最快。准入是最后一道防线,而这里是人可以在 PR 中修复的位置——把同样的规则挂在两侧,原因就在这里。
固定只决定身份,不决定安全
摘要所保证的只有内容一致性这一件事。它不保证的三件事,由其他维度的机制来补上。在 /root/imgpolicy/limits.tsv 中恰好写三行。每行是用一个制表符分隔的两栏,第一栏是 content-safety、author、build-path 各出现一次,第二栏是补上该位置的机制,从 scan、signature、provenance 中选一个合适的(第一栏的顺序按这个顺序书写)。然后在 /root/imgpolicy/revoked.txt 中每行写一个已吊销的摘要——现在只有一行。registry.internal/sidecar:1.0 所指向的摘要已被发现存在漏洞,在 /root/imgpolicy/images.lock 中找到那个值并原样写入(不写 @ 之前的名称,只写以 sha256: 开头的值)。
同一个摘要拉取两次,得到的是相同的字节。仅此而已——这些字节是否安全、是谁构建的、是在什么源码上由哪条流水线构建的,都不在哈希里。漏洞扫描回答“内容是否安全”,签名回答“谁来担保”,来源证明(SLSA provenance)回答“是怎样构建的”。这三个维度不能互相替代,而且三者都依赖摘要来指明指向什么。吊销列表是第四个维度——只要固定做得好,“指名道姓地拦住那一个”就成为可能。制表符用 printf 'a\tb\n' 写入更安全。编辑器把制表符换成空格的情况很常见。要从锁文件中取值,使用 jq -r '.["registry.internal/sidecar:1.0"]' /root/imgpolicy/images.lock。
让仓库回答现在是否可以提升为拦截
创建 /root/imgpolicy/audit.sh <매니페스트디렉터리> [폐기목록](占位符依次为清单目录与吊销列表)。省略吊销列表参数时,使用 /root/imgpolicy/revoked.txt。从该目录的 .yaml 和 .yml 中取出所有镜像引用,对每个没有用摘要固定的引用,输出一行 FLOATING <파일이름> <참조>(占位符依次为文件名与引用);已固定、但该摘要在吊销列表中的,输出一行 REVOKED <파일이름> <참조>(这些行要排序后输出)。最后一行恰好是 RESULT floating=<수> revoked=<수> ready=<yes|no>(占位符均为数量),只有两者都为 0 时才是 ready=yes。退出码是:ready=yes 时为 0,否则为 1。创建之后,把 ./audit.sh manifests 的输出保存到 /root/imgpolicy/audit.txt,并在该文件末尾追加一行 EXIT <종료코드>(占位符为退出码)。第 1 步中创建的那批清单就是对象。
这一步回答的问题不是“规则对不对”,而是“现在可以启用吗”。在扩大策略之前先统计会撞上什么,是推行顺序的第二格,而如果在这个数字变成 0 之前就提升到 Deny,部署就会停摆,策略也会被回退。被回退过一次的策略,通常不会再重新启用——所以统计这个数字,往往比编写规则更重要。从引用中只取出摘要,使用 ${ref#*@},与列表中是否恰好有一行相同,用 grep -qxF 来看。如果门禁总是以 0 结束,就没有人会去举报它——请用干净的一批和不干净的一批分别测试。