Background Controller UpdateRequest Processing#
Overview#
The Kyverno background controller processes UpdateRequest (UR) CRDs to execute mutateExisting and generateExisting rules against live cluster resources β outside the admission webhook flow. This article covers how URs are initialized (by the policy controller on policy add/update), how triggers are matched against policy criteria (including the operations field), and key bugs and fixes specific to CEL-based (MutatingPolicy/GeneratingPolicy) pipelines.
UpdateRequest Types and Creation#
There are three UR types relevant to background processing:
| UR Type | Policy Kind | Builder |
|---|---|---|
Mutate | ClusterPolicy/Policy (mutateExisting) | newMutateUR() |
Generate | ClusterPolicy/Policy (generateExisting) | newGenerateUR() |
CELGenerate | GeneratingPolicy/NamespacedGeneratingPolicy | newGenerateUR() |
CEL mutate URs (CELMutate) are created by newCELMutateUR() / newCELMutateURFromNamespacedPolicy() in pkg/policy/mpol.go, added by PR #16255.
All URs live in the Kyverno namespace with a ur- generateName prefix . Mutate URs are deduplicated via label-based lookup with MutateLabelsSet(policyKey, trigger) before creation . Generate URs do not have an equivalent per-trigger dedup guard .
Policy Controller: Initializing URs on Policy Events#
mutateExisting#
handleMutate() fires when a ClusterPolicy/Policy is added or updated. For each rule with HasMutateExisting() and MutateExistingOnPolicyUpdate: true, it:
- Calls
getTriggers()to list candidate trigger resources by kind/namespace selector. - Calls
listMutateURs(policyKey, trigger)and skips creation if a UR already exists (deduplication). - Calls
handleUpdateRequest()βengine.ApplyBackgroundChecks()to validate the trigger actually matches the rule before creating the UR.
generateExisting#
handleGenerateForExisting() iterates rules with generateExisting: true (rule-level or policy-level). Per trigger it builds a PolicyContext via common.NewBackgroundContext() and calls engine.ApplyBackgroundChecks() to filter non-matching resources before appending to the UR's RuleContext. URs are then batched at 100 entries (generateBatchSize) to avoid etcd size limits .
PolicyContext Construction: NewBackgroundContext()#
NewBackgroundContext() builds the engine.PolicyContext for background rule evaluation:
- With an
AdmissionRequestin the UR context: callsengine.NewPolicyContextFromAdmissionRequest(), extractingnew/oldresources from the raw request body . - Without an
AdmissionRequest: callsengine.NewPolicyContext()using the operation stored inurContext.AdmissionRequestInfo.Operation. - Validates the admission-request resource matches the live trigger resource (name/namespace/kind/apiVersion check via
check()). - Sets
WithAdmissionOperation(false)to mark the context as background .
Operation Field Handling in Match/Exclude#
MatchesResourceDescription() is the entry point for matching a resource against a rule's match/exclude blocks. It delegates to doesResourceMatchConditionBlock(), which checks the operations field first :
if len(conditionBlock.Operations) > 0 {
if !slices.Contains(conditionBlock.Operations, operation) {
return []error{"operation does not match"}
}
}
In background contexts, the operation value comes from urContext.AdmissionRequestInfo.Operation β which for policy-controller-initiated URs is typically empty/"". If a rule's match block specifies operations: [CREATE], the resource will fail to match unless the UR carries a valid Operation. This is the root cause of the CEL-policy operation field bug fixed by PR #16255.
CEL-Based Policy Fixes (PR #16255)#
PR #16255 wired MutatingPolicy and NamespacedMutatingPolicy into the background controller's UR pipeline and fixed three specific issues:
1. Synthetic AdmissionRequest for Background URs#
Background-scan URs carry no admission request. The fix in pkg/background/mpol/processor.go constructs a synthetic AdmissionRequest per target resource, setting Operation: Update and populating Object.Raw, Kind, Resource, Namespace, and Name. This ensures CEL variables like request.object correctly reflect the target .
2. NamespacedMutatingPolicy API Routing#
The cluster-scoped MutatingPolicy API rejects "namespace/name" keys with a 404. GetPolicy() now routes namespace/name keys directly to the NamespacedMutatingPolicy API. For backward compatibility with older URs that store bare policy names, a fallback uses AdmissionRequest.Namespace to locate the namespaced policy .
3. Scope Predicate from Resolved Policy Object#
The processor derives the scopePredicate from the resolved policy object, not from the UR key. This prevents a same-named MutatingPolicy (cluster-scoped) and NamespacedMutatingPolicy from cross-matching each other's targets .
CEL Namespace Resolution Bug (Issue #16953)#
A separate, still-open bug affects MutatingPolicy background evaluation. Evaluate() (called by pkg/background/mpol/processor.go) passes nil as the namespace argument to handlePolicy(), while the admission-path Handle() resolves the namespace via nsResolver. Consequently :
namespaceSelectoris silently ignored during background mutation β the selector is evaluated against anilnamespace and always passes.- The
namespaceObjectCEL variable is alwaysnullin background contexts.
The proposed fix mirrors Handle()'s namespace resolution in Evaluate() .
Trigger Validation and the UID/Name Fallback#
GetTrigger() resolves the trigger resource in three ordered tiers: UID-based list-and-scan β name-based direct fetch β raw AdmissionRequest body . The UID fallback is silent: if no live resource matches the UR's stored UID, the controller proceeds with any same-named resource. This can cause generate rules to fire against the wrong resource (Issue #16566, unfixed as of v1.18.1) .
Key Files#
| File | Purpose |
|---|---|
pkg/policy/mutate.go | handleMutate() β creates mutateExisting URs on policy events |
pkg/policy/generate.go | handleGenerateForExisting() β creates generateExisting URs |
pkg/policy/updaterequest.go | UR builders: newMutateUR(), newGenerateUR(), splitUR() |
pkg/background/common/context.go | NewBackgroundContext() β constructs PolicyContext for background rules |
pkg/engine/utils/match.go | MatchesResourceDescription() / doesResourceMatchConditionBlock() β operation field check |
pkg/background/mpol/processor.go | CEL MutatingPolicy background processor (synthetic AdmissionRequest, policy lookup) |
pkg/background/common/resource.go | GetTrigger() / GetResource() β trigger resolution with UID/name/raw fallback |