# Kubernetes Policy Enforcement with OPA Gatekeeper

URL: https://edu.chainguard.dev/chainguard/chainguard-images/staying-secure/enforcement/opa-gatekeeper.md
Last Modified: September 2, 2025
Tags: Chainguard Containers, Overview, Policy

How to enforce best practices and ensure compliance with OPA Gatekeeper.

Gatekeeper is an admission controller that enforces policies in Kubernetes clusters. This article describes how it can be leveraged to ensure resources follow best practices related to the use of Chainguard Containers.
Prerequisites To follow the examples in this guide, you will need the following:
kubectl — the command line interface tool for Kubernetes — installed on your local machine. Administrative access to a Kubernetes cluster where OPA Gatekeeper is already installed. Ensure Images are Pulled From Allowed Repositories You can use the K8sAllowedReposV2 constraint from the Gatekeeper Library to ensure that images are only pulled from a list of allowed repositories.
To configure this constraint, add the constraint template to your cluster.
kubectl create -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/refs/heads/master/library/general/allowedreposv2/template.yamlThen, create a constraint that only allows images hosted in cgr.dev. Note that if you are mirroring Chainguard container images into a different registry, then you could replace this with your own URLs:
kubectl create -f - &lt;&lt;EOF apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sAllowedReposv2 metadata: name: repo-is-cgr-dev spec: match: kinds: - apiGroups: [&#34;&#34;] kinds: [&#34;Pod&#34;] excludedNamespaces: - &#34;kube-system&#34; parameters: allowedImages: - &#34;cgr.dev/*&#34; EOFNote that you may not be able to control where images provided by the platform are hosted in certain managed Kubernetes solutions.
To test that this constraint is working correctly, try to create a non-compliant pod:
kubectl create -f - &lt;&lt;EOF apiVersion: v1 kind: Pod metadata: name: nginx-disallowed spec: containers: - name: nginx image: nginx EOFError from server (Forbidden): error when creating &#34;STDIN&#34;: admission webhook &#34;validation.gatekeeper.sh&#34; denied the request: [repo-is-cgr-dev] container &lt;nginx&gt; has an invalid image &lt;nginx&gt;, allowed images are [&#34;cgr.dev/*&#34;]This example tries to create a pod using a container image downloaded from the Docker Hub registry, not Chainguard&rsquo;s registry. As this output indicates, attempting to create a non-compliant pod resulted in an error, and the request was denied.
Ensure Images are Referenced By Digest Chainguard Containers are updated frequently to incorporate CVE fixes and package updates. The tags for Chainguard&rsquo;s container images are highly mutable, meaning that the underlying image changes frequently, even for very specific tags like v1.2.3-r1.
To prevent the risk of updates introducing breaking changes, you can pull by digest to ensure the use of a specific image.
The K8sImageDigests constraint from the Gatekeeper Library can be used to mandate this practice inside a Kubernetes cluster.
Add the template to your cluster:
kubectl create -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/refs/heads/master/library/general/imagedigests/template.yamlThen create the constraint:
kubectl create -f - &lt;&lt;EOF apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sImageDigests metadata: name: container-image-must-have-digest spec: match: kinds: - apiGroups: [&#34;&#34;] kinds: [&#34;Pod&#34;] excludedNamespaces: - &#34;kube-system&#34; EOFBe aware that in certain managed Kubernetes solutions, you may not be able to control whether images provided by the platform are referenced by digest.
To test the constraint, try to create a non-compliant pod:
kubectl create -f - &lt;&lt;EOF apiVersion: v1 kind: Pod metadata: name: nginx-disallowed spec: containers: - name: nginx image: cgr.dev/chainguard/nginx EOFError from server (Forbidden): error when creating &#34;STDIN&#34;: admission webhook &#34;validation.gatekeeper.sh&#34; denied the request: [container-image-must-have-digest] container &lt;nginx&gt; uses an image without a digest &lt;cgr.dev/chainguard/nginx&gt;This example attempts to create a pod using the nginx Chainguard container image, but does not pull the image by its digest as required by the constraint. As the output indicates, the attempt resulted in an error and the request was denied.
Warn First, Deny Later When introducing new constraints into a cluster, it is a good idea to initially configure them with enforcementAction: warn so as to avoid blocking existing workloads.
This way, when a user creates a non-compliant resource, they will get a warning like the following example. This gives the user a signal that they should update their configuration.
Warning: [container-image-must-have-digest] container &lt;nginx&gt; uses an image without a digest &lt;cgr.dev/chainguard/nginx&gt;You can also find non-compliant resources that exist in the cluster by reviewing the constraint&rsquo;s violations:
kubectl get k8simagedigests container-image-must-have-digest -o json | jq -r &#39;.status.violations[]&#39;{ &#34;enforcementAction&#34;: &#34;warn&#34;, &#34;group&#34;: &#34;&#34;, &#34;kind&#34;: &#34;Pod&#34;, &#34;message&#34;: &#34;container &lt;nginx&gt; uses an image without a digest &lt;cgr.dev/chainguard/nginx&gt;&#34;, &#34;name&#34;: &#34;nginx-disallowed&#34;, &#34;namespace&#34;: &#34;default&#34;, &#34;version&#34;: &#34;v1&#34; }Once all the violations have been addressed, you can remove enforcementAction: warn and Gatekeeper will start to block the creation of resources that violate the constraint.
Ensure Images Are Signed By Chainguard This will require having Gatekeeper external data enabled.
Install Ratify helm repo add ratify https://notaryproject.github.io/ratify # download the notary verification certificate curl -sSLO https://raw.githubusercontent.com/deislabs/ratify/main/test/testdata/notation.crt helm install ratify \ ratify/ratify \ --namespace gatekeeper-system \ --set-file notationCerts={./notation.crt} \ --set featureFlags.RATIFY_CERT_ROTATION=true \ --set policy.useRego=true Create a Verifier The verifier will set up the cosign verification. This example uses the public Chainguard images.
Note: If you want to use a private registry, you can follow the identity patterns laid out here
apiVersion: config.ratify.deislabs.io/v1beta1 kind: Verifier metadata: name: verifier-cosign-chainguard spec: name: cosign artifactTypes: application/vnd.dev.cosign.artifact.sig.v1&#43;json parameters: trustPolicies: - name: chainguard-public scopes: - &#34;cgr.dev/chainguard/*&#34; tLogVerify: true keyless: ctLogVerify: true certificateOIDCIssuer: &#34;https://token.actions.githubusercontent.com&#34; certificateIdentity: &#34;https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main&#34;To use a private registry, you need a couple more steps.
First set a variable for your org
PARENT=your-organizationNext, create two more variables to hold the UIDPs of your organization’s catalog_syncer and apko_builder identities, respectively:
CATALOG_SYNCER=$(chainctl iam account-associations describe $PARENT -o json | jq -r &#39;.[].chainguard.service_bindings.CATALOG_SYNCER&#39;) APKO_BUILDER=$(chainctl iam account-associations describe $PARENT -o json | jq -r &#39;.[].chainguard.service_bindings.APKO_BUILDER&#39;)Then your verifier would use those variables for your certificateOIDCIssuer and certificateIdentityRegexp. Substitute ${PARENT}, ${CATALOG_SYNCER} and ${APKO_BUILDER} with the literal values.
apiVersion: config.ratify.deislabs.io/v1beta1 kind: Verifier metadata: name: verifier-cosign-chainguard spec: name: cosign artifactTypes: application/vnd.dev.cosign.artifact.sig.v1&#43;json parameters: trustPolicies: - name: chainguard-private scopes: - &#34;cgr.dev/${PARENT}/*&#34; tLogVerify: true keyless: ctLogVerify: true certificateOIDCIssuer: &#34;https://issuer.enforce.dev&#34; certificateIdentityRegExp: &#34;https://issuer.enforce.dev/(${CATALOG_SYNCER}|${APKO_BUILDER})&#34; Create the Policy Create the Ratify policy. This defines a policy evaluating the verification results for a subject.
apiVersion: config.ratify.deislabs.io/v1beta1 kind: Policy metadata: name: ratify-policy spec: type: config-policy parameters: artifactVerificationPolicies: &#34;application/vnd.dev.cosign.artifact.sig.v1&#43;json&#34;: &#34;any&#34; default: &#34;any&#34; Create the Constraint Template By default, a Gatekeeper constraint template uses Rego v0 syntax. This enables v1 syntax. If you want to use v0 syntax the policy will need to be updated. This example also adds an exempt images array to allow specific non-signed images.
apiVersion: templates.gatekeeper.sh/v1 kind: ConstraintTemplate metadata: name: k8srequiredimagesignatures spec: crd: spec: names: kind: K8sRequiredImageSignatures validation: openAPIV3Schema: type: object properties: exemptImages: type: array items: type: string targets: - target: admission.k8s.gatekeeper.sh code: - engine: Rego source: version: &#34;v1&#34; rego: | package k8srequiredimagesignatures default exemptions := [] exemptions := input.parameters.exemptImages all_images contains val.image if { containers := object.get(input.review.object.spec.template.spec, &#34;containers&#34;, []) initContainers := object.get(input.review.object.spec.template.spec, &#34;initContainers&#34;, []) ephContainers := object.get(input.review.object.spec.template.spec, &#34;ephemeralContainers&#34;, []) vals := array.concat(containers, array.concat(initContainers, ephContainers)) some val in vals } ratify_response(image) = resp if { resp := external_data({ &#34;provider&#34;: &#34;ratify-provider&#34;, &#34;keys&#34;: [image], }) } responses contains {&#34;image&#34;: resp[0], &#34;data&#34;: resp[1]} if { some image in all_images not image in exemptions rat_resp := ratify_response(image) resp := rat_resp.responses[_] } violation contains {&#34;msg&#34;: msg} if { some resp in responses resp.data.system_error != &#34;&#34; msg := sprintf(&#34;image %q verification system error: %v&#34;, [resp.image, resp.data.system_error]) } violation contains {&#34;msg&#34;: msg} if { some resp in responses count(resp.data.responses) == 0 msg := sprintf(&#34;image %q returned no verification response&#34;, [resp.image]) } violation contains {&#34;msg&#34;: msg} if { some resp in responses not resp.data.isSuccess reason := object.get(resp.data, &#34;message&#34;, &#34;verification failed&#34;) msg := sprintf(&#34;image %q is not signed by Chainguard: %v&#34;, [resp.image, reason]) } violation contains {&#34;msg&#34;: msg} if { some resp in responses err := resp.data.errors[_] err[0] == resp.image msg := sprintf(&#34;image %q verification error: %v&#34;, [resp.image, err[1]]) } Create the Constraint The constraint ties the constraint template to the Kubernetes kinds you define.
apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sRequiredImageSignatures metadata: name: require-signed-images spec: match: kinds: - apiGroups: [&#34;&#34;] kinds: [&#34;Pod&#34;] - apiGroups: [&#34;apps&#34;] kinds: [&#34;Deployment&#34;, &#34;StatefulSet&#34;, &#34;DaemonSet&#34;, &#34;ReplicaSet&#34;] parameters: exemptImages: - &#34;registry.k8s.io/pause:3.9&#34; Try Deploying a Chainguard Image apiVersion: apps/v1 kind: Deployment metadata: name: nginx-chainguard spec: replicas: 1 selector: matchLabels: app: nginx-chainguard template: metadata: labels: app: nginx-chainguard spec: containers: - name: nginx image: cgr.dev/chainguard/nginx:latest ports: - containerPort: 8080This should successfully create a deployment.
Try Deploying a non-Chainguard Image apiVersion: apps/v1 kind: Deployment metadata: name: nginx-unsigned spec: replicas: 1 selector: matchLabels: app: nginx-unsigned template: metadata: labels: app: nginx-unsigned spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80You will get an error like this explaining that the deployment was blocked because the image was not signed by Chainguard.
Error from server (Forbidden): error when creating &#34;bad-deployment.yaml&#34;: admission webhook &#34;validation.gatekeeper.sh&#34; denied the request: [require-signed-images] image &#34;docker.io/library/nginx@sha256:7150b3a39203cb 5bee612ff4a9d18774f8c7caf6399d6e8985e97e28eb751c18&#34; is not signed by Chainguard: verification failed Learn More By combining OPA Gatekeeper with Chainguard container images, you gain a powerful way to enforce security and compliance across your Kubernetes clusters. Gatekeeper ensures that only container images meeting your defined policies are deployed, while Chainguard Containers provide a minimal, hardened foundation to reduce risk from the start. Together, they help teams ship software more securely and confidently, without slowing down development.
If you&rsquo;d like to learn more about Gatekeeper, we encourage you to refer to the official documentation.

