Kyverno is removing the old ClusterPolicy type in version 1.20, so every team still running validate, mutate, generate, or image rules needs a plan now. This guide shows you how to migrate Kyverno ClusterPolicy to CEL using real rule examples. Also, you will learn how to move your PolicyException objects and check stored data before you upgrade.
If you are new to CEL-based policies and want to learn the basics, you can check this guide on Kyverno CEL policies for Kubernetes.
Why Kyverno Is Removing ClusterPolicy
Kyverno used to have one policy type, ClusterPolicy, that could validate, mutate, generate, and verify images all in one file. As Kubernetes added native support for CEL, Kyverno built five new focused policy types on top of it:
ValidatingPolicy
MutatingPolicy
GeneratingPolicy
ImageValidatingPolicy
DeletingPolicy
These new types run faster because many of them convert directly into Kubernetes' own ValidatingAdmissionPolicy and MutatingAdmissionPolicy, so checks can run inside the API server instead of a separate webhook call.
Here is the official deprecation timeline for ClusterPolicy, Policy, CleanupPolicy, and the legacy PolicyException:
| Release |
Date |
Status |
| v1.17 |
February 2026 |
Marked deprecated |
| v1.18 |
April 2026 |
Critical fixes only |
| v1.19 |
August 2026 |
Officially deprecated, last release with full support |
| v1.20 |
November 2026 (estimated) |
Removed completely |
Kyverno v1.19 is the current latest stable release, and it is also the last version where ClusterPolicy still works fully. This is why every team must plan and test its move now, before v1.20 launches and the old objects stop being served.
What You Need Before You Start
Before migrating to CEL, make sure these things are ready:
- A test or staging Kubernetes cluster. Do not test this on production.
kubectl configured to talk to that cluster.
- Helm 3 installed on your machine.
- Kyverno v1.19 or the latest stable release installed with Helm.
- The Kyverno CLI, matching the same version as your cluster install.
- A full backup of every
ClusterPolicy, Policy, CleanupPolicy, and PolicyException object.
Why You Must Migrate Kyverno ClusterPolicy to CEL Before v1.20
When v1.20 comes out, ClusterPolicy, Policy, and CleanupPolicy will be gone from Kyverno completely. If you don't migrate Kyverno ClusterPolicy to CEL before that, your old rules stop working the moment you upgrade, and your cluster loses that protection. Kyverno does not convert old rules for you, so you must create and test the new CEL policies yourself before removing the old ones.
The old and new policies can run together during the switch, since they use separate API groups, so you can migrate step by step instead of all at once.
Step 1: Upgrade Kyverno to the Latest Stable Release
Add the Helm repository and install or upgrade Kyverno to v1.19, the current latest stable release with full CEL policy support:
helm repo add kyverno https://kyverno.github.io/kyverno/helm repo update helm upgrade --install kyverno kyverno/kyverno \ -n kyverno --create-namespace \ --version 3.5.1
Note: the Helm chart version numbers and the Kyverno app version numbers are different. Check the chart's Chart.yaml appVersion field or run the command below to confirm Kyverno v1.19:
kubectl get pods -n kyverno -o jsonpath='{.items[0].spec.containers[0].image}'
Confirm every pod is healthy before moving on:
kubectl get pods -n kyvernokubectl get crds | grep policies.kyverno.io
In the output, you must see CRDs for:
validatingpolicies, mutatingpolicies, generatingpolicies, imagevalidatingpolicies, and deletingpolicies.
These are the new CEL-based types you will use in the next steps.
Step 2: Back Up and List Every Legacy Policy
Before you change anything, save a copy of every old policy. This way, you can go back to it if something breaks. To do this, you can use the commands below:
mkdir -p ~/kyverno-migration-backupkubectl get clusterpolicy -A -o yaml > ~/kyverno-migration-backup/clusterpolicies.yamlkubectl get policy -A -o yaml > ~/kyverno-migration-backup/policies.yamlkubectl get cleanuppolicy,clustercleanuppolicy -A -o yaml > ~/kyverno-migration-backup/cleanuppolicies.yamlkubectl get policyexception.kyverno.io -A -o yaml > ~/kyverno-migration-backup/policyexceptions.yaml
Now check how many rules you have of each type. This tells you how much work is left:
kubectl get clusterpolicy -A --no-headers | wc -lkubectl get policyexception.kyverno.io -A --no-headers | wc -l
Step 3: Convert Validate Rules to ValidatingPolicy
Validate rules are the simplest to convert. Here is a real legacy ClusterPolicy that blocks Pods without a team label:
1apiVersion: kyverno.io/v12kind: ClusterPolicy3metadata:4 name: require-team-label5spec:6 validationFailureAction: Enforce7 background: true8 rules:9 - name: check-team-label10 match:11 any:12 - resources:13 kinds:14 - Pod15 validate:16 message: "The label 'team' is required on all Pods."17 pattern:18 metadata:19 labels:20 team: "?*"
Now here is the same rule, written as a ValidatingPolicy with a CEL expression instead of a pattern:
1apiVersion: policies.kyverno.io/v12kind: ValidatingPolicy3metadata:4 name: require-team-label5spec:6 validationActions: [Enforce]7 evaluation:8 background:9 enabled: true10 matchConstraints:11 resourceRules:12 - apiGroups: [""]13 apiVersions: ["v1"]14 resources: ["pods"]15 operations: ["CREATE", "UPDATE"]16 validations:17 - expression: "has(object.metadata.labels) && 'team' in object.metadata.labels"18 message: "The label 'team' is required on all Pods."
Here's what changed: pattern becomes a true/false CEL expression under validations, message stays the same, and validationFailureAction is now called validationActions.
Start with simple rules like this one when you migrate Kyverno ClusterPolicy to CEL. They're the easiest, and they help your team learn CEL before moving to harder rules.
One thing to watch for: with deny rules, the logic reverses. In ClusterPolicy, a deny rule blocks the request when its condition is true. In ValidatingPolicy, the expression must return true to allow the request. So when you rewrite a deny rule, reverse the condition.
Step 4: Convert Mutate Rules to MutatingPolicy
Here is a legacy mutate rule that adds a default CPU and memory limit to containers that do not define one:
1apiVersion: kyverno.io/v12kind: ClusterPolicy3metadata:4 name: add-default-resources5spec:6 background: false7 rules:8 - name: add-limits9 match:10 any:11 - resources:12 kinds:13 - Pod14 mutate:15 patchStrategicMerge:16 spec:17 containers:18 - (name): "*"19 resources:20 limits:21 +(cpu): "500m"22 +(memory): "256Mi"
Here is the same rule as a MutatingPolicy. It uses the ApplyConfiguration style and a CEL map() loop to go through each container:
1apiVersion: policies.kyverno.io/v12kind: MutatingPolicy3metadata:4 name: add-default-resources5spec:6 evaluation:7 admission:8 enabled: true9 matchConstraints:10 resourceRules:11 - apiGroups: [""]12 apiVersions: ["v1"]13 resources: ["pods"]14 operations: ["CREATE"]15 mutations:16 - patchType: ApplyConfiguration17 applyConfiguration:18 expression: >19 Object{20 spec: Object.spec{21 containers: object.spec.containers.map(c,22 Object.spec.containers{23 name: c.name,24 resources: Object.spec.containers.resources{25 limits: {"cpu": "500m", "memory": "256Mi"}26 }27 }28 )29 }30 }
This part confuses most teams, because CEL has no direct match for patchStrategicMerge. You must rewrite each mutation as ApplyConfiguration merge style, shown above, or JSONPatch for single-field edits.
Always test mutate rules on a real sample resource with the Kyverno CLI before turning them on in a cluster.
Step 5: Convert Generate Rules to GeneratingPolicy
A common legacy pattern is to auto-create a default NetworkPolicy whenever a new namespace appears:
1apiVersion: kyverno.io/v12kind: ClusterPolicy3metadata:4 name: generate-default-networkpolicy5spec:6 rules:7 - name: default-deny8 match:9 any:10 - resources:11 kinds:12 - Namespace13 generate:14 apiVersion: networking.k8s.io/v115 kind: NetworkPolicy16 name: default-deny-all17 namespace: "{{request.object.metadata.name}}"18 synchronize: true19 data:20 spec:21 podSelector: {}22 policyTypes:23 - Ingress24 - Egress
Here is the same rule as a GeneratingPolicy. It writes the new object's contents as a CEL expression:
1apiVersion: policies.kyverno.io/v12kind: GeneratingPolicy3metadata:4 name: generate-default-networkpolicy5spec:6 evaluation:7 synchronize: true8 matchConstraints:9 resourceRules:10 - apiGroups: [""]11 apiVersions: ["v1"]12 resources: ["namespaces"]13 operations: ["CREATE"]14 generations:15 - expression: >16 [Object{17 apiVersion: "networking.k8s.io/v1",18 kind: "NetworkPolicy",19 metadata: Object.metadata{20 name: "default-deny-all",21 namespace: object.metadata.name22 },23 spec: Object.spec{24 podSelector: Object.spec.podSelector{},25 policyTypes: ["Ingress", "Egress"]26 }27 }]
synchronize and generateExisting keep the same names, but now sit under spec.evaluation instead of directly in the rule. Test this policy on a throwaway namespace first, then check the result with:
kubectl get networkpolicy -n <test-namespace> -o yaml
Step 6: Convert Image Verification Rules to ImageValidatingPolicy
If your cluster checks image signatures with Cosign, here is the old rule:
1apiVersion: kyverno.io/v12kind: ClusterPolicy3metadata:4 name: verify-signed-images5spec:6 rules:7 - name: check-signature8 match:9 any:10 - resources:11 kinds:12 - Pod13 verifyImages:14 - imageReferences:15 - "registry.example.com/*"16 attestors:17 - entries:18 - keys:19 publicKeys: |-20 -----BEGIN PUBLIC KEY-----21 ...22 -----END PUBLIC KEY-----
Here is the same rule as an ImageValidatingPolicy, the CEL policy type made just for image security:
1apiVersion: policies.kyverno.io/v12kind: ImageValidatingPolicy3metadata:4 name: verify-signed-images5spec:6 matchImageReferences:7 - glob: "registry.example.com/*"8 attestors:9 - name: my-cosign-key10 cosign:11 key:12 data: |-13 -----BEGIN PUBLIC KEY-----14 ...15 -----END PUBLIC KEY-----16 validations:17 - expression: >18 images.containers.all(image,19 verifyImageSignatures(image, [attestors.my-cosign-key]) > 0)20 message: "Container image is not signed by a trusted key."
This shows why you should not migrate Kyverno ClusterPolicy to CEL all at once. ImageValidatingPolicy has its own CEL functions, like verifyImageSignatures() and verifyAttestationSignatures(). So test image checks separately from your plain validation rules.
Step 7: Migrate PolicyException Objects
The old kyverno.io PolicyException only works with legacy ClusterPolicy rules. It is also deprecated and removed in v1.20. It needs its own migration to the new policies.kyverno.io PolicyException, which is the type that works with all CEL policies.
Here is an old exception that lets one deployment skip a rule:
1apiVersion: kyverno.io/v22kind: PolicyException3metadata:4 name: allow-legacy-app5 namespace: default6spec:7 exceptions:8 - policyName: require-team-label9 ruleNames:10 - check-team-label11 match:12 any:13 - resources:14 names:15 - legacy-app
Here is the new exception. It points to the CEL policy by name and uses a match condition instead of named rules, since CEL policies don't have named rules inside them:
1apiVersion: policies.kyverno.io/v12kind: PolicyException3metadata:4 name: allow-legacy-app5 namespace: default6spec:7 policyRefs:8 - name: require-team-label9 kind: ValidatingPolicy10 matchConditions:11 - name: only-legacy-app12 expression: "object.metadata.name == 'legacy-app'"
Repeat this for every exception in your backup file. Don't skip this; if you miss one, a resource that used to be allowed will suddenly get blocked.
Step 8: Roll Out New Rules in Stages
Because CEL policies live in a separate API group, you can run old and new rules together safely. Use this pattern every time you migrate Kyverno ClusterPolicy to CEL for a rule that matters:
- Create the new CEL policy with
validationActions: [Audit] so it only reports violations without blocking anything.
- Watch the
PolicyReport objects for a few days: kubectl get polr -A and kubectl describe polr <name> -n <namespace>.
- Limit the rollout further with
matchConditions, for example, only matching one namespace at first, then widening it once you trust the results.
- When reports look clean, switch
validationActions to [Enforce].
- Only after the CEL policy is in
Enforce mode and stable, disable or delete the old ClusterPolicy rule it replaces.
Note: Never delete the old rule and switch on the new one at the same moment. Running both for a week or two in audit mode is the safest way to catch a CEL expression mistake before it blocks real traffic.
Step 9: Test Everything With the Kyverno CLI
Testing is a must when you migrate Kyverno ClusterPolicy to CEL. A small mistake in a CEL expression can quietly allow or block the wrong resources. So before any policy touches a real cluster, test it locally first:
kyverno apply require-team-label.yaml --resource pod-test.yaml
If you already have Kyverno CLI tests for your old ClusterPolicy rules, you can reuse them for the new CEL policies with no changes to the test files themselves; only the policy file needs to change.
Step 10: Check for Deprecated Stored Resources
In Kyverno v1.19, CEL policy objects are stored internally as v1beta1. In v1.20, this changes to v1. So after you upgrade, before your final switch, update any old stored objects to the new storage version with this command:
Also, confirm no legacy objects remain before you upgrade to v1.20:
kubectl get clusterpolicy,policy,cleanuppolicy,clustercleanuppolicy -Akubectl get policyexception.kyverno.io -A
Both commands should show "No resources found" everywhere before you upgrade to v1.20. If anything still shows up, go back and finish converting it.
Step 11: Remove the Old ClusterPolicy Objects
Once every CEL replacement is in Enforce mode and has run cleanly for a week or more, remove the legacy objects:
kubectl delete -f ~/kyverno-migration-backup/clusterpolicies.yamlkubectl delete -f ~/kyverno-migration-backup/policyexceptions.yaml
Keep the backup files even after deletion. They are your rollback plan if you ever need to check exactly what a legacy rule used to do.
Run Your New CEL Policies on Solid Infrastructure
A policy engine is only as good as the cluster running it. Once you migrate Kyverno ClusterPolicy to CEL, you need steady CPU, fast network to the API server, and reliable disk speed. This is because background scans, mutations, and image checks all add extra load.
For production clusters, consider dedicated hosting built for Kubernetes workloads as your target. Dedicated hardware gives Kyverno's controllers guaranteed resources, so policy checks stay fast even under heavy load.
Conclusion
Kyverno's move to CEL is a real improvement with faster checks, better Kubernetes fit, and five focused policy types instead of one big one. But you only get these benefits once you migrate Kyverno ClusterPolicy to CEL, rule by rule, testing in audit mode before you enforce.
First, upgrade Kyverno, back up your old rules, and convert validate rules. Then, mutate, generate, and image rules, migrate your exceptions, and roll out in stages. Finally, check for leftover objects before v1.20 removes the old types for good.
We hope you enjoy this guide. For more detailed information, you can check the Kyverno Official Migration.