Write a ValidatingAdmissionPolicy whose CEL expression checks that every container image contains an at sign, which marks a digest reference, bind it in Warn and Audit mode first, then switch to Deny. The Kubernetes policy API has been stable since version 1.30, so no webhook is needed.
Why digests
The Kubernetes documentation says a digest identifies a specific version of an image and is immutable, while tags can be moved. With a digest, the cluster runs the same code each time. A reference can carry both: registry.example.com/app:1.4@sha256:..., and only the digest is used to pull.
The policy
This example covers pods and checks regular containers. Test it on your cluster version before relying on it.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-image-digest.example.com
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "object.spec.containers.all(c, c.image.contains('@sha256:'))"
message: "All container images must be pinned by sha256 digest."
Add a second validation for init containers, using has(object.spec.initContainers) to guard against the field being absent. Without it, init containers and sidecars escape the rule.
The binding
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-image-digest-binding
spec:
policyName: require-image-digest.example.com
validationActions: [Warn, Audit]
matchResources:
namespaceSelector:
matchLabels:
environment: staging
Per the documentation, validationActions accepts Deny, Warn and Audit, and Deny and Warn should not be combined.
Rollout steps
- Apply with Warn and Audit in one namespace.
- Read the audit events and count violations.
- Change deployment templates to resolve tags to digests at release time.
- Widen the namespace selector.
- Switch to Deny, with system namespaces excluded.
Testing before you enforce
Create a test pod with a tag-only image and confirm you see a warning with the policy message. Create one with a digest and confirm it passes. Then create a pod with a digest in the main container and a tag in an init container, and check that the second validation catches it. Run these as a small script in CI against a scratch cluster, so a Kubernetes upgrade cannot silently change behavior.
Also check how the rule behaves for ephemeral containers and for pods created by Jobs and CronJobs. Controllers create pods from templates, so the rule runs at pod creation, not when you apply the Deployment. A failing rule may therefore show up as a stalled rollout rather than a clear error at apply time. Read the events on the ReplicaSet when that happens.
Cases to think about
Pods created by controllers carry the image from the template, so the policy evaluates pod creation, not just the Deployment. Cluster add-ons often use tags; exclude their namespaces or fix them separately. Do the rule check on registry too if you want, by requiring a prefix.
Remember a digest alone does not make an image safe. It makes it repeatable. Pair it with scanning. Our post on tags vs digests explains the tradeoff.
Quick check
Does a digest rule stop a vulnerable image?
No. It only guarantees the image cannot silently change. You still need scanning and rebuilds.
Do I need a webhook for this?
No. The in-process policy API handles it. Webhooks are for logic that needs external data.
Safeguard offers policies, gates and guardrails for what is allowed to ship; try the free scan to see your inventory.
Sources
Last reviewed: 19 October 2026.