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
+(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
/ 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()}}"
{{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
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.
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>.
.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
patchStrategicMergefor declarative patches orpatchesJson6902for surgical RFC 6902 operations - Generate creates companion resources automatically — ideal for namespace provisioning (NetworkPolicy, Quota, LimitRange)
synchronize: truekeeps 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.