Admission Policy CEL Evaluation#
Overview#
The pkg/admissionpolicy package evaluates native Kubernetes ValidatingAdmissionPolicy (VAP) and MutatingAdmissionPolicy (MAP) resources by constructing a k8s.io/apiserver/pkg/admission.Attributes record and passing it to the upstream CEL evaluation machinery. A critical requirement for correct CEL evaluation is that request.userInfo.username β and all other UserInfo fields β must be present in the activation map the CEL engine reads. During live admission a real user is always present; during background scans (reports controller) there is no admission user, which historically caused "no such key: username" failures.
The Root Cause: omitempty and Serialization#
authenticationv1.UserInfo.Username carries a json:"username,omitempty" tag. When the admission request is serialized into the map[string]any that the CEL engine consumes, an empty username is silently dropped. The resulting CEL map has no username key at all β so expressions like request.userInfo.username fail at runtime rather than returning an empty string. This affects any VAP or MAP that directly references request.userInfo.username without a has() guard, including GKE's built-in validating-node-p4sa-audience policy (present on GKE β₯ 1.32) .
Solution: Sentinel Username and ResolveUser#
The fix, landed in PR #16561, introduces two constructs in pkg/admissionpolicy/user_info.go:
-
backgroundUsernameβ a sentinel constant"system:kyverno:background-scan". It is intentionally not a valid service account name (nosystem:serviceaccount:prefix) so it does not collide with real-user allowlists or denylists in policies. -
ResolveUser(userInfo *authenticationv1.UserInfo) UserInfoβ copies all providedUserInfofields (groups, uid, extra) and substitutesbackgroundUsernameonly when the incoming username is empty . When a real username is present it is preserved unchanged.
Both Validate and Mutate now call ResolveUser instead of raw NewUser, so both nil and empty-struct UserInfo inputs are handled consistently:
Validateβuser := ResolveUser(userInfo)before buildingadmission.NewAttributesRecord.Mutateβ identical pattern.
UserInfo Adapter#
UserInfo wraps authenticationv1.UserInfo and implements the k8s.io/apiserver/pkg/authentication/user.Info interface required by admission.NewAttributesRecord. Its four methods (GetName, GetUID, GetGroups, GetExtra) delegate directly to the underlying struct fields. NewUser is the lower-level constructor that wraps a UserInfo value as-is; ResolveUser is the higher-level entry point that applies the sentinel fallback.
How admission.Attributes Flows Into CEL#
Both Validate and Mutate construct a single admission.Attributes record at the start of evaluation and pass it to all downstream helpers (validateResource, mutateResource, their matcher variants). Inside those helpers:
admission.NewVersionedAttributes(a, ...)wraps the record for the upstream Kubernetes CEL validator/mutator.- The validator (VAP) calls
validator.Validateand the mutator (MAP) callsmatcher.Matchandpatcher.Patchβ all receiving the sameversionedAttributes. request.userInfois surfaced in the CEL activation map by the upstream Kubernetes library serializingversionedAttributes.RequestInfoβ so the username must be non-empty beforeNewVersionedAttributesis called.
Bug Fix History (UserInfo / CEL)#
| PR | What changed |
|---|---|
| #15449 | Switched background scanner from nil to &UserInfo{} β insufficient because empty username is still omitted by omitempty |
| #15619 | Wired admissionpolicy.NewUser(request.Request.UserInfo) into admission.Attributes for live admission paths |
| #16561 | Introduced ResolveUser + backgroundUsername; fixes background scan "no such key: username" for both VAP and MAP |
Key Files#
| File | Purpose |
|---|---|
pkg/admissionpolicy/user_info.go | UserInfo adapter, NewUser, ResolveUser, backgroundUsername |
pkg/admissionpolicy/validate.go | Validate entry point and VAP evaluation helpers |
pkg/admissionpolicy/mutate.go | Mutate entry point and MAP evaluation helpers |