Document that matchConditions cannot use namespaceObject or variables
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:
namespaceObjectcompiles but is alwaysnull. The CEL environment declaresnamespaceObjectfor 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 anilnamespace (matcher.go#L85). This happens even for ValidatingAdmissionPolicy, where the namespace has already been fetched and is passed to validations (validator.go#L104vs#L127).- With
failurePolicy: Fail, every matching request is rejected withno such key: metadata. - With
failurePolicy: Ignore, the policy is silently skipped.
- With
variablesis rejected when the policy is created. API validation compiles match conditions with a stateless compiler that has novariablesdeclared (validateMatchConditionsExpression). This matches the API field docs forspec.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 thatnamespaceObjectis unavailable. Users reasonably assume the "Validation" variable list applies, sincenamespaceObjectcompiles.
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, andparams(policies only).- Not
namespaceObject: it is not populated for match conditions and always evaluates tonull. - 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:
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):
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
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 thatnamespaceObjectis alwaysnullandvariablescannot be used. Mirror the note inmutating-admission-policy.md.kubernetes/kubernetes,
MatchCondition.expressiongodoc inadmissionregistration/{v1,v1beta1,v1alpha1}/types.goand 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 referencesnamespaceObject. - Whether
namespaceObjectshould 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
Source: kubernetes/kubernetes