# Serves: R-NET-05 (egress is a declared property) + R-IDP-02 (auth mode is declared) — # this policy does NOT weaken either requirement. It makes the one class of pod that # CANNOT carry the labels from git SATISFY them, instead of being exempted from them. # # THE DEFECT IT FIXES (observed on LIVE prod, 2026-09-05/06): # `DataVolume unifi/unifi-rootdisk` sat in ImportScheduled for hours holding a detached # 40Gi `prime-…` scratch PVC. `kubectl -n cdi logs deploy/cdi-deployment` showed: # admission webhook "validate.kyverno.svc-fail" denied the request: resource # Pod/unifi/importer-prime- was blocked due to the following policies # require-auth-mode-label / require-egress-class-label # CDI generates its own short-lived worker pods (importer-*, uploader-*, cloner-*/ # clone-source-*) from inside the cdi-deployment controller. Those pods are not in git, # so nothing can put the platform labels on them, and both label policies match ANY Pod # in a namespace labelled homelab.keithjackson.dev/app-namespace=true. Result: the VM # never gets a disk. This is a COLD-START defect, not a prod quirk — dev's identical # import Succeeded before the policies were enforcing, so dev has never exercised it and # would hit it on any rebuild. # # WHY MUTATE (and not an exception/exclusion): # The honest reading of R-NET-05/R-IDP-02 is "every pod's egress path and auth posture is # DECLARED". A CDI importer pod has truthful values for both: # egress-class: internet — it pulls the image from the public internet over https # (cloud-images.ubuntu.com / quay.io) and must take the # default WAN path, which is exactly what Cilium's egress # gateway does for `internet` (ADR-0013/ADR-0015). It is NOT # a `vpn` workload. # auth-mode: none — the pod serves no UI and terminates when the import ends; # there is nothing to put an IdP in front of. docs/13 permits # `none` for internal, non-public workloads (the # ServiceDefinition policy forbids only public + none). # So the requirement is SATISFIED rather than bypassed: the invariant "every pod in an app # namespace carries both labels" stays literally true cluster-wide, both label policies # stay untouched (no `exclude`, no PolicyException, background scanning still on), and the # labels are functional — the pod is now selectable by the Cilium egress fabric instead of # being an unlabelled pod that silently falls through to the default path. # # THE MATCH IS THE GENERATING ServiceAccount, NOT A NAME PREFIX: # `subjects: ServiceAccount cdi/cdi-sa` is the identity in the AdmissionReview's userInfo — # the controller that issued the CREATE. Verified on prod from the Kyverno admission log of # the blocked request: # user={"username":"system:serviceaccount:cdi:cdi-sa", ...} roles=["cdi:cdi-deployment"] # name=importer-prime-be3f9d1c-… namespace=unifi operation=CREATE # A pod NAME (`importer-*`) or CDI's own labels (`cdi.kubevirt.io`) are attacker-CHOOSABLE: # anyone who can create a pod in the namespace can name it `importer-prime-x` and copy the # labels, and would then inherit the exemption. The requesting ServiceAccount cannot be # chosen by the pod author — only by someone who already holds cdi-sa's token, which already # confers "create any pod in any namespace" cluster-wide. So this adds no reachable # privilege. It is also VERSION-ROBUST: it covers every CDI worker kind (importer, uploader, # cloner/clone-source) without enumerating names that change between CDI releases — # confirmed on prod that cdi-sa is the ONLY cdi-namespace ServiceAccount that can create # pods (`kubectl auth can-i create pods -n unifi --as …`: cdi-sa yes; cdi-apiserver no; # cdi-uploadproxy no). # # DELIBERATELY NOT COVERED — KubeVirt's own generated pods: # virt-launcher-* pods INHERIT the VM's pod-template labels, so a VM authored in this repo # already passes (verified on dev: virt-launcher-unifi-22dg7 carries egress-class=internet, # auth-mode=native from apps/vms/unifi/30-virtualmachine.yaml). Exempting # kubevirt-controller would REMOVE that useful enforcement on VM authors, so it is not done. # KubeVirt's hotplug (`hp-volume-*`) and export (`virt-export-*`) pods do NOT inherit those # labels, but nothing in this repo uses volume hotplug or VirtualMachineExport; if either is # adopted, extend this policy then — with the same ServiceAccount-scoped shape. # # OPERATOR NOTE — this is the cluster's FIRST resource-MUTATING policy. Verified on prod # 2026-09-06 that `kyverno-resource-mutating-webhook-cfg` currently holds 0 webhooks; with the # chart's `autoUpdateWebhooks=true` Kyverno registers `mutate.kyverno.svc-fail` for Pods within # seconds of this policy being applied. Prod runs `forceFailurePolicyIgnore=false`, so that # webhook is failurePolicy: Fail — the same posture as the validating webhook that is ALREADY in # the path for these pods, so no new fail-open surface and no new blocking surface. CDI retries # the importer pod on a loop, so the stuck import proceeds once the policy lands (the detached # `prime-…` scratch PVC left behind by the blocked attempts is a separate operator cleanup). # # RESIDUAL TRADE-OFF (state it plainly, it is reversible by deleting this file): # Anything created by `cdi:cdi-sa` in an app namespace now gets `egress-class: internet` + # `auth-mode: none` supplied for it instead of being rejected for not declaring them. Whoever # holds cdi-sa's token can therefore run a pod in an app namespace without declaring its egress # class. That identity can already create arbitrary pods in every namespace (ClusterRole `cdi`: # pods create/delete cluster-wide), so it is not a new capability — but the declaration is now # assumed rather than demanded for that one identity. Nothing else changes: the pod is still # subject to restrict-host-namespaces, the namespace default-deny baseline, and every other # policy, and `internet` is the LEAST-reach of the two egress classes (it does not grant the VPN # path). # # PROOF (offline, no cluster — platform/kyverno/tests/controller-worker-pods/): # (a) a CDI importer pod created by cdi:cdi-sa is mutated and then PASSES both policies; # (b) the SAME namespace's ordinary pod — including one that FORGES the importer name and # CDI's labels — is NOT mutated and is still DENIED by both policies. apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: label-cdi-worker-pods annotations: policies.kyverno.io/title: Label CDI-generated worker pods policies.kyverno.io/category: Network Isolation policies.kyverno.io/severity: medium homelab.keithjackson.dev/requirements: R-NET-05,R-IDP-02 policies.kyverno.io/description: >- Adds homelab.keithjackson.dev/egress-class=internet and auth-mode=none to the short-lived worker Pods that the CDI controller (ServiceAccount cdi/cdi-sa) generates in application namespaces, so they satisfy require-egress-class-label and require-auth-mode-label instead of being exempted from them. spec: # No validationFailureAction: this is a MUTATE policy, there is nothing to fail. # background MUST be false: `match.subjects` is userInfo, which exists only on an # AdmissionReview. Kyverno rejects a userInfo match with background scanning enabled # ("invalid variable used at path ... policy contains 'userInfo'"), and a background scan # of an already-created pod has no requester to match anyway. background: false rules: - name: add-worker-pod-labels match: # One filter entry: the resource conditions AND the subject must both match. all: - resources: kinds: - Pod operations: - CREATE namespaceSelector: matchLabels: homelab.keithjackson.dev/app-namespace: "true" subjects: # The CDI controller (cdi-deployment) ServiceAccount — the identity that # CREATEs importer-*/uploader-*/cloner-* pods. Not the pods' own SA. - kind: ServiceAccount namespace: cdi name: cdi-sa mutate: patchStrategicMerge: metadata: labels: # `+(key)` is Kyverno's ADD-IF-ABSENT anchor: an explicit value already on the # pod is never overwritten (so a future CDI worker that legitimately needs # egress-class: vpn can be given it, and virt-launcher-style inherited labels # would survive). +(homelab.keithjackson.dev/egress-class): internet +(homelab.keithjackson.dev/auth-mode): none