← Back to Guide
Automation & IaC L2 · PRACTICAL ~60 min

Kyverno Policy Suite: Validate, Mutate, Block

Write three production-grade Kyverno ClusterPolicies covering the three primary policy types — validation, mutation, and blocking — then test each rule against conformant and non-conformant Kubernetes manifests.

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

Steps

01

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
02

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.

policies/require-resource-limits.yaml
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
03

Test Policy 1 — Reject a non-conformant Deployment

test/deploy-no-limits.yaml
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 rejected
kubectl 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.
04

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.

policies/add-app-name-label.yaml
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 }}"
The +(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"}
05

Policy 3 — Validate: block containers running as UID 0 (root)

policies/block-root-containers.yaml
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: 0
kubectl 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
06

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