Kyverno Policy: Migration to CEL Based Policies
Quick question: the last time a Kyverno policy rejected a resource, did the error actually tell you why — or did you spend twenty minutes squinting at a pattern block trying to reverse-engineer your own rule?
If you’ve been running Kyverno for a while, you already know the answer. And you’re not alone — it’s the single most common complaint we hear from platform teams, and it’s exactly what Kyverno’s move to CEL (Common Expression Language) fixes.
With Kyverno 1.17, that move stops being a nice-to-have: ClusterPolicy and CleanupPolicy, the two resource types most of us have leaned on for years, have been deprecated and are scheduled for removal in v1.20, currently scheduled for October, 2026.
Let’s talk about why that’s actually good news, and what it takes to get ahead of it.
Why CEL is worth getting excited about
We get it — “your policy syntax is changing” doesn’t usually sound like a win but this one is! It has been widely adopted in Kubernetes and spreading through the CNCF ecosystem. You already know CEL. KRO, agentgateway etc. all are using CEL. Pattern-based rules were fine for simple checks, but the moment you needed real logic — negation, iteration, “match any of these three conditions” — you were fighting the syntax instead of writing the rule. CEL removes that friction:
- You can actually read it. namespace: “!default” becomes object.metadata.namespace != “default”. No more decoding pattern negation — it just says what it means.
- It thinks the way you do. “Does this image come from one of our three approved registries?” used to mean wrestling nested pattern anchors. Now it’s just container.image.startsWith(“registry.company.com/”) || container.image.startsWith(“quay.io/company/”).
- You’re speaking Kubernetes’ native language. CEL is the same expression language Kubernetes itself uses for ValidatingAdmissionPolicy. This isn’t a Kyverno detour — it’s where the whole ecosystem is headed, and getting comfortable with it now pays off well beyond Kyverno.
- Errors that actually help you. Compiled, optimized expressions, and validation failures that tell you exactly what went wrong instead of a generic pattern mismatch. Debugging a rejected resource stops being guesswork.
- One policy, one job. Instead of a ClusterPolicy quietly bundling validate, mutate, generate, and image-verification rules into one sprawling resource, each new CEL policy does exactly one thing. Easier to read, easier to test, easier to hand to a teammate and say “here’s what this does.”
“The error told me a pattern didn’t match. It did not tell me which line, which field, or why.” — every platform engineer who’s debugged a ClusterPolicy at 11pm.
So what’s actually changing in 1.20?
Here’s the timeline, plainly: Kyverno flagged ClusterPolicy and CleanupPolicy as deprecated in v1.17. Since then they’ve been in maintenance mode — critical fixes only, through v1.19 — and full removal is planned for v1.20. In their place, five CEL-based policy types now live under the stable policies.kyverno.io/v1 API:
| What you used to write | What replaces it |
| ClusterPolicy (validate rules) | ValidatingPolicy |
| ClusterPolicy (mutate rules) | MutatingPolicy |
| ClusterPolicy (generate rules) | GeneratingPolicy |
| ClusterPolicy (verifyImages rules) | ImageValidatingPolicy |
| CleanupPolicy | DeletingPolicy |
One rule, before and after
Enough theory — let’s look at an actual policy. Take one of the most common rules out there: blocking the mutable :latest image tag. Here’s the ClusterPolicy version most of us have running today, with two separate pattern rules to get the job done:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
validationFailureAction: Enforce
background: true
rules:
- name: require-image-tag
match:
any:
- resources:
kinds:
- Pod
validate:
message: "An image tag is required"
pattern:
spec:
containers:
- image: "*:*"
- name: validate-image-tag
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Using 'latest' tag is not allowed"
pattern:
spec:
containers:
- image: "!*:latest"
And here’s the same rule as a CEL-based ValidatingPolicy — both pattern rules collapsed into a single, readable expression:
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: disallow-latest-tag
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: ['']
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
validations:
- message: "image tag 'latest' is not allowed"
expression: "object.spec.containers.all(container, !container.image.endsWith(':latest'))"
Notice what happened: two pattern rules became one CEL expression using .all() to check every container in the pod at once. match.any.resources.kinds became matchConstraints.resourceRules. validationFailureAction: Enforce became validationActions: [Deny]. And the rule itself — !container.image.endsWith(‘:latest’) — reads like what it actually does, instead of a pattern you have to mentally negate.
Before you rewrite anything — check the library
Here’s the part we wish more teams knew before they started migrating by hand: not every policy is a snowflake. If your ClusterPolicy library looks like most of ours do — pod security standards, resource hygiene, image verification, the usual best-practice checks — there’s a good chance someone’s already written the CEL version for you.
The Kyverno policy library at kyverno.io is a maintained, community-curated catalog of these common policies, and it’s actively being filled out with CEL-based equivalents alongside the legacy pattern versions. The disallow-latest-tag example above? Already published there, both versions, ready to copy. So before you sit down to translate a rule line by line, take five minutes and search the library. You might just find you can swap it in and move on to the policies that actually need your attention.
The policies the library can’t write for you
That leaves the policies your team wrote for your own workloads, your own naming conventions, your own exceptions — the stuff no public catalog can anticipate. For those, here’s what the migration honestly involves:
- Splitting every multi-rule ClusterPolicy into separate, single-purpose CEL policies
- Rewriting match/exclude conditions and context variables as CEL expressions
- Flipping the logic — old deny conditions described what should fail; CEL validate expressions assert what should pass, so the boolean logic runs in the opposite direction
- Swapping pattern comprehensions for CEL’s all(), exists(), and exists_one() operators
- Converting CleanupPolicy schedules into DeletingPolicy resources
- Testing every converted policy against the same fixtures the original passed — because “looks right” and “behaves identically” are not the same thing
One policy, one afternoon — no big deal. A real policy library spanning a few dozen clusters, several teams, and a pile of exceptions? That’s where a missed edge case doesn’t show up until an audit, an incident, or a workload that silently stops being checked at all.
Where nctl ai comes in
This is the exact problem we built nctl ai (Nirmata Assistant) to solve. It’s an AI agent that runs as a CLI right on your own workstation — scoped only to the directories you explicitly hand it, built specifically for Kubernetes policy-as-code work, including upgrading Kyverno policies from older versions to CEL.
A Nirmata financial customer was able to migrate 30 custom cluster policies to CEL based policy structure in 2 days with unit and chainsaw tests, which they did not have in the first place. They were able to implement a migration plan to promote those policies with proper automated tests with high confidence with positive and negative use cases..
Instead of translating each custom ClusterPolicy and CleanupPolicy by hand, point nctl ai at your existing library and ask it to generate the CEL equivalents — plus the Kyverno test cases you need to prove the new policy behaves exactly like the old one before you cut over. Use it interactively (nctl ai) or single-shot for scripting and CI (nctl ai –prompt “…”). It asks before it changes anything, so you get migration at scale without handing an agent the keys to your cluster.
Two commands and you’re up and running:
brew tap nirmata/tap && brew install nctl
nctl ai
Ask your team these questions before your next planning cycle
- How many ClusterPolicy and CleanupPolicy resources are actually running across our clusters right now?
- Which of those are standard checks we could pull straight from the Kyverno policy library instead of migrating by hand?
- Which ones are custom — written for our workloads, our naming conventions, our exceptions?
- Do we have test fixtures for each policy, so we can actually prove parity after conversion?
- Who owns this migration, and is it on the roadmap before v1.20 ships — or are we planning to find out the hard way?
The legacy types are already in maintenance-only mode, so every new ClusterPolicy written today is just a policy someone migrates again tomorrow. Check the library for what’s already solved, bring in nctl ai for what’s custom, and start with your smallest, lowest-risk policy. Better to make this move on your own schedule than on v1.20’s.
Curious what nctl ai can do with your own policy library? Check out the nctl ai docs, or just reach out.
We’re at cs@nirmata.com and always happy to talk policy migration.
— The Nirmata Team

