A team running wildcard policies across a K3s/Kubernetes fleet ran into a problem while migrating to ValidatingAdmissionPolicy (CEL). Some time after the migration, they noticed that requests for one specific resource type had been slipping past validation entirely. The root cause was a combination of a CEL runtime error and the failurePolicy setting. When you rewrite a Kyverno ClusterPolicy as a ValidatingAdmissionPolicy, does it really carry the same meaning? This article walks through the concrete pitfall and a safer way to migrate.

1. ClusterPolicy Is Going Away. Why You Need to Decide on a Migration Path Now
Kyverno has long handled Kubernetes policy management through ClusterPolicy, a YAML-based pattern-matching mechanism. But Kyverno 1.17 flagged ClusterPolicy and CleanupPolicy as deprecated, and subsequent minor releases now ship only critical fixes for them. The designated migration target is a new CEL (Common Expression Language)-based ValidatingPolicy, implemented as a superset of Kubernetes' own ValidatingAdmissionPolicy, which reached GA in 1.30.
The biggest strength of ValidatingAdmissionPolicy is that it evaluates CEL expressions directly inside the kube-apiserver process. Unlike Kyverno or OPA Gatekeeper, there's no external webhook controller to deploy and operate, and no network hop. In real-world benchmarks, running the same policy logic as CEL versus as a webhook showed a median added latency of about 0.3ms for CEL versus about 1.65ms for the webhook. From Kubernetes introducing it as an alpha feature in 2022 through its GA in 1.30, the design philosophy has been consistent: simple validation shouldn't require the operational overhead of running a webhook server.

Because of these performance and operational advantages, many teams are moving their Kyverno wildcard policies — configurations that validate multiple resource kinds at once — to CEL-based policies. But this migration involves pitfalls that go beyond a simple syntax conversion.
2. What Happened When a Wildcard Policy Was Ported As-Is
Kyverno provides a feature that auto-generates a ValidatingAdmissionPolicy from an existing ClusterPolicy. Simply enabling the generation flag on the Kyverno admission controller converts resource selectors like match.any.resources.kinds into CEL expressions, and keeps the resulting ValidatingAdmissionPolicy definition automatically in sync.
However, this auto-conversion has a clear limitation. Because ValidatingAdmissionPolicy is, by Kubernetes' own design, a validation-only mechanism, Kyverno's mutate, generate, and image-verification capabilities simply cannot be targeted for conversion. The more rule types a single ClusterPolicy bundled together, the more likely functionality gets dropped during migration.
What's trickier still is how a single CEL expression behaves once you port a wildcard policy that validates multiple resource kinds (Deployment, DaemonSet, StatefulSet, and so on) together. Kyverno's own CEL expression troubleshooting guide explicitly notes that unchecked access to optional fields is a classic source of incidents.

3. The Root Cause: CEL Field-Missing Errors Combined with failurePolicy: Ignore
This is the crux of the matter. The object variable referenced inside a CEL expression has different available fields depending on resource kind. For example, object.spec.replicas exists on a Deployment but not on a DaemonSet. If a CEL expression written to validate multiple kinds at once lacks a has() guard, a request for a DaemonSet triggers a runtime error the moment it's evaluated.
Here's how matchConditions evaluation works: if any condition evaluates to FALSE, the policy is skipped. If any condition instead returns an error, behavior follows the failurePolicy setting. With failurePolicy: Fail (the default), the request fails safe and is rejected. But during migration, it's common to set failurePolicy: Ignore out of a desire not to break existing behavior. In that case, the policy evaluation itself is silently skipped, and a request that should have been blocked passes through unchanged.
Chainguard's analysis likewise points out that while ValidatingAdmissionPolicy works well for simple validation, more complex requirements — like image signature verification or referencing external data — still need a traditional admission controller, underscoring that migrating doesn't solve everything. The same benchmark report also documents a case where a policy with an integer value set on a label field passed policy-creation-time type checking, only to have every request rejected at runtime with a type error. Incomplete type checking combined with how runtime errors are handled is one of the most commonly overlooked aspects of CEL-based validation design.

This issue is a continuation of the theme explored in an earlier article comparing the wildcard-policy overhead of Kyverno and OPA Gatekeeper — a risk that was invisible at the tool-selection stage only surfaced once teams moved to CEL.
An incident where "something that should have been validated silently passes through" also resembles, structurally, the configuration gap that broke Pod Security Admission on K3s in an earlier case. Admission control mechanisms stay invisible when working correctly — but get the boundary conditions wrong, and they fail silently with real consequences.
Eliminating this kind of invisible risk entirely is hard in day-to-day K8s operations, but visibility into policy enforcement status and Deny/Audit outcomes goes a long way toward catching anomalies faster. Kubo's Captain UI is designed to provide exactly this kind of operational transparency by default.
4. A Safe Migration Path — has() Guards, Audit First, Staged Rollout
The following steps reduce risk when migrating to CEL-based policies in production.
Always add guard clauses to CEL expressions
Anywhere an optional field is referenced, add a guard using has() or the ? optional-chaining syntax. Rather than assuming a field exists on a given resource kind, check for it explicitly — this is the most basic defense against runtime errors in the first place.
Default failurePolicy to Fail
Early in a migration it's tempting to set failurePolicy: Ignore to avoid breaking existing behavior, but that's equivalent to tolerating validation gaps. For any policy that matters, failurePolicy: Fail should be the default, and if you deliberately choose Ignore, document the reasoning and the risk.
Validate with Audit before switching to Deny
validationActions supports three modes — Deny, Warn, and Audit — and using Audit lets you see how a policy evaluates without actually blocking requests. Observing production traffic for days or weeks before flipping to Deny is the safer, staged way to roll out.
Split policies by resource kind
Bundling multiple resource kinds into one wildcard policy looks efficient at first, but the more complex the CEL expression gets, the higher the risk of missing a guard somewhere. Splitting matchConstraints.resourceRules by resource kind, and writing a tailored CEL expression for each, ends up being both more maintainable and more thoroughly validated.

Ideally, an infrastructure team can run this staged migration on its own, but in practice, knowing what to check in Audit mode and when it's safe to flip to Deny takes experience. With Kubo Cloud's Captain UI, Audit results and Deny outcomes show up on the same screen, making it easier for a team to share the evidence behind a migration decision.
In industries like finance and healthcare, where a policy violation can immediately become a compliance issue, there's also strong demand to work through policy migration step by step with outside support. For those requirements, Kubo On-Premise — which lets you run policy operations entirely within your own managed environment — is an option worth considering.
Summary — CEL Migration Is Faster, But It Can Also Fail Silently
ValidatingAdmissionPolicy offers clear benefits — no external controller, low latency — but depending on how CEL runtime errors and failurePolicy are configured, it also carries the risk that validation silently disappears for certain resources. Kubernetes' own guidance on admission webhooks keeps coming back to the same point: decide explicitly, at design time, which way a failure should fall. With Kyverno's ClusterPolicy heading toward removal, many teams are about to face this migration.
As a checklist: ① enforce has() guards everywhere, ② default to failurePolicy: Fail, ③ validate with Audit first, and ④ split policies by resource kind. Whether a CEL rewrite of your policy is "really the same" isn't something you can tell from reading the syntax alone — you need to build actual request-based verification into the migration process itself.
To keep decisions like this from becoming tribal knowledge, Kubo ships AI-Driven Deployment and Captain UI visibility as standard. Work like policy migration — where success goes unnoticed and failure causes quiet damage — is exactly where platform-level transparency pays off. If that's useful to you, feel free to reach out via Contact Us.