NamespaceSelector Policy Enforcement#
namespaceSelector inside matchConstraints appears in all CEL-based policy types (ValidatingPolicy, MutatingPolicy, GeneratingPolicy, ImageValidatingPolicy) and is supposed to restrict which namespaces a policy acts on. In practice, several independent bugs across the admission pipeline cause the selector to be silently ignored or to produce runtime errors. This article maps each failure mode to its root cause, affected components, and workaround.
Bug 1: matchConstraints Dropped During CEL Compilation (Fixed)#
Symptoms: ValidatingPolicy, MutatingPolicy, and GeneratingPolicy with matchConstraints (including namespaceSelector) evaluate against empty constraints, producing spurious Excluded/pass results regardless of the selector.
Root cause: The CEL compilers for vpol, mpol, and gpol initialized the compiled Policy struct but never copied spec.MatchConstraints into it. The engine then called matchPolicy(nil, ...), which immediately returned false, treating every resource as excluded.
Fix β PR #14599 (merged 2026-02-10): The compiler was updated to store spec.MatchConstraints in the compiled Policy struct for all three policy types, and the engine switched from reading the spec directly to calling policy.CompiledPolicy.MatchConstraints(). Changed files: pkg/cel/policies/vpol/compiler/policy.go, compiler.go, engine/engine.go; same pattern in mpol and gpol .
Bug 2: Cached Namespace Resolver Returns nil, Crashing CEL Expressions#
Symptoms: namespaceObject.metadata.labels (and any namespaceObject.* access) in matchConditions or validations raises no such key: metadata during live admission, rejecting requests that should be allowed. No error appears in Kyverno logs.
Root cause (issue #16518): PR #13570 ("Improve VPOL performance") replaced a live CoreV1().Namespaces().Get() call with a cached lister. On a lister cache miss, the resolver returns nil β the engine passes this directly to CEL as namespaceObject = null. Because null has no metadata key, any label access errors at runtime. With validationActions: [Deny], the failing matchCondition causes the admission request to be denied .
The error is swallowed silently (if err != nil { return nil } in the resolver), which is why no logs appear .
Engine location: The nsResolver is a engine.NamespaceResolver function wired into engineImpl at construction time. Namespace resolution runs in Handle().
Workarounds (until fixed):
- Use optional chaining:
namespaceObject.?metadata.labels['my-label'].orValue('')to avoid hard errors onnull. - Move namespace filtering from
matchConditionsinto amatchConstraints.namespaceSelector(but see Bug 3 below for autogen implications).
Proposed fix (community branch rx18-eng/kyverno:fix/vpol-namespaceobject-nil): Fall back to a minimal synthetic namespace (name + kubernetes.io/metadata.name label) instead of null on a cache miss, across vpol, mpol, gpol, dpol, and image verification engines .
Bug 3: Autogen Blocked by namespaceSelector in matchConstraints#
Symptoms (issue #16175): When a ValidatingPolicy (or MutatingPolicy) sets matchConstraints.namespaceSelector, status.autogen is empty β no pod-controller variants are generated. The same policy with namespace filtering expressed via matchConditions autogenerates correctly.
Root cause β two independent code paths:
-
CanAutoGenguard inpkg/cel/autogen/support.go: a non-nilnamespaceSelectorwith any labels/expressions causes the function to returnfalse, aborting autogen entirely. -
CreateMatchConstraintsdropsnamespaceSelectorinpkg/cel/autogen/match.go: even if autogen runs, the generated spec sets onlyResourceRulesβNamespaceSelectoris never copied to the output.
Community-proposed fix: Remove the NamespaceSelector guard from CanAutoGen; add a namespaceSelector parameter to CreateMatchConstraints and propagate it at all three call sites (vpol, ivpol, mpol) . The ObjectSelector guard is intentionally kept β object labels don't translate meaningfully across resource kinds.
Side-effect of the current workaround: Using matchConditions for namespace filtering keeps autogen working but generates Policy Report entries for every namespace (including filtered-out ones), creating noise in multi-tenant clusters .
Bug 4: NamespacedMutatingPolicy β namespaceSelector Not Enforced at Webhook Level#
Symptoms (issue #16580): A NamespacedMutatingPolicy with matchConstraints.namespaceSelector targeting a single namespace mutates resources in all namespaces. The kyverno-policy-mutating-webhook-cfg webhook only excludes kube-system and kyverno; the policy-level selector is ignored.
Root cause: For namespaced policies, resolveNamespaceSelector pins the webhook to the policy's own namespace via kubernetes.io/metadata.name β the matchConstraints.namespaceSelector is not additionally ANDed in. The engine therefore receives admission requests from every namespace allowed by the global webhook filter and applies the policy without further namespace gating.
Workaround: Add an explicit CEL guard in the mutation expression:
namespaceObject.metadata.name == "<target-namespace>" && <your-mutation-condition> ? Object{...} : Object{}
Bug 5: Cluster-Scoped Resources Bypass namespaceSelector#
Symptoms (issue #16127): A ValidatingPolicy with matchConstraints.namespaceSelector correctly blocks resources in the targeted namespace but also blocks cluster-scoped resources (e.g., ClusterRole) β which have no namespace at all.
Root cause: The Kubernetes admission machinery passes cluster-scoped resources with an empty namespace field. A webhook whose namespaceSelector targets a specific namespace label has no label to evaluate for these requests. Kyverno forwards them to the engine anyway, which then evaluates the policy without a namespace context to exclude them .
Webhook Configuration: How namespaceSelector Reaches the Webhook#
For cluster-scoped policies, resolveNamespaceSelector merges matchConstraints.NamespaceSelector with the global webhook NamespaceSelector from config. "Basic" policies (no matchConditions, no Exact matchPolicy, no custom timeout) are grouped by their computed (namespaceSelector, objectSelector) pair via groupBySelectors, preventing policies with different selectors from sharing a webhook and silently overwriting each other's filters. "Fine-grained" policies get their own per-policy webhook entry .
Key Source Files#
| File | Relevance |
|---|---|
pkg/cel/autogen/support.go | CanAutoGen β blocks autogen when namespaceSelector is set |
pkg/cel/autogen/match.go | CreateMatchConstraints β drops namespaceSelector in generated specs |
pkg/controllers/webhook/validating.go | Webhook builder: resolveNamespaceSelector, groupBySelectors |
pkg/cel/policies/vpol/engine/engine.go | vpol engine: Handle(), matchPolicy(), namespace resolver call |
pkg/cel/policies/vpol/compiler/policy.go | Compiled Policy struct + MatchConstraints() getter (added by PR #14599) |
Bug Tracker Summary#
| Issue | Policy Type | Status |
|---|---|---|
| #14556 / PR #14599 | vpol, mpol, gpol | Fixed (v1.18+, merged 2026-02-10) |
| #16518 | vpol (+ mpol, gpol) | Open β cache-miss nil crash |
| #16175 | vpol, mpol (autogen) | Open β autogen blocked + namespaceSelector dropped |
| #16580 | NamespacedMutatingPolicy | Open β webhook not filtered |
| #16127 | ValidatingPolicy | Open β cluster-scoped bypass |