Objective
Kyverno operates as a Kubernetes admission webhook and can validate, mutate, generate, and verify resources before they are persisted. This exercise implements three rules that represent the most common platform engineering use cases: enforcing resource limits, auto-injecting missing labels, and blocking root containers. Understanding the distinction between validate (reject), mutate (fix silently), and generate (create related resources) is essential for designing a policy-as-code strategy.
Prerequisites
- A running Kubernetes cluster (kind, minikube, or cloud-managed)
- kubectl configured and targeting the cluster
- Helm 3 installed
- Basic understanding of Kubernetes Deployments and admission webhooks
Steps
Install Kyverno via Helm
# Add the Kyverno Helm repo helm repo add kyverno https://kyverno.github.io/kyverno/ helm repo update # Install Kyverno in its own namespace helm install kyverno kyverno/kyverno \ --namespace kyverno \ --create-namespace \ --set replicaCount=1 # use 3 for production HA # Wait for Kyverno to be ready kubectl -n kyverno rollout status deployment/kyverno-admission-controller # Confirm the webhook is registered kubectl get validatingwebhookconfigurations | grep kyverno kubectl get mutatingwebhookconfigurations | grep kyverno
Policy 1 — Validate: require resource requests and limits
This validating policy blocks any Deployment whose containers are missing CPU/memory requests or limits. It fires at admission time — the Deployment is rejected before it reaches etcd.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
annotations:
policies.kyverno.io/title: Require Resource Limits
policies.kyverno.io/description: >
All containers must declare CPU and memory requests and limits
to prevent resource contention and enable accurate scheduling.
spec:
validationFailureAction: Enforce # Audit = log only, Enforce = block
background: true # also scan existing resources
rules:
- name: check-resource-limits
match:
any:
- resources:
kinds: [Deployment]
validate:
message: "All containers must have CPU and memory requests and limits."
foreach:
- list: "request.object.spec.template.spec.containers"
deny:
conditions:
any:
- key: "{{ element.resources.requests.cpu }}"
operator: Equals
value: ""
- key: "{{ element.resources.requests.memory }}"
operator: Equals
value: ""
- key: "{{ element.resources.limits.cpu }}"
operator: Equals
value: ""
- key: "{{ element.resources.limits.memory }}"
operator: Equals
value: ""kubectl apply -f policies/require-resource-limits.yaml # Verify the policy is ready kubectl get clusterpolicy require-resource-limits ## NAME ADMISSION BACKGROUND VALIDATE ACTION READY ## require-resource-limits true true Enforce True
Test Policy 1 — Reject a non-conformant Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: bad-deploy
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: bad-deploy
template:
metadata:
labels:
app: bad-deploy
spec:
containers:
- name: app
image: nginx:1.25
# No resources block — should be rejectedkubectl apply -f test/deploy-no-limits.yaml ## Error from server: error when creating "test/deploy-no-limits.yaml": ## admission webhook "validate.kyverno.svc-fail" denied the request: ## resource Deployment/default/bad-deploy was blocked due to the following policies ## require-resource-limits: ## check-resource-limits: All containers must have CPU and memory requests and limits.
Policy 2 — Mutate: inject missing app.kubernetes.io/name label
This mutating policy silently adds the standard app.kubernetes.io/name label to any Deployment that omits it, deriving the value from the Deployment's own name. Mutations run before validation, so downstream policies can assume the label exists.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-app-name-label
annotations:
policies.kyverno.io/title: Add App Name Label
policies.kyverno.io/description: >
Injects app.kubernetes.io/name label from Deployment name
when the label is absent.
spec:
rules:
- name: add-label-from-name
match:
any:
- resources:
kinds: [Deployment]
mutate:
patchStrategicMerge:
metadata:
labels:
+(app.kubernetes.io/name): "{{ request.object.metadata.name }}"
spec:
template:
metadata:
labels:
+(app.kubernetes.io/name): "{{ request.object.metadata.name }}"+(key) syntax means "add only if key is absent" — it will not overwrite an explicitly set label. This is the correct pattern for default injection.kubectl apply -f policies/add-app-name-label.yaml # Deploy without the label kubectl create deployment no-label-deploy --image=nginx:1.25 \ --replicas=1 -- /bin/sh -c "sleep 3600" 2>/dev/null || true # But first add resources so it passes the first policy kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: no-label-deploy namespace: default spec: replicas: 1 selector: matchLabels: app: no-label-deploy template: metadata: labels: app: no-label-deploy spec: containers: - name: app image: nginx:1.25 resources: requests: { cpu: "100m", memory: "128Mi" } limits: { cpu: "200m", memory: "256Mi" } EOF # Verify the label was injected kubectl get deployment no-label-deploy -o jsonpath='{.metadata.labels}' ## {"app":"no-label-deploy","app.kubernetes.io/name":"no-label-deploy"} kubectl get deployment no-label-deploy \ -o jsonpath='{.spec.template.metadata.labels}' ## {"app":"no-label-deploy","app.kubernetes.io/name":"no-label-deploy"}
Policy 3 — Validate: block containers running as UID 0 (root)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-root-containers
annotations:
policies.kyverno.io/title: Block Root Containers
policies.kyverno.io/description: >
Containers must not run as root (UID 0). Either securityContext.runAsNonRoot
must be true or runAsUser must be > 0.
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-runasnonroot
match:
any:
- resources:
kinds: [Deployment, StatefulSet, DaemonSet, Job]
validate:
message: "Containers must not run as root. Set securityContext.runAsNonRoot: true or runAsUser > 0."
foreach:
- list: "request.object.spec.template.spec.containers"
deny:
conditions:
all:
- key: "{{ element.securityContext.runAsNonRoot || false }}"
operator: Equals
value: false
- key: "{{ element.securityContext.runAsUser || 0 }}"
operator: Equals
value: 0kubectl apply -f policies/block-root-containers.yaml # Test: root container (should be blocked) kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: root-deploy namespace: default spec: replicas: 1 selector: matchLabels: { app: root-deploy } template: metadata: labels: { app: root-deploy } spec: containers: - name: app image: nginx:1.25 securityContext: runAsUser: 0 # root — should be rejected resources: requests: { cpu: "100m", memory: "128Mi" } limits: { cpu: "200m", memory: "256Mi" } EOF ## Error: ... block-root-containers: Containers must not run as root. # Test: non-root container (should pass) kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: nonroot-deploy namespace: default spec: replicas: 1 selector: matchLabels: { app: nonroot-deploy } template: metadata: labels: { app: nonroot-deploy } spec: containers: - name: app image: nginx:1.25 securityContext: runAsNonRoot: true runAsUser: 1000 resources: requests: { cpu: "100m", memory: "128Mi" } limits: { cpu: "200m", memory: "256Mi" } EOF ## deployment.apps/nonroot-deploy created
Review policy reports and violations
# Kyverno generates PolicyReport objects for background scan results kubectl get policyreport -A # See violations across the cluster kubectl get policyreport -A -o json | \ jq '.items[].results[] | select(.result == "fail") | {policy:.policy, resource:.resources[0].name, message:.message}' # Check audit results for a specific policy kubectl describe clusterpolicy require-resource-limits # List all policies and their ready state kubectl get clusterpolicy ## NAME ADMISSION BACKGROUND VALIDATE ACTION READY ## add-app-name-label true false - True ## block-root-containers true true Enforce True ## require-resource-limits true true Enforce True
Success Criteria
Further Reading
- Kyverno documentation: kyverno.io/docs — policy types, foreach, JMESPath reference
- Kyverno Playground: playground.kyverno.io — test policies without a cluster
- PolicyReport CRD specification: wgpolicyk8s.io/v1alpha2
- OPA Gatekeeper alternative: open-policy-agent.github.io/gatekeeper