Background Controller Trigger Validation#
The background controller resolves trigger resources for UpdateRequest processing using a multi-tier fallback strategy. The entry point is GetTrigger(), which routes to operation-specific helpers or directly to GetResource() depending on whether an AdmissionRequest is present and what operation it represents.
How Trigger Resolution Works#
GetResource() implements the core lookup in three ordered tiers :
-
UID-based list-and-scan (lines 95β105): If the
ResourceSpeccarries a non-empty UID, the function callsclient.ListResource()for the given API version, kind, and namespace, then scans items for a UID match. -
Name-based direct fetch (lines 106β121): If UID is absent or if the UID scan returns no match, the function falls through to a
client.GetResource()call by name. There is no error or warning emitted when the UID scan fails to match β execution silently proceeds to the name lookup. -
Admission request raw body (lines 123β136): If neither lookup returns a resource and an
AdmissionRequestis attached to theUpdateRequestSpec, the trigger is reconstructed from the raw object bytes in the request body.
The Silent Fallback: A Correctness Concern#
When a UID is present but no live resource matches it (e.g., the triggering resource was deleted, or the admission request was rejected by the API server), GetResource() does not surface an error β it silently falls through to the name-based lookup . This means the controller may act on a different resource that shares the trigger's name but not its UID.
This creates a known correctness bug . Consider the following scenario:
- A
CREATE Namespaceadmission request is issued byhelm install --create-namespaceagainst an already-existing namespace. - Before the API server rejects the request with
AlreadyExists, Kyverno's admission webhook has already processed it: a mutate policy defaults a label (e.g.,layer: business) onto the phantom object, which then matches a generate rule and enqueues anUpdateRequest. - The UR stores the phantom UID (
spec.ruleContext[].trigger.uid) β different from the live namespace's UID. - The API server rejects the CREATE; the UR survives.
- The background controller calls
GetResource(), finds no UID match for the phantom UID, and silently falls back to the name-based fetch β returning the live namespace, which haslayer: operationaland does not satisfy the rule's selector. - The generate rule executes and creates the downstream resource (e.g., a
LimitRange) in the live namespace.
The UpdateRequest itself contains all data needed to detect the mismatch β spec.context.admissionRequestInfo.admissionRequest.object.metadata.uid β but no comparison is performed before UR execution .
Downstream Cleanup: The Same Pattern#
The same UID β name fallback exists in the generate cleanup path. getDownstreams() builds a label selector that includes generate.kyverno.io/trigger-uid , then calls fetch(). If FindDownstream() returns an empty list for the UID selector, fetch() removes the trigger-uid label, substitutes generate.kyverno.io/trigger-name, and re-queries β with no validation that the name-matched resources actually belong to the original trigger. An identical fallback applies to CloneList resources .
The trigger labels themselves are stamped onto generated resources via TriggerInfo(), which sets both the trigger-uid and trigger-name labels. The 63-character label value limit causes trigger names to be trimmed β this is the historical reason the name-based fallback exists, to handle resources whose trimmed name-label would not match a full UID search.
Key Files#
| File | Purpose |
|---|---|
pkg/background/common/resource.go | GetTrigger() and GetResource() β trigger resolution with UID/name/raw fallback |
pkg/background/generate/cleanup.go | getDownstreams() / fetch() β label-based downstream lookup with same UID β name fallback |
pkg/background/common/labels.go | TriggerInfo() and ManageLabels() β stamps trigger metadata labels on generated resources |
pkg/background/common/constants.go | Label key constants: generate.kyverno.io/trigger-uid, trigger-name, etc. |
Known Issue & Workarounds#
Issue #16566 documents the bug. Confirmed in Kyverno v1.16.x through v1.18.1; no fix merged as of July 2026.
Suggested fix (from issue): before executing a generate UR, the background controller should fetch the trigger object from the cluster, compare its UID to the UR's stored trigger UID, and mark the UR Skipped or Failed on mismatch rather than proceeding.
Operational workarounds :
- Guard mutate defaults with an
apiCallprecondition that checks whether the resource already exists in the cluster, so phantom objects never receive matching labels. - Use
apiCall-based preconditions in generate rules that read live resource labels instead of relying onrequest.object.