Kubernetes Policy Enforcement with Kyverno
How to enforce best practices and ensure compliance with Kyverno.
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.
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.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 - <<EOF
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedReposv2
metadata:
name: repo-is-cgr-dev
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- "kube-system"
parameters:
allowedImages:
- "cgr.dev/*"
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 - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx-disallowed
spec:
containers:
- name: nginx
image: nginx
EOFError from server (Forbidden): error when creating "STDIN": admission webhook "validation.gatekeeper.sh" denied the request: [repo-is-cgr-dev] container <nginx> has an invalid image <nginx>, allowed images are ["cgr.dev/*"]This example tries to create a pod using a container image downloaded from the Docker Hub registry, not Chainguard’s registry. As this output indicates, attempting to create a non-compliant pod resulted in an error, and the request was denied.
Chainguard Containers are updated frequently to incorporate CVE fixes and package updates. The tags for Chainguard’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 - <<EOF
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageDigests
metadata:
name: container-image-must-have-digest
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- "kube-system"
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 - <<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 "STDIN": admission webhook "validation.gatekeeper.sh" denied the request: [container-image-must-have-digest] container <nginx> uses an image without a digest <cgr.dev/chainguard/nginx>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.
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 <nginx> uses an image without a digest <cgr.dev/chainguard/nginx>You can also find non-compliant resources that exist in the cluster by reviewing the constraint’s violations:
kubectl get k8simagedigests container-image-must-have-digest -o json | jq -r '.status.violations[]'{
"enforcementAction": "warn",
"group": "",
"kind": "Pod",
"message": "container <nginx> uses an image without a digest <cgr.dev/chainguard/nginx>",
"name": "nginx-disallowed",
"namespace": "default",
"version": "v1"
}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.
This will require having Gatekeeper external data enabled.
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=trueThe 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+json
parameters:
trustPolicies:
- name: chainguard-public
scopes:
- "cgr.dev/chainguard/*"
tLogVerify: true
keyless:
ctLogVerify: true
certificateOIDCIssuer: "https://token.actions.githubusercontent.com"
certificateIdentity: "https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main"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 '.[].chainguard.service_bindings.CATALOG_SYNCER')
APKO_BUILDER=$(chainctl iam account-associations describe $PARENT -o json | jq -r '.[].chainguard.service_bindings.APKO_BUILDER')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+json
parameters:
trustPolicies:
- name: chainguard-private
scopes:
- "cgr.dev/${PARENT}/*"
tLogVerify: true
keyless:
ctLogVerify: true
certificateOIDCIssuer: "https://issuer.enforce.dev"
certificateIdentityRegExp: "https://issuer.enforce.dev/(${CATALOG_SYNCER}|${APKO_BUILDER})"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:
"application/vnd.dev.cosign.artifact.sig.v1+json": "any"
default: "any"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: "v1"
rego: |
package k8srequiredimagesignatures
default exemptions := []
exemptions := input.parameters.exemptImages
all_images contains val.image if {
containers := object.get(input.review.object.spec.template.spec, "containers", [])
initContainers := object.get(input.review.object.spec.template.spec, "initContainers", [])
ephContainers := object.get(input.review.object.spec.template.spec, "ephemeralContainers", [])
vals := array.concat(containers, array.concat(initContainers, ephContainers))
some val in vals
}
ratify_response(image) = resp if {
resp := external_data({
"provider": "ratify-provider",
"keys": [image],
})
}
responses contains {"image": resp[0], "data": resp[1]} if {
some image in all_images
not image in exemptions
rat_resp := ratify_response(image)
resp := rat_resp.responses[_]
}
violation contains {"msg": msg} if {
some resp in responses
resp.data.system_error != ""
msg := sprintf("image %q verification system error: %v", [resp.image, resp.data.system_error])
}
violation contains {"msg": msg} if {
some resp in responses
count(resp.data.responses) == 0
msg := sprintf("image %q returned no verification response", [resp.image])
}
violation contains {"msg": msg} if {
some resp in responses
not resp.data.isSuccess
reason := object.get(resp.data, "message", "verification failed")
msg := sprintf("image %q is not signed by Chainguard: %v", [resp.image, reason])
}
violation contains {"msg": msg} if {
some resp in responses
err := resp.data.errors[_]
err[0] == resp.image
msg := sprintf("image %q verification error: %v", [resp.image, err[1]])
}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: [""]
kinds: ["Pod"]
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet", "ReplicaSet"]
parameters:
exemptImages:
- "registry.k8s.io/pause:3.9"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.
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 "bad-deployment.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [require-signed-images] image "docker.io/library/nginx@sha256:7150b3a39203cb
5bee612ff4a9d18774f8c7caf6399d6e8985e97e28eb751c18" is not signed by Chainguard: verification failedBy 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’d like to learn more about Gatekeeper, we encourage you to refer to the official documentation.
Last updated: 2025-09-02 10:00