- 1. Validate Rules
- 2. Mutate Rules
- 3. Generate Rules
- 4. Variables and Context
- 5. Advanced Patterns
- 6. From validationFailureAction to failureAction
- 7. Where Each Rule Type Runs, and in What Order
- 8. Running One Policy Locally Before Putting It in the Cluster
- 9. Failure Cases and the Order to Diagnose Them
- 10. When Not to Use This
- 11. References
- 12. Summary
1. Validate Rules
1.1 Pattern Matching
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-run-as-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-security-context
match:
any:
- resources:
kinds:
- Pod
validate:
message: 'Containers must run as non-root'
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
Operators: ?* (non-empty), * (any, including null), X|Y (or), !X (not), >X, <X, >=X, <=X (numeric comparison).
1.2 deny Rules
Reject resources based on conditions:
rules:
- name: deny-latest-tag
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Using 'latest' tag is not allowed. Use a specific version tag."
deny:
conditions:
any:
- key: '{{ request.object.spec.containers[].image }}'
operator: AnyIn
value:
- '*:latest'
1.3 CEL Expressions
CEL (Common Expression Language) on Kubernetes 1.25+:
rules:
- name: check-replica-count
match:
any:
- resources:
kinds:
- Deployment
validate:
cel:
expressions:
- expression: 'object.spec.replicas >= 2'
message: 'Deployment must have at least 2 replicas'
- expression: 'object.spec.replicas <= 100'
message: 'Deployment cannot exceed 100 replicas'
1.4 foreach
Validate each element of a collection:
rules:
- name: check-each-container
match:
any:
- resources:
kinds:
- Pod
validate:
message: 'All containers must have resource limits'
foreach:
- list: 'request.object.spec.containers'
deny:
conditions:
any:
- key: '{{ element.resources.limits.memory }}'
operator: Equals
value: ''
2. Mutate Rules
2.1 patchStrategicMerge
Kubernetes Strategic Merge Patch:
rules:
- name: add-sidecar
match:
any:
- resources:
kinds:
- Deployment
selector:
matchLabels:
inject-sidecar: 'true'
mutate:
patchStrategicMerge:
spec:
template:
spec:
containers:
- name: log-collector
image: fluentbit:latest
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
volumes:
- name: shared-logs
emptyDir: {}
2.2 patchesJson6902
JSON Patch (RFC 6902):
rules:
- name: add-annotation
match:
any:
- resources:
kinds:
- Service
mutate:
patchesJson6902: |-
- op: add
path: /metadata/annotations/modified-by
value: kyverno
- op: replace
path: /spec/type
value: ClusterIP
2.3 foreach mutate
rules:
- name: add-pull-secret-to-all-containers
match:
any:
- resources:
kinds:
- Pod
mutate:
foreach:
- list: 'request.object.spec.containers'
patchStrategicMerge:
spec:
imagePullSecrets:
- name: my-registry-secret
3. Generate Rules
3.1 data-based Generation
Create resources from data defined in the policy:
rules:
- name: generate-default-limitrange
match:
any:
- resources:
kinds:
- Namespace
generate:
apiVersion: v1
kind: LimitRange
name: default-limits
namespace: '{{ request.object.metadata.name }}'
synchronize: true
data:
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: Container
3.2 clone-based Generation
Clone an existing resource:
rules:
- name: clone-configmap
match:
any:
- resources:
kinds:
- Namespace
generate:
apiVersion: v1
kind: ConfigMap
name: shared-config
namespace: '{{ request.object.metadata.name }}'
synchronize: true
clone:
namespace: default
name: template-configmap
3.3 The synchronize Option
With synchronize: true:
- Changes to the source resource propagate to the generated resource
- The Background Controller performs the synchronization
- Manual edits to the generated resource are reverted automatically
4. Variables and Context
4.1 JMESPath Variables
rules:
- name: add-ns-label
match:
any:
- resources:
kinds:
- Deployment
mutate:
patchStrategicMerge:
metadata:
labels:
namespace: '{{ request.object.metadata.namespace }}'
owner: '{{ request.userInfo.username }}'
4.2 API Call Context
rules:
- name: check-namespace-labels
match:
any:
- resources:
kinds:
- Pod
context:
- name: namespaceInfo
apiCall:
urlPath: '/api/v1/namespaces/{{ request.namespace }}'
jmesPath: "metadata.labels.environment || 'unknown'"
validate:
message: 'Pods can only run in labeled namespaces'
deny:
conditions:
any:
- key: '{{ namespaceInfo }}'
operator: Equals
value: 'unknown'
4.3 ConfigMap Lookups
rules:
- name: check-allowed-registries
match:
any:
- resources:
kinds:
- Pod
context:
- name: allowedRegistries
configMap:
name: allowed-registries
namespace: kyverno
validate:
message: 'Image must be from an allowed registry'
foreach:
- list: 'request.object.spec.containers'
deny:
conditions:
all:
- key: '{{ element.image }}'
operator: AnyNotIn
value: '{{ allowedRegistries.data.registries }}'
5. Advanced Patterns
5.1 Conditional Anchors
# () anchor: conditional — validate only if the field exists
validate:
pattern:
spec:
template:
spec:
containers:
- (image): "*/nginx:*" # only for nginx images
resources:
limits:
memory: ">=256Mi"
# X() negation anchor: the field must not exist
validate:
pattern:
spec:
template:
spec:
containers:
- name: "*"
X(securityContext):
X(privileged): true # privileged must not be true
5.2 Equality Anchor
# =() equality anchor
validate:
pattern:
spec:
=(replicas): '>=3' # if replicas is set, it must be 3 or more
6. From validationFailureAction to failureAction
Every example in this post uses spec.validationFailureAction. They are left that way because that is what the policies running in most clusters look like today, but the field is deprecated and moves to the rule-level spec.rules[*].validate[*].failureAction. The values are Enforce and Audit, and when unspecified it is Audit. Enforce blocks the offending request; Audit lets it through and records the violation in a report.
Why it moved is the point of the change. As a policy-level field, every validate rule inside one policy shared the same enforcement level. Wanting to block one rule while still only observing another meant splitting the policy in two, duplicating the match block and adding another object to manage. At rule level, one rule in a policy can be Enforce while another is Audit. Adding a new rule to an existing policy in Audit, reading reports for a few days, then promoting only that rule to Enforce becomes possible without splitting anything.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-run-as-non-root
spec:
rules:
- name: check-security-context
match:
any:
- resources:
kinds:
- Pod
validate:
# rule level, instead of policy-level validationFailureAction
failureAction: Enforce
message: 'Containers must run as non-root'
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
- name: warn-on-missing-limits
match:
any:
- resources:
kinds:
- Pod
validate:
# only this rule stays in observation mode
failureAction: Audit
message: 'Containers should declare resource limits'
pattern:
spec:
containers:
- resources:
limits:
memory: '?*'
It is worth knowing what moved alongside it. webhookTimeoutSeconds and failurePolicy are also marked deprecated as of 1.13 and move to webhookConfiguration.timeoutSeconds and webhookConfiguration.failurePolicy. schemaValidation has been deprecated since 1.11 and, per the docs, currently has no effect. Other policy-level fields remain in use: background turns on scanning of existing resources to find violations and generate reports and defaults to true; admission decides whether rules apply during admission control, defaults to true, and makes the policy background-only when set to false; applyRules states how many rules apply to a matching resource, where One stops after the first match and All is the default.
spec:
background: true # default true — scan existing resources, generate reports
admission: true # default true — false makes it background-only
applyRules: All # All (default) or One (stop after the first match)
webhookConfiguration:
timeoutSeconds: 20 # replaces webhookTimeoutSeconds as of 1.13
failurePolicy: Fail # replaces spec.failurePolicy as of 1.13
Which field is actually read differs by version, so check the exact field in the docs for the version you run.
7. Where Each Rule Type Runs, and in What Order
Kyverno looks like a single engine, but each rule type executes in a different place. Mutate rules run in the mutating webhook, validate rules in the validating webhook. The Kubernetes API server calls mutating admission first and validating admission second, so the object a validate rule sees is the object after the mutate rules have already touched it. Not knowing that order means getting caught by your own policy. Run a mutate policy that injects a sidecar next to a validate policy that requires resource limits on every container, and the injected sidecar becomes subject to the limits check; if the injection spec has no limits, the deployment is blocked and the log names a container the user never wrote. Putting resource limits in a sidecar injection policy is a requirement, not a matter of taste.
Generate rules run somewhere else entirely. Admission does not create the resource; it records an UpdateRequest, and the background controller performs the creation afterwards. Checking for the generated object immediately after creating a namespace may therefore find nothing, and that is design, not a bug. When generation does not happen, look at whether an UpdateRequest was recorded before suspecting the policy. No request at all means the match did not fire; a request with no resource means the problem is on the background controller side. That single fork halves the search space.
kubectl -n kyverno get updaterequests
kubectl auth can-i create helmrepositories --as system:serviceaccount:kyverno:kyverno-background-controller
synchronize: true is not free either. It propagates source changes to the generated resource and reverts manual edits to it, which means watches and writes scale with the number of target namespaces. On a five-namespace cluster nobody notices; on a several-hundred-namespace cluster it becomes standing load on the background controller. And that controller ships with only a minimal set of permissions — any additional permissions are up to the user to add. So when you start generating something that is not a standard resource, run the auth can-i above with that resource name first. Without the permission, admission succeeds and only the resource quietly fails to appear, which is the hardest kind of failure to notice.
Finally, if applyRules is One, only the first matching rule is applied and evaluation stops. If you have rules listed in order and are wondering why the later ones never run, check that field first.
8. Running One Policy Locally Before Putting It in the Cluster
A policy can be run locally before it reaches a cluster, and that one habit prevents most incidents. Give kyverno apply the policy file and the manifest to check via --resource. For a policy that uses variables, inject them one by one with --set or pass a values file with -f. Choose the output shape with -t for a table, --detailed-results for detail, and -p for report form. If failures or errors are found the command exits 1, so it works directly as a CI gate. Running this on every policy edit also prevents the classic accident of writing a match block that matches nothing and mistaking the resulting silence for success — a policy that matches nothing passes everything in the cluster and looks identical to a working one.
kyverno apply policy.yaml --resource pod.yaml
kyverno apply policy.yaml --resource pod.yaml --set namespace=prod,team=payments
kyverno apply policy.yaml --resource pod.yaml -f values.yaml
kyverno apply policy.yaml --resource pod.yaml -t --detailed-results
kyverno apply policy.yaml --resource pod.yaml --policy-report
# 1 if failures or errors are found
echo $?
Once local checks pass, the same command can be aimed at a cluster. -c checks against the cluster in the current context, and the docs also show pulling policies straight from a git source. That combination is useful for seeing in advance how many existing resources a new policy would catch. After that, apply the policy for real and confirm what it looks like in-cluster from the policy list and the reports — including UpdateRequests when generate rules are involved.
kyverno apply policy.yaml --cluster
kyverno apply https://github.com/kyverno/policies/openshift/ --git-branch main --cluster
kubectl apply -f policy.yaml
kubectl get cpol,pol -A
kubectl -n kyverno get updaterequests
9. Failure Cases and the Order to Diagnose Them
The most common mistake when a policy does not behave as expected is to start by staring at the policy YAML. Policy syntax is the cause less often than you would think; most of the time the policy was never evaluated, or a variable resolved to an empty value. Fixing the order makes this much faster.
First, is the policy ready? Check that the ready column of the policy list is true everywhere — the docs list this as the first diagnostic step. Second, are the webhooks registered? Kyverno registers as two types of webhooks, and without registration the policy exists but no request ever passes through it. That accounts for most cases where nothing is being blocked while the policy looks perfectly fine.
kubectl -n kyverno get po
kubectl get cpol,pol -A
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations
Third, a variable that silently resolves to empty. If the jmesPath of an apiCall does not match the actual response shape, the result is an empty value rather than an error, and an empty value in a condition makes that condition always true or always false. It is the classic shape of a policy that looks correct while only its results are wrong. Seeing it requires raising the log level: the docs point at -v=4 as the level that shows variable substitution and -v=6 as maximum verbosity. If that still does not catch it, dumpPayload=true prints the full AdmissionReview contents so you can see the object that actually arrived.
kubectl -n kyverno edit deploy kyverno-admission-controller
kubectl -n kyverno logs <pod_name> -f
# raise QPS and burst when client-side throttling shows up
# --clientRateLimitQPS=500 --clientRateLimitBurst=500
Fourth, writing a foreach list wrapped in braces. The list takes a JMESPath expression directly, so it is not wrapped in variable notation — which is why every foreach example in this post carries quotes and nothing else. Fifth, misused anchors. Conditional (), equality =(), existence ^() and negation X() all mean different things, and mistaking the negation anchor for a value comparison is especially common. The negation anchor means the key must not exist, not that its value must differ.
Sixth, background controller permissions. When generation does not happen, run the auth can-i from section 7 against the target resource. Seventh, admission reports stacking up. The docs explain that reports accumulate when the reports controller is not working properly or fails to aggregate admission reports fast enough. Check the reports controller status first, then raise QPS and burst if client-side throttling is visible.
Last is the worst case. If a policy blocks the API server so that nothing can be deployed, delete the webhook configurations or scale the admission controller to zero to bring the cluster back first. Both switch policy enforcement off entirely, so they must be reverted once the cause is fixed.
10. When Not to Use This
If what you are checking is the value of a single field and nothing more is needed, the in-tree ValidatingAdmissionPolicy with CEL is enough. Operating one component fewer means one fewer thing to upgrade, one fewer place to suspect during an incident, and one fewer webhook certificate rotation to think about. Kyverno also has CEL-based validate, but that is a useful option once you already run Kyverno — it is not a reason to adopt it. What justifies Kyverno is generate, mutate, and things in-tree policy cannot do at all, such as image verification.
It is also common for a value being filled in by mutate to really belong as a chart default. Put it in Helm values and it is visible in git, reviewed, and rolled back with a tag; put it in a mutate rule and it exists only in the cluster. Six months later nobody knows why that annotation is there, and the difference between the manifest and the live object keeps fighting your deployment tool's drift detection. Keep only the values that must be enforced organization-wide in mutate, and leave the ones teams are allowed to change in the chart.
Policies that match every resource with a wildcard deserve particular caution. Every request then passes through the webhook, which amounts to a latency tax on the whole cluster, and that cost attaches to every request reaching the API server rather than to one policy's performance. If a response does not come back within the timeout, the default failurePolicy of Fail rejects the request — so when Kyverno slows down, the cluster does not get slower, it stops. Narrowing matches to the kinds and namespaces you actually need is availability work, not performance tuning.
Managing resources across hundreds of namespaces with generate and synchronize is also worth reconsidering. GitOps tooling does that job better and, more importantly, leaves a trace in git. Kyverno's generate earns its keep when it has to react to something only knowable at admission time, such as a namespace being created.
11. References
- Kyverno — Validate Rules —
validationFailureActiondeprecation andfailureAction, foreachlistnotation, anchor types, version applicability of podSecurity/CEL/assert (checked 2026-08-16) - Kyverno — Policy Settings —
background,admission,applyRules, the 1.13 deprecation ofwebhookTimeoutSecondsandfailurePolicy(checked 2026-08-16) - Kyverno CLI — kyverno apply —
--resource,--set,--values-file,-t,--detailed-results,--policy-report,--cluster, exit code 1 on failure (checked 2026-08-16) - Kyverno — Troubleshooting — policy ready check, webhook registration check,
kubectl -n kyverno get updaterequests, background controller permission check, admission report accumulation,-v=4/-v=6/dumpPayload=true(checked 2026-08-16)
12. Summary
- validate: Pattern matching, deny conditions, CEL expressions, foreach
- mutate: Strategic Merge Patch, JSON Patch for automatic resource modification
- generate: data/clone-based auto-generation with synchronize
- Variable system: JMESPath, API calls, ConfigMap lookups for dynamic policies
- Anchor system: Conditional, negation, equality anchors for fine-grained matching
- Enforcement level: moved from policy-level
validationFailureActionto rule-levelfailureAction - Execution site: mutate in the mutating webhook, validate in the validating webhook, generate in the background controller
The next post covers Kyverno image verification and supply chain security.