#142501·Kubernetes

Document that matchConditions cannot use namespaceObject or variables

Author: JaydipGabaniCreated Sep 28, 2026Updated Sep 28, 2026
Labelskind/documentationsig/api-machineryarea/admission-controlneeds-triage

What happened?

The docs imply that admission policy matchConditions can use every CEL variable that validation expressions can. Two of those variables don't work in match conditions:

  1. namespaceObject compiles but is always null. The CEL environment declares namespaceObject for every expression type (compile.go#L256). So a match condition that references it passes API validation. At runtime, however, the shared match condition evaluator always passes a nil namespace (matcher.go#L85). This happens even for ValidatingAdmissionPolicy, where the namespace has already been fetched and is passed to validations (validator.go#L104 vs #L127).
    • With failurePolicy: Fail, every matching request is rejected with no such key: metadata.
    • With failurePolicy: Ignore, the policy is silently skipped.
  2. variables is rejected when the policy is created. API validation compiles match conditions with a stateless compiler that has no variables declared (validateMatchConditionsExpression). This matches the API field docs for spec.variables ("…except MatchConditions because MatchConditions are evaluated before the rest of the policy"). It contradicts the website docs.

Where the docs mislead:

  • kubernetes.io, ValidatingAdmissionPolicy › Matching requests: matchConditions (source):

    Match conditions have access to the same CEL variables as validation expressions.

  • API reference for MatchCondition.expression (v1/types.go#L1570-L1583): the doc lists the available variables but doesn't say that namespaceObject is unavailable. Users reasonably assume the "Validation" variable list applies, since namespaceObject compiles.

This has been the behavior since namespaceObject was introduced in v1.28 (#118267). It affects ValidatingAdmissionPolicy, MutatingAdmissionPolicy, and the matchConditions of validating/mutating webhook configurations.

What did you expect to happen?

The docs should state which variables match conditions can use:

  • object, oldObject, request, authorizer, authorizer.requestResource, and params (policies only).
  • Not namespaceObject: it is not populated for match conditions and always evaluates to null.
  • Not variables: it is rejected when the policy is created.

It should also point users to the supported alternatives: namespaceSelector on the policy or binding for namespace labels, or a guarded check inside validations, where namespaceObject is available.

How can we reproduce it (as minimally and precisely as possible)?

Against k8s.io/apiserver@master (v0.0.0-20260928183837-a21d4fc0471f). The program compiles expressions the same way API validation does, then runs the shared match condition evaluator for a Pod in a namespace labelled env: prod:

repro main.go
go
package main

import (
	"context"
	"fmt"

	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apiserver/pkg/admission"
	plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
	"k8s.io/apiserver/pkg/authentication/user"
	"k8s.io/apiserver/pkg/cel/environment"
	"k8s.io/apimachinery/pkg/util/version"
)

func main() {
	env := environment.MustBaseEnvSet(version.MajorMinor(1, 36))
	opts := plugincel.OptionalVariableDeclarations{HasAuthorizer: true}

	// Same compiler shape API validation uses for matchConditions (stateless, no `variables`).
	compiler := plugincel.NewCompiler(env)
	for _, expr := range []string{
		`namespaceObject.metadata.labels["env"] == "prod"`,
		`variables.foo == "bar"`,
	} {
		mc := &matchconditions.MatchCondition{Expression: expr}
		res := compiler.CompileCELExpression(mc, opts, environment.NewExpressions)
		fmt.Printf("compile %-50q -> error=%v\n", expr, res.Error)
	}

	mc := admissionregistrationv1.MatchCondition{Name: "ns-is-prod", Expression: `namespaceObject.metadata.labels["env"] == "prod"`}
	cond := plugincel.NewConditionCompiler(env).CompileCondition(
		[]plugincel.ExpressionAccessor{(*matchconditions.MatchCondition)(&mc)}, opts, environment.StoredExpressions)

	pod := &corev1.Pod{TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"}, ObjectMeta: metav1.ObjectMeta{Name: "p", Namespace: "prod-ns"}}
	gvk := schema.GroupVersionKind{Version: "v1", Kind: "Pod"}
	attr := admission.NewAttributesRecord(pod, nil, gvk, "prod-ns", "p", schema.GroupVersionResource{Version: "v1", Resource: "pods"}, "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{Name: "u"})
	va := &admission.VersionedAttributes{Attributes: attr, VersionedKind: gvk, VersionedObject: admission.NewLazyObject(pod)}
	for _, fp := range []admissionregistrationv1.FailurePolicyType{admissionregistrationv1.Fail, admissionregistrationv1.Ignore} {
		fp := fp
		r := matchconditions.NewMatcher(cond, &fp, "policy", "validate", "demo").Match(context.Background(), va, nil, nil)
		fmt.Printf("runtime failurePolicy=%-6s -> Matches=%v Error=%v\n", fp, r.Matches, r.Error)
	}
}
compile "namespaceObject.metadata.labels[\"env\"] == \"prod\"" -> error=<nil>
compile "variables.foo == \"bar\""                         -> error=compilation failed: ERROR: <input>:1:1: undeclared reference to 'variables' (in container '')
runtime failurePolicy=Fail   -> Matches=false Error=expression 'namespaceObject.metadata.labels["env"] == "prod"' resulted in error: no such key: metadata
runtime failurePolicy=Ignore -> Matches=false Error=<nil>

Equivalent policy (illustrative; not yet run against a live cluster):

yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: demo-ns-match
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE"]
      resources: ["configmaps"]
  matchConditions:
  - name: prod-namespaces-only
    expression: 'namespaceObject.metadata.labels["env"] == "prod"'
  validations:
  - expression: 'true'

Based on the code path above, the API server should accept this policy. With a binding in place, every namespaced ConfigMap create should then be rejected with no such key: metadata.

Proposed documentation changes

  1. kubernetes/website, validating-admission-policy.md: replace "Match conditions have access to the same CEL variables as validation expressions." with an explicit list, plus a note that namespaceObject is always null and variables cannot be used. Mirror the note in mutating-admission-policy.md.

  2. kubernetes/kubernetes, MatchCondition.expression godoc in admissionregistration/{v1,v1beta1,v1alpha1}/types.go and the internal type: add something like

    'namespaceObject' is not available in match conditions and always evaluates to null. 'variables' cannot be referenced.

    Then regenerate the OpenAPI, proto, and swagger docs.

Out of scope (possible follow-ups, for SIG input)

  • Warning at create/update time (WarningsOnCreate/WarningsOnUpdate) when a match condition references namespaceObject.
  • Whether namespaceObject should actually be populated for policy match conditions. KEP-3488 currently says match conditions have "no referential lookup" (README#L1314), so this would need a KEP update.

Kubernetes version

v1.28 onward; verified on master at 8d3071315a65c453c861d06e9ad66d036a6413ec.

/kind documentation /sig api-machinery /area admission-control