Back to Distributed Systems & Kubernetes Series

Kyverno Track Part 2: Mutate & Generate

June 6, 2026 Wasil Zafar 34 min read

Beyond validation, Kyverno can mutate resources on the fly (inject labels, set defaults, add sidecars) and generate new resources automatically (create NetworkPolicies, ResourceQuotas when namespaces appear). These rules reduce toil and enforce consistency without manual intervention.

Table of Contents

  1. Mutate Rules
  2. JSON Patch Mutations
  3. Generate Rules
  4. Variable Substitution
  5. Exercises
  6. Key Takeaways & Next Steps

Mutate Rules

Mutate rules modify incoming resources before they are persisted to etcd. The API server calls the Kyverno mutating webhook, which applies patches and returns the modified resource. The user sees the final (mutated) resource in their kubectl get output.

patchStrategicMerge — Add Labels & Set Defaults

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-labels
  annotations:
    policies.kyverno.io/title: Add Default Labels
    policies.kyverno.io/category: Best Practices
spec:
  rules:
    - name: add-team-label
      match:
        any:
          - resources:
              kinds:
                - Deployment
                - StatefulSet
                - DaemonSet
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              # Add label if not already present
              +(managed-by): kyverno
              +(environment): "{{request.namespace}}"
          spec:
            template:
              metadata:
                labels:
                  +(managed-by): kyverno
Anchor Notation: The +(key) anchor means "add if not present." Without the +, the field would be overwritten unconditionally. Other anchors: -(key) removes a field, =(key) conditional (if exists).

Set imagePullPolicy to Always for all containers:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: set-image-pull-policy
spec:
  rules:
    - name: set-pull-always
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        patchStrategicMerge:
          spec:
            containers:
              # Match all containers (using the name as the merge key)
              - (name): "*"
                imagePullPolicy: Always

Inject tolerations for all pods in a specific namespace:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-gpu-toleration
spec:
  rules:
    - name: add-toleration
      match:
        any:
          - resources:
              kinds:
                - Pod
              namespaces:
                - gpu-workloads
      mutate:
        patchStrategicMerge:
          spec:
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"

Inject a Sidecar Container

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: inject-logging-sidecar
spec:
  rules:
    - name: inject-fluentbit
      match:
        any:
          - resources:
              kinds:
                - Deployment
              selector:
                matchLabels:
                  inject-logging: "true"
      mutate:
        patchStrategicMerge:
          spec:
            template:
              spec:
                containers:
                  - name: fluentbit-sidecar
                    image: fluent/fluent-bit:2.2
                    resources:
                      requests:
                        cpu: 50m
                        memory: 64Mi
                      limits:
                        cpu: 100m
                        memory: 128Mi
                    volumeMounts:
                      - name: app-logs
                        mountPath: /var/log/app
                volumes:
                  - name: app-logs
                    emptyDir: {}

JSON Patch Mutations

For precise, surgical changes that patchStrategicMerge can't express, use RFC 6902 JSON Patch operations: add, remove, replace, move, copy.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-security-annotations
spec:
  rules:
    - name: add-scan-annotation
      match:
        any:
          - resources:
              kinds:
                - Deployment
      mutate:
        patchesJson6902: |-
          - op: add
            path: "/metadata/annotations/security.company.io~1last-scanned"
            value: "pending"
          - op: add
            path: "/spec/template/spec/containers/0/securityContext"
            value:
              runAsNonRoot: true
              allowPrivilegeEscalation: false
              readOnlyRootFilesystem: true
              capabilities:
                drop:
                  - ALL
Path Encoding: In JSON Patch paths, / in key names is encoded as ~1 and ~ as ~0. So the annotation key security.company.io/last-scanned becomes security.company.io~1last-scanned in the path.

Generate Rules

Generate rules create new Kubernetes resources when a trigger resource is created (or updated). Common use cases: auto-create NetworkPolicy, ResourceQuota, LimitRange, or RoleBinding when a new namespace appears.

Generate NetworkPolicy on Namespace Creation

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-networkpolicy
spec:
  rules:
    - name: default-deny-ingress
      match:
        any:
          - resources:
              kinds:
                - Namespace
      exclude:
        any:
          - resources:
              namespaces:
                - kube-system
                - kyverno
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny-ingress
        namespace: "{{request.object.metadata.name}}"
        synchronize: true
        data:
          spec:
            podSelector: {}
            policyTypes:
              - Ingress

When any namespace is created, Kyverno automatically creates a default-deny-ingress NetworkPolicy in that namespace. All ingress traffic is denied by default — teams must explicitly allow what they need.

Generate ResourceQuota

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-resourcequota
spec:
  rules:
    - name: default-quota
      match:
        any:
          - resources:
              kinds:
                - Namespace
              selector:
                matchLabels:
                  team-type: "standard"
      generate:
        apiVersion: v1
        kind: ResourceQuota
        name: default-quota
        namespace: "{{request.object.metadata.name}}"
        synchronize: true
        data:
          spec:
            hard:
              requests.cpu: "4"
              requests.memory: 8Gi
              limits.cpu: "8"
              limits.memory: 16Gi
              pods: "20"
              services: "10"
              persistentvolumeclaims: "5"

Synchronize: Keep Generated Resources in Sync

When synchronize: true is set:

  • If the generated resource is deleted, Kyverno recreates it
  • If the policy is updated, Kyverno updates all generated resources to match
  • If the trigger resource is deleted, the generated resource is also deleted
# Test: create a namespace and verify generated resources
kubectl create namespace test-team
kubectl label namespace test-team team-type=standard

# Check generated resources
kubectl get networkpolicy -n test-team
# NAME                    POD-SELECTOR   AGE
# default-deny-ingress   <none>         5s

kubectl get resourcequota -n test-team
# NAME            AGE   REQUEST   LIMIT
# default-quota   5s    ...       ...

# Try deleting — it comes back (synchronize: true)
kubectl delete networkpolicy default-deny-ingress -n test-team
kubectl get networkpolicy -n test-team
# NAME                    POD-SELECTOR   AGE
# default-deny-ingress   <none>         2s    # recreated!

Variable Substitution

Kyverno supports variable substitution using {{ }} syntax. Variables can reference the admission request, resource fields, and external data.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-ns-label-to-pods
spec:
  rules:
    - name: add-namespace-label
      match:
        any:
          - resources:
              kinds:
                - Pod
      # Precondition: only apply if namespace has 'cost-center' label
      preconditions:
        all:
          - key: "{{request.object.metadata.namespace}}"
            operator: NotEquals
            value: "kube-system"
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              +(namespace): "{{request.namespace}}"
              +(created-by): "{{request.userInfo.username}}"
            annotations:
              +(kyverno.io/created-at): "{{time.now()}}"
Common Variables: {{request.object.metadata.name}} — resource name, {{request.namespace}} — target namespace, {{request.userInfo.username}} — requesting user, {{request.operation}} — CREATE/UPDATE/DELETE, {{time.now()}} — current timestamp.

Exercises

Exercise 1: Write a mutate rule that adds the annotation team: {{request.namespace}} to all Pods. Use patchStrategicMerge with the +(key) anchor so existing annotations are not overwritten. Test by creating a pod in namespace backend and verifying the annotation exists.
Exercise 2: Create a generate rule that creates a LimitRange in every new namespace with default container limits of 200m CPU and 256Mi memory. Set synchronize: true. Verify by creating a namespace and checking kubectl describe limitrange -n <ns>.
Exercise 3: Write a JSON Patch mutation that removes the .spec.template.spec.containers[*].securityContext.privileged field from any Deployment. Use op: remove with a precondition that checks if the field exists. Test by applying a Deployment with privileged: true and confirming it's stripped.

Key Takeaways & Next Steps

  • Mutate modifies resources before persistence — use patchStrategicMerge for declarative patches or patchesJson6902 for surgical RFC 6902 operations
  • Generate creates companion resources automatically — ideal for namespace provisioning (NetworkPolicy, Quota, LimitRange)
  • synchronize: true keeps generated resources in sync with policy changes and prevents deletion
  • Variable substitution with {{ }} makes policies dynamic — reference request context, resource fields, and built-in functions
  • Preconditions add conditional logic — apply mutations/generations only when specific conditions are met

Next in the Series

In Part 3: Verify Images & Supply Chain, we'll enforce cosign signatures, SBOM attestations, and Notary v2 verification to secure the container image supply chain at the admission layer.