CEL Policy Exception Handling#
Kyverno's CEL-based policy engines β vpol (ValidatingPolicy/NamespacedValidatingPolicy) and ivpol (ImageValidatingPolicy/NamespacedImageValidatingPolicy) β compile PolicyException objects into each policy at reconcile time, not at evaluation time. A compiled policy already embeds its exceptions; the engine checks for skips during evaluation by inspecting the compiled artifact .
How Exceptions Are Compiled In#
Static provider (NewProvider) β used in CLI and background scan pipelines. Both vpol and ivpol static providers iterate over all policies, match exceptions by PolicyRef.Name/Kind, and call compiler.Compile(policy, matchedExceptions) immediately .
Dynamic (Kubernetes) provider (NewKubeProvider) β used by the admission controller. Both vpol and ivpol providers spin up controller-runtime controllers that watch policy CRDs. When polexEnabled is true, each provider also registers a handler.Funcs watcher on PolicyException objects against the controller-runtime manager's cache .
On any PolicyException create/update/delete event, the handler extracts polex.Spec.PolicyRefs, resolves the referenced policy names, and enqueues reconcile.Request items for those policies β dropping the exception object itself .
During reconciliation, both reconcilers call engine.ListExceptions to enumerate matching exceptions:
engine.ListExceptions(r.polexLister, policy.GetKind(), policy.GetName())
ListExceptions lists from a PolicyExceptionLister interface backed by a client-go SharedInformerFactory lister β not the controller-runtime manager cache that fired the event . The lister is scoped to a namespace when ExceptionNamespace is set .
After fetching exceptions, the vpol reconciler calls r.compiler.Compile(policy, exceptions) and stores the result in r.policies[key] under a sync.RWMutex . The ivpol reconciler stores exceptions directly on the Policy struct (no compilation step) alongside autogen variants .
Fetch() on both reconcilers returns the cached compiled policies under a read lock .
Wiring in main.go#
The exception lister is constructed once at startup from the kyvernoInformer client-go factory and passed to all three CEL providers :
celExceptionLister := celengine.NewPolicyExceptionLister(
kyvernoInformer.Policies().V1beta1().PolicyExceptions().Lister(),
internal.ExceptionNamespace(),
)
The controller-runtime manager's cache is synced at startup via mgr.GetCache().WaitForCacheSync(ctx) , but this is independent of the client-go informer factory sync for PolicyException. Both informers watch the same PolicyException resource with different resync periods, and neither is guaranteed to deliver events in the same order.
Race Condition: Dual-Informer Desync (Issue #16989)#
Root cause: The controller-runtime cache and the client-go SharedInformerFactory maintain two independent watch streams on PolicyException. When the manager cache delivers a create/update event first, the reconciler reads from r.polexLister (client-go factory) before that lister has received the same object. The result: exceptions are empty, the policy compiles without the exception, the cache is updated, and reconciliation returns success with no requeue .
manager cache βββΆ event arrives βββΆ enqueue policy reconcile
client-go lister (still stale) βββΆ ListExceptions returns []
βββΆ Compile(policy, []) cached βββΆ no requeue
Impact in HA deployments: Each admission replica resolves the race independently. A replica that compiled the policy without exceptions permanently ignores the PolicyException for its lifetime β it never detects the inconsistency, logs no error, and reports healthy. Two identical AdmissionReview requests routed to different replicas can return allowed: true and allowed: false .
The two watch streams are distinguishable in logs by their resync periods:
resyncPeriod=15m0sβ client-go factory listerresyncPeriod=10h45mβ¦β controller-runtime manager cache
Scope: Affects vpol and ivpol engines in steady-state (not just startup). The mpol engine uses the same celExceptionLister pattern and is similarly exposed.
Related Known Issues#
| Issue | Description |
|---|---|
| #16989 | Active bug β dual-informer race causing PolicyException to be permanently ignored per-replica |
| #16952 | PolicyException.matchConditions not rewritten for autogen targets (separate defect, same code path) |
| #16281 | Startup race β policies ignored on webhook start before initial reconcile completes; fixed in PR #16434 |
Key Source Files#
| File | Role |
|---|---|
pkg/cel/engine/exception.go | PolicyExceptionLister interface, NewPolicyExceptionLister, ListExceptions |
pkg/cel/policies/vpol/engine/provider.go | NewProvider (static) and NewKubeProvider (dynamic) for vpol; exception watcher registration |
pkg/cel/policies/ivpol/engine/provider.go | Same pattern for ivpol |
pkg/cel/policies/vpol/engine/reconciler.go | vpol reconciler: fetches exceptions via lister, compiles, stores under RWMutex |
pkg/cel/policies/ivpol/engine/reconciler.go | ivpol reconciler: same pattern, stores raw exceptions (not compiled) on Policy struct |
cmd/kyverno/main.go | Wiring: celExceptionLister construction and provider setup |