The tag never changed, but what came up was not yesterday's
Goal
You build an eye that tells image references apart by shape, attach a rule requiring digest pinning to admission as a warning and then raise it to a block only in a narrow place, write a lock file and resolver that rewrite tags into digests, and apply the same rule to build outputs outside the cluster.
Why it matters
A tag is a name tag that can be moved and a digest is a hash of the content and cannot be moved. Almost all of image policy is a corollary of this one sentence. A registry does not stop you from pushing a new image onto the same tag, so a team that deployed only by tag can neither reproduce the incident where "yesterday and today differ" nor escape a rollback where "the thing to go back to is gone." That is why the first line of the rule is not banning latest but digest pinning. But what pinning guarantees is only content identity — whether that content is safe, who made it, and how it was made are not in the hash, and vulnerability scanning, signatures, and provenance attestations each fill that place on a different axis. There are two pitfalls on the rule-writing side too. The container lists are not just spec.containers but three including initContainers and ephemeralContainers, and image references are not only in Pods but also in a workload's Pod template, at a path one level deeper. A policy that misses these two is bypassed even when it is on. And pinning is paired with an update procedure. The lock file is the place where that procedure lives.
Steps
-
Work in
/root/imgpolicy. In/root/imgpolicy/manifests/, create four manifests —web.yamlis a Podprobe-web(initContainersmigrateisregistry.internal/migrate:2.1, containerswebisregistry.internal/api:1.4.0),cache.yamlis a Podprobe-cache(containerscacheisregistry.internal/sidecar@sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266),batch.yamlis a Podprobe-batch(containersbatchis the taglessregistry.internal/runtime, andtailisbusybox:latest), andapi.yamlis a Deploymentprobe-api(the template containerapiisregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc). Then create/root/imgpolicy/classify.sh <디렉터리>(taking a directory as its argument). It pulls out all the image references from the.yamland.ymlfiles in that directory, sorts them without duplicates, and prints one per line as<참조> <모양>(the reference, then its shape). The shape is exactly one of four words:digest,latest,tag, andnotag. Finally, save the result with./classify.sh manifests > /root/imgpolicy/01-shapes.txt. -
Start with
export KUBECONFIG=/root/.kube/configandkubectl config use-context kwok-lab. In/root/imgpolicy/policy.yaml, write a ValidatingAdmissionPolicyrequire-image-digestofadmissionregistration.k8s.io/v1—matchConstraints.resourceRulescatchesCREATEandUPDATEonv1podsin the core group (""), the variablefloatingis the list of those images inobject.spec.containersthat lack@sha256:, and the validation issize(variables.floating) == 0with a messageExpression that contains that list. Create the namespaceimg-warnand attach the labelimage-policy=warn. In/root/imgpolicy/binding-warn.yaml, write a ValidatingAdmissionPolicyBindingimage-digest-warn—policyNameisrequire-image-digest,validationActionsis["Warn", "Audit"], andmatchResources.namespaceSelector.matchLabelsisimage-policy: warn. Create a test Pod/root/imgpolicy/pod-tag.yaml— Podprobe-tag, initContainersmigrateisregistry.internal/migrate:2.1, and containerswebisregistry.internal/api:1.4.0. After applying the policy and binding, sendpod-tag.yamltoimg-warnwith--dry-run=serverand save the output, including standard error, to/root/imgpolicy/02-warn.txt. Even though a warning appears, the Pod must be created. -
First create
/root/imgpolicy/pod-init.yaml— Podprobe-init, initContainersmigrateisregistry.internal/migrate:2.1(not pinned), and containersappisregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc. If you send this toimg-warnwith--dry-run=server, no warning appears. Then edit/root/imgpolicy/policy.yaml— make the variableallImagesa list of images that concatenatesspec.containers,spec.initContainers, andspec.ephemeralContainers(check a field that may be absent first withhas(...)), and havefloatingkeep only those without@sha256:. Also addpods/ephemeralcontainerstoresourcesinmatchConstraints. And in/root/imgpolicy/pod-pinned.yaml, create a Podprobe-pinned(containersappisregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc) and actually create it inimg-warn(kubectl apply -n img-warn -f pod-pinned.yaml). An ephemeral container cannot be attached when creating a Pod, so you send it to the subresource: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 -
After applying the fixed policy, (1) send pod-init.yaml to img-warn again and (2) send the ephemeral container request above, and save both outputs, including standard error, to /root/imgpolicy/03-lists.txt. This time both must show a warning.
4. Create two more namespaces — attach the label image-policy=enforce to img-prod, and attach no label at all to img-legacy (outside the scope). Do not delete the warning binding; leave it as it is. In /root/imgpolicy/binding-deny.yaml, write a second binding image-digest-deny — the same policyName, validationActions of ["Deny"], and a selector of image-policy: enforce. Next, add a rule for CREATE and UPDATE on deployments of apps group v1 to matchConstraints of /root/imgpolicy/policy.yaml, and put in a variable podSpec that picks object.spec.template.spec if has(object.spec.template) and object.spec otherwise. allImages is now taken from the three lists of variables.podSpec. In /root/imgpolicy/dep-tag.yaml, create a Deployment probe-dep (the template container api is registry.internal/api:1.4.0), send it to img-prod and img-legacy in turn with --dry-run=server, and save the two outputs to /root/imgpolicy/04-deny.txt. It must be rejected in img-prod and created as it is in img-legacy.
5. In /root/imgpolicy/images.lock, write the lock file as a single JSON object. The keys are references with a tag attached and the values are digests — registry.internal/api:1.4.0 is sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc, registry.internal/migrate:2.1 is sha256:3e7c7af932c7b5e39c7812b321bbbd291da943717e96860e5976ac609ef84030, registry.internal/runtime:3.20 is sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c, and registry.internal/sidecar:1.0 is sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266. Then create the resolver /root/imgpolicy/resolve.sh <매니페스트> [잠금파일] (a manifest argument and an optional lock file argument). If you omit the lock file argument, it uses /root/imgpolicy/images.lock. It reads the manifest and prints the whole manifest, with tag references rewritten as <이름>@<다이제스트> (the name at the digest), to standard output, and passes lines already pinned by digest through unchanged. For a reference not in the lock file, do not make up a digest; print LOCK MISS <참조> (with the reference) to standard error, one per line, and end with exit code 3 (print nothing to standard output then). After creating it, save the resolved result with ./resolve.sh manifests/web.yaml > /root/imgpolicy/resolved/web.yaml, save the standard error of ./resolve.sh manifests/batch.yaml to /root/imgpolicy/05-miss.txt, and then append one line EXIT <종료코드> (with the exit code) to the end of that file.
6. Write /root/imgpolicy/Dockerfile — the first stage is FROM registry.internal/builder:3.2 AS build, and the second stage is FROM registry.internal/runtime@sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c. In /root/imgpolicy/build.json, write the list of build outputs. There are three keys — build (the string api-2026-09-17), base_images (an array of strings holding the Dockerfile's FROM references in the order written), and artifacts (an array of objects with name and image: api is registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc and worker is registry.internal/worker:2.0). In /root/imgpolicy/policies/image-refs.yaml, write a policy with apiVersion: json.kyverno.io/v1alpha1 · kind: ValidatingPolicy. There are two rules — base-pinned requires that the number of base_images lacking @sha256: be 0, and artifact-pinned requires that the number of artifacts[].image lacking @sha256: be 0. Then create the gate /root/imgpolicy/jsongate.sh <페이로드> [정책] (a payload argument and an optional policy argument; if you omit the policy argument, /root/imgpolicy/policies/image-refs.yaml). It parses the human-readable output of kyverno json scan, prints FAIL <그 줄> (with that line) for each violation line, and at the end prints RESULT failed=<개수> (with the count) and then ends with exit code 1 if there are violations and 0 if not. If there is no decision line (PASSED, FAILED, ERROR:) at all, do not treat it as a pass but end with exit code 2. Finally, create /root/imgpolicy/06-gate.txt — the first line is SCAN EXIT <코드> recording the exit code when the policy is run as it is (with the code), below it the output of ./jsongate.sh build.json, and the last line is GATE EXIT <코드> (with the code).
7. What a digest guarantees is only content identity. Mechanisms on other axes fill the three things it does not guarantee. Write exactly three lines in /root/imgpolicy/limits.tsv. Each line has two fields separated by one tab; the first field is each of content-safety, author, and build-path once, and the second field is the suitable one of scan, signature, and provenance as the mechanism that fills that place (write the first fields in that order). And in /root/imgpolicy/revoked.txt, write revoked digests, one per line — for now there is just one line. The digest that registry.internal/sidecar:1.0 points to has turned out to be vulnerable, so find that value in /root/imgpolicy/images.lock and write it as it is (do not write the name before @; write only the value that starts with sha256:).
8. Create /root/imgpolicy/audit.sh <매니페스트디렉터리> [폐기목록] (a manifest directory argument and an optional revocation list argument). If you omit the revocation list argument, it uses /root/imgpolicy/revoked.txt. It pulls out all the image references from the .yaml and .yml files in that directory and prints one line each: FLOATING <파일이름> <참조> (the file name, then the reference) for every reference not pinned by digest, and REVOKED <파일이름> <참조> for one that is pinned but whose digest is in the revocation list (print those lines sorted). The last line is exactly RESULT floating=<수> revoked=<수> ready=<yes|no> (each placeholder a count), and ready=yes only when both are 0. The exit code is 0 if ready=yes and 1 otherwise. After creating it, save the output of ./audit.sh manifests to /root/imgpolicy/audit.txt and append one line EXIT <종료코드> (with the exit code) to the end of that file. The bundle of manifests you made in step 1 is the target as it is.
Notes
- You start with
export KUBECONFIG=/root/.kube/configandkubectl config use-context kwok-lab. It is a real kube-apiserver v1.30.4 that kwok runs inside the Pod, and you keep all outputs under/root/imgpolicy. - kwok does not actually run Pods. All this lab looks at is the admission stage, so
kubectl create -f <파일> --dry-run=server(with a file name in place of the placeholder) is enough — it runs admission as it is and leaves no object. - There is no runtime that can actually download images. So you handle the correspondence between tags and digests with a lock file (a tag to digest table) that you build yourself. It has the same shape as an image lock file in practice.
- This image has no
cosign,skopeo,conftest, oropa, and no internet either. So you cannot actually run signature verification and provenance attestation, and in step 7 they are covered only as concepts and a distinction of axes. Checks outside the cluster are done withkyverno json scan. - Right after you change a policy or binding, it is reflected a round trip or two late. If the result looks like the old one, send it again a few seconds later.
- Common mistake: looking only at
spec.containers. IfinitContainersandephemeralContainersremain, the policy is bypassed even while it is on. - Common mistake: deciding whether there is a tag by
:in the whole reference.registry.internal:5000/team/appis wrongly classified as a reference with a tag. - Common mistake: trusting the exit code of
kyverno json scan. It ends with 0 even when there are violations — you confirm it by printing it yourself in step 6. - Kubernetes images · Validating Admission Policy · CEL in Kubernetes · OCI descriptor · OCI distribution spec · SLSA provenance · kyverno-json
Four shapes of image reference are in the same bundle
Work in /root/imgpolicy. In /root/imgpolicy/manifests/, create four manifests — web.yaml is a Pod probe-web (initContainers migrate is registry.internal/migrate:2.1, containers web is registry.internal/api:1.4.0), cache.yaml is a Pod probe-cache (containers cache is registry.internal/sidecar@sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266), batch.yaml is a Pod probe-batch (containers batch is the tagless registry.internal/runtime, and tail is busybox:latest), and api.yaml is a Deployment probe-api (the template container api is registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc). Then create /root/imgpolicy/classify.sh <디렉터리> (taking a directory as its argument). It pulls out all the image references from the .yaml and .yml files in that directory, sorts them without duplicates, and prints one per line as <참조> <모양> (the reference, then its shape). The shape is exactly one of four words: digest, latest, tag, and notag. Finally, save the result with ./classify.sh manifests > /root/imgpolicy/01-shapes.txt.
An image reference is <레지스트리>/<이름>[:태그][@sha256:...] (registry, name, optional tag, and optional digest). If a digest is attached, only the digest is used for pulling even when a tag is also there, so treat it as digest. If you decide whether there is a tag by looking for : in the whole reference, a reference whose host has a port attached, such as registry.internal:5000/team/app, is wrongly seen as having a tag — decide by looking only at the part after the last /. In the shell you get the last piece with ${ref##*/}.
Attach a rule requiring digests as a warning
Start with export KUBECONFIG=/root/.kube/config and kubectl config use-context kwok-lab. In /root/imgpolicy/policy.yaml, write a ValidatingAdmissionPolicy require-image-digest of admissionregistration.k8s.io/v1 — matchConstraints.resourceRules catches CREATE and UPDATE on v1 pods in the core group (""), the variable floating is the list of those images in object.spec.containers that lack @sha256:, and the validation is size(variables.floating) == 0 with a messageExpression that contains that list. Create the namespace img-warn and attach the label image-policy=warn. In /root/imgpolicy/binding-warn.yaml, write a ValidatingAdmissionPolicyBinding image-digest-warn — policyName is require-image-digest, validationActions is ["Warn", "Audit"], and matchResources.namespaceSelector.matchLabels is image-policy: warn. Create a test Pod /root/imgpolicy/pod-tag.yaml — Pod probe-tag, initContainers migrate is registry.internal/migrate:2.1, and containers web is registry.internal/api:1.4.0. After applying the policy and binding, send pod-tag.yaml to img-warn with --dry-run=server and save the output, including standard error, to /root/imgpolicy/02-warn.txt. Even though a warning appears, the Pod must be created.
The policy decides only "what to judge," and the binding decides "where and with what strength." Warn only returns a warning through the response header and lets the request pass. The reason to put the first stage of the rollout in Warn is to count what gets caught first. CEL strings have contains, startsWith, and endsWith, and lists have map, filter, and join. Warnings come out on standard error, so you must add 2>&1 for them to be captured in the file too. kubectl create -f <파일> --dry-run=server (with a file name in place of the placeholder) runs admission as it is but leaves no object. The rejection message and the warning come out just like a real request.
Look at only one container list and it is bypassed as it is
First create /root/imgpolicy/pod-init.yaml — Pod probe-init, initContainers migrate is registry.internal/migrate:2.1 (not pinned), and containers app is registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc. If you send this to img-warn with --dry-run=server, no warning appears. Then edit /root/imgpolicy/policy.yaml — make the variable allImages a list of images that concatenates spec.containers, spec.initContainers, and spec.ephemeralContainers (check a field that may be absent first with has(...)), and have floating keep only those without @sha256:. Also add pods/ephemeralcontainers to resources in matchConstraints. And in /root/imgpolicy/pod-pinned.yaml, create a Pod probe-pinned (containers app is registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc) and actually create it in img-warn (kubectl apply -n img-warn -f pod-pinned.yaml). An ephemeral container cannot be attached when creating a Pod, so you send it to the subresource:
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 -
After applying the fixed policy, (1) send pod-init.yaml to img-warn again and (2) send the ephemeral container request above, and save both outputs, including standard error, to /root/imgpolicy/03-lists.txt. This time both must show a warning.
A Pod has three container lists — containers, initContainers, and ephemeralContainers. The most common policy hole in practice is looking only at the first list. In CEL you can concatenate lists with +, and you wrap a field that may be absent in a ternary of the form has(x) ? x : []. ephemeralContainers cannot be set on Pod CREATE (Forbidden: cannot be set on create) and comes in only through the pods/ephemeralcontainers subresource — so you must write that subresource in the policy's resources for the same rule to see paths such as kubectl debug. Because ?dryRun=All is attached, this request receives only the decision and leaves nothing on the Pod.
Turn blocking on not broadly but only in a narrow place
Create two more namespaces — attach the label image-policy=enforce to img-prod, and attach no label at all to img-legacy (outside the scope). Do not delete the warning binding; leave it as it is. In /root/imgpolicy/binding-deny.yaml, write a second binding image-digest-deny — the same policyName, validationActions of ["Deny"], and a selector of image-policy: enforce. Next, add a rule for CREATE and UPDATE on deployments of apps group v1 to matchConstraints of /root/imgpolicy/policy.yaml, and put in a variable podSpec that picks object.spec.template.spec if has(object.spec.template) and object.spec otherwise. allImages is now taken from the three lists of variables.podSpec. In /root/imgpolicy/dep-tag.yaml, create a Deployment probe-dep (the template container api is registry.internal/api:1.4.0), send it to img-prod and img-legacy in turn with --dry-run=server, and save the two outputs to /root/imgpolicy/04-deny.txt. It must be rejected in img-prod and created as it is in img-legacy.
You can attach several bindings to one policy, and each binding has its own scope and its own strength. So instead of "cast broadly and dig out exceptions," the rollout becomes "cast narrowly and widen" — an exception list is something nobody deletes as time passes, but a narrow scope needs a decision each time you widen it. Image references are not only in Pods. Deployments, StatefulSets, and Jobs hold the same string inside the Pod template, with the path one level deeper. A policy that catches only Pods ends up blocking the Pods controllers create, but by then the deployment has already started, so it is hard for a person to find the cause. If you put the two shapes into one variable with a CEL ternary, you can keep the validation as one.
Write tags as digests and fix using only that
In /root/imgpolicy/images.lock, write the lock file as a single JSON object. The keys are references with a tag attached and the values are digests — registry.internal/api:1.4.0 is sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc, registry.internal/migrate:2.1 is sha256:3e7c7af932c7b5e39c7812b321bbbd291da943717e96860e5976ac609ef84030, registry.internal/runtime:3.20 is sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c, and registry.internal/sidecar:1.0 is sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266. Then create the resolver /root/imgpolicy/resolve.sh <매니페스트> [잠금파일] (a manifest argument and an optional lock file argument). If you omit the lock file argument, it uses /root/imgpolicy/images.lock. It reads the manifest and prints the whole manifest, with tag references rewritten as <이름>@<다이제스트> (the name at the digest), to standard output, and passes lines already pinned by digest through unchanged. For a reference not in the lock file, do not make up a digest; print LOCK MISS <참조> (with the reference) to standard error, one per line, and end with exit code 3 (print nothing to standard output then). After creating it, save the resolved result with ./resolve.sh manifests/web.yaml > /root/imgpolicy/resolved/web.yaml, save the standard error of ./resolve.sh manifests/batch.yaml to /root/imgpolicy/05-miss.txt, and then append one line EXIT <종료코드> (with the exit code) to the end of that file.
The lock file is a table in which "what this tag points to right now" is written so people can read it. You deploy by digest and update by editing this table — pinning and updating are a pair. You can safely look up one key with jq -r --arg k "$ref" '.[$k] // empty' <파일> (with the lock file in place of the placeholder). To strip the tag from a reference, you must delete from the : after the last / — sed 's/:[^:/]*$//' does not touch the host port. Whether the resolver really reads the lock file shows immediately if you give it a different lock file. The grader of this lab does exactly that.
Apply the same rule to build outputs outside the cluster too
Write /root/imgpolicy/Dockerfile — the first stage is FROM registry.internal/builder:3.2 AS build, and the second stage is FROM registry.internal/runtime@sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c. In /root/imgpolicy/build.json, write the list of build outputs. There are three keys — build (the string api-2026-09-17), base_images (an array of strings holding the Dockerfile's FROM references in the order written), and artifacts (an array of objects with name and image: api is registry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc and worker is registry.internal/worker:2.0). In /root/imgpolicy/policies/image-refs.yaml, write a policy with apiVersion: json.kyverno.io/v1alpha1 · kind: ValidatingPolicy. There are two rules — base-pinned requires that the number of base_images lacking @sha256: be 0, and artifact-pinned requires that the number of artifacts[].image lacking @sha256: be 0. Then create the gate /root/imgpolicy/jsongate.sh <페이로드> [정책] (a payload argument and an optional policy argument; if you omit the policy argument, /root/imgpolicy/policies/image-refs.yaml). It parses the human-readable output of kyverno json scan, prints FAIL <그 줄> (with that line) for each violation line, and at the end prints RESULT failed=<개수> (with the count) and then ends with exit code 1 if there are violations and 0 if not. If there is no decision line (PASSED, FAILED, ERROR:) at all, do not treat it as a pass but end with exit code 2. Finally, create /root/imgpolicy/06-gate.txt — the first line is SCAN EXIT <코드> recording the exit code when the policy is run as it is (with the code), below it the output of ./jsongate.sh build.json, and the last line is GATE EXIT <코드> (with the code).
The scan is KYVERNO_EXPERIMENTAL=true kyverno json scan --payload <JSON> --policy <YAML> (with a payload and a policy in place of the placeholders) — if you leave out the environment variable, the command itself is rejected because it is experimental. This tool has an exit code of 0 even when it finds violations, and the violations are not recorded in --output json either. So a gate written as if kyverno json scan ...; then always passes — printing it yourself in this step is to confirm exactly that. The key of spec.rules[].assert.all[].check is a JMESPath expression wrapped in parentheses and the value is the expected value. In a JMESPath filter, "does not contain" is picked by comparing the result of contains with false — JSON literals are wrapped in backticks, so both strings and the false value need backticks. To test an expression on its own, kyverno jp query -i <파일> '<식>' (with a file and an expression in place of the placeholders) is the fastest. Admission is the last line of defense, and this is a place where people can fix things in a PR — that is why you put the same rule on both sides.
Pinning decides identity, not safety
What a digest guarantees is only content identity. Mechanisms on other axes fill the three things it does not guarantee. Write exactly three lines in /root/imgpolicy/limits.tsv. Each line has two fields separated by one tab; the first field is each of content-safety, author, and build-path once, and the second field is the suitable one of scan, signature, and provenance as the mechanism that fills that place (write the first fields in that order). And in /root/imgpolicy/revoked.txt, write revoked digests, one per line — for now there is just one line. The digest that registry.internal/sidecar:1.0 points to has turned out to be vulnerable, so find that value in /root/imgpolicy/images.lock and write it as it is (do not write the name before @; write only the value that starts with sha256:).
If you pull the same digest twice, you get the same bytes. That is all — whether those bytes are safe, who made them, and what pipeline made them from what source are not in the hash. Vulnerability scanning answers "is the content safe," a signature answers "who vouches," and a provenance attestation (SLSA provenance) answers "how was it made." The three axes cannot substitute for one another, and all three lean on the digest for what to point to. A revocation list is a fourth axis — if you have done the pinning well, "pointing at that one and blocking it" becomes possible. It is safest to insert a tab character with printf 'a\tb\n'. It is common for an editor to turn tabs into spaces. To pull a value out of the lock file, use jq -r '.["registry.internal/sidecar:1.0"]' /root/imgpolicy/images.lock.
Let the repository answer whether it is okay to raise to blocking now
Create /root/imgpolicy/audit.sh <매니페스트디렉터리> [폐기목록] (a manifest directory argument and an optional revocation list argument). If you omit the revocation list argument, it uses /root/imgpolicy/revoked.txt. It pulls out all the image references from the .yaml and .yml files in that directory and prints one line each: FLOATING <파일이름> <참조> (the file name, then the reference) for every reference not pinned by digest, and REVOKED <파일이름> <참조> for one that is pinned but whose digest is in the revocation list (print those lines sorted). The last line is exactly RESULT floating=<수> revoked=<수> ready=<yes|no> (each placeholder a count), and ready=yes only when both are 0. The exit code is 0 if ready=yes and 1 otherwise. After creating it, save the output of ./audit.sh manifests to /root/imgpolicy/audit.txt and append one line EXIT <종료코드> (with the exit code) to the end of that file. The bundle of manifests you made in step 1 is the target as it is.
The question this step answers is not "is the rule right" but "is it okay to turn it on now." Counting what will get caught before widening the policy is the second slot of the rollout order, and if you raise to Deny before that number reaches 0, deployments stop and the policy is rolled back. A policy that has been rolled back once usually does not get turned on again — so counting this number is often more important than writing the rule. To strip only the digest from a reference, use ${ref#*@}, and check whether it is exactly equal to one line of the list with grep -qxF. If a gate always ends with 0, nobody reports it — test it with both a clean bundle and a dirty bundle.