MutatingPolicy CEL Engine Namespace Resolution#
The MutatingPolicy CEL engine (pkg/cel/policies/mpol/engine/) exposes two evaluation entry points β Handle() and Evaluate() β that resolve the namespace object inconsistently, causing divergent behavior between admission-time and background (mutate-existing) policy evaluation.
The Inconsistency#
Handle() (admission webhook path) explicitly resolves the namespace via e.nsResolver before forwarding to handlePolicy():
var namespace *corev1.Namespace
if ns := request.Request.Namespace; ns != "" {
namespace = e.nsResolver(ns)
}
Evaluate() (background/inline path) passes nil unconditionally :
r, patched := e.handlePolicy(ctx, mpol, attr, request, nil, true)
Because the background processor in pkg/background/mpol/processor.go calls engine.Evaluate() β not Handle() β any policy that relies on namespaceSelector or the namespaceObject CEL variable during background reconciliation receives a nil namespace context.
Impact#
namespaceSelector matching is silently broken for mutate-existing policies. When handlePolicy() calls e.matcher.Match(...), the matcher receives a nil namespace and therefore cannot evaluate labels against the selector. The result is that the selector is ignored: mutations are applied to all matching resources regardless of namespace labels .
namespaceObject CEL variable is always null in Evaluate(). The prepareData() function in pkg/cel/policies/mpol/compiler/eval.go populates the namespaceObject CEL activation key from whatever *corev1.Namespace it receives. When Evaluate() passes nil, CEL expressions referencing namespaceObject.metadata.labels (or any sub-field) will fail at runtime rather than returning expected values.
Affected scenarios:
| Scenario | Calls | Namespace resolved? |
|---|---|---|
| Admission webhook (inline mutation) | Handle() | β
Yes, via nsResolver |
Background scan (mutate-existing) | Evaluate() via processor.go | β No β hardcoded nil |
Namespace Data Flow#
The namespace object flows top-down through the call stack:
Handle() / Evaluate()
ββ handlePolicy(β¦, namespace *corev1.Namespace, β¦) [engine.go:181]
ββ CompiledPolicy.Evaluate(β¦, namespace, β¦) [policy.go]
ββ prepareData(attr, request, namespace) [eval.go:23-66]
ββ compiler.NamespaceObjectKey β CEL activation map
prepareData() converts the namespace via utils.ObjectToResolveVal(namespace) and injects it at compiler.NamespaceObjectKey. A nil input produces a null CEL value rather than a panic β the failure is silent at the matching layer .
Proposed Fix (Issue #16953)#
The open bug report proposes mirroring Handle()'s namespace resolution in Evaluate():
+var namespace *corev1.Namespace
+if ns := attr.GetNamespace(); ns != "" {
+ namespace = e.nsResolver(ns)
+}
for _, mpol := range mpols {
if predicate != nil && predicate(mpol.Policy) {
- r, patched := e.handlePolicy(ctx, mpol, attr, request, nil, true)
+ r, patched := e.handlePolicy(ctx, mpol, attr, request, namespace, true)
Note that existing tests for Evaluate() use a synthetic nsResolver but do not catch this bug because Evaluate() never calls e.nsResolver() β fixing this will also require updating those test cases .
Historical Context#
namespaceObject support in MutatingPolicy was added in PR #15625; before that, the variable was absent from the CEL activation map entirely . The current bug is the second generation of this class of missing-namespace fix β the engine correctly wires the resolver in Handle() but forgot to replicate it in Evaluate().
A similar class of issue was filed for ValidatingPolicy admission matchConditions , suggesting namespace resolution gaps are a recurring pattern across CEL policy types. See the CEL Context Injection knowledge article for the full fix history across policy types.
Key Files#
| File | Role |
|---|---|
pkg/cel/policies/mpol/engine/engine.go | Handle() and Evaluate() β the divergent entry points |
pkg/cel/policies/mpol/compiler/eval.go | prepareData() β constructs the CEL activation map including namespaceObject |
pkg/background/mpol/processor.go | Background processor that calls engine.Evaluate() for mutate-existing |
| Issue #16953 | Open bug report with root cause analysis and diff |