diff --git a/k8s/bases/infrastructure/controllers/flux-operator/helm-release.yaml b/k8s/bases/infrastructure/controllers/flux-operator/helm-release.yaml index 8b4320e3e..41deabeff 100644 --- a/k8s/bases/infrastructure/controllers/flux-operator/helm-release.yaml +++ b/k8s/bases/infrastructure/controllers/flux-operator/helm-release.yaml @@ -45,6 +45,40 @@ spec: # # Applied here rather than in the FluxInstance because the operator is # installed by Helm, so the FluxInstance's kustomize patches never reach it. + + # C-0211 (CIS-5.7.3) — the two non-privilege fields, supplied at TEMPLATE level. + # + # flux-system is excluded from add-pod-security-context / add-container-security-context + # because its workloads need elevated privilege by design, and the namespace-scoped + # add-baseline-context-optin-* rules cannot close the gap here: all three match + # `kinds: Pod`, so they mutate at ADMISSION and leave the stored workload spec + # Kubescape reads untouched. Only a template-level change moves the measurement — + # the same reason #3541 (velero) and #3544 (the four FluxInstance controllers) were + # done this way rather than by labelling the namespace. + # + # Measured against the live PROD stored specs 2026-09-04: of flux-system's six + # workloads, four controllers already carry both fields from #3544, leaving + # flux-operator and tofu-controller. tofu-controller is retired and pending removal + # in #3480, so flux-operator is the namespace's last workload needing this. + # + # Both fields are inert here, which is the point rather than a caveat — this changes + # a declared posture without changing how the operator runs: + # - fsGroupChangePolicy governs the kubelet's volume ownership pass, which runs only + # when a pod sets fsGroup. This pod sets none, and its only volumes are an emptyDir + # and the projected service-account token — no PVC, no hostPath. + # - seLinuxOptions {level: s0} is the default non-privileged MCS level and a no-op on + # this AppArmor cluster (runc ignores SELinux labels when SELinux is not the active + # LSM), retained as CIS-5.7.3 defense-in-depth on any SELinux node. + # + # Neither carries the runAsGroup-without-runAsUser coupling documented above, so + # neither can stop the sandbox building. + # + # 🔴 The pod-level op adds a LEAF into the chart's pod securityContext, which renders + # as `{}` (read from the live Deployment 2026-09-04). A leaf add is deliberate: adding + # the whole object would REPLACE it and silently discard any field the chart starts + # setting later, which is exactly what the +(key) anchors elsewhere exist to prevent. + # The cost is that a chart that stopped rendering the object would fail this patch — + # loudly, on a Renovate chart bump with CI in front of it, never silently. postRenderers: - kustomize: patches: @@ -61,3 +95,10 @@ spec: - op: add path: /spec/template/spec/containers/0/securityContext/runAsGroup value: 65532 + - op: add + path: /spec/template/spec/containers/0/securityContext/seLinuxOptions + value: + level: s0 + - op: add + path: /spec/template/spec/securityContext/fsGroupChangePolicy + value: OnRootMismatch diff --git a/scripts/validate-eks-ci-role-policy/main.go b/scripts/validate-eks-ci-role-policy/main.go index 52bdf9d22..c8bdfe579 100644 --- a/scripts/validate-eks-ci-role-policy/main.go +++ b/scripts/validate-eks-ci-role-policy/main.go @@ -1655,7 +1655,39 @@ const ( // The previous approved aggregate digest was: // // 686bf5ade0d7869f3a20d50a05a7ac083744f0e88ede4fc02298ab9f67dfdf19 -const expectedRenderedSurfaceSHA = "a16fbd577f8155e64aacc02cb624f26d9628d75658ce804215d9d69a3884bf92" +// +// Re-approved 2026-09-04 for the flux-operator C-0211 template fix (#3239), which +// adds two securityContext ops to that HelmRelease's existing postRenderer patch. +// +// Conservation proven by rendering ALL FIVE overlays from both trees with the same +// renderer and taking the set difference in BOTH directions over the complete +// apiVersion|kind|namespace|name identity: 548 documents on main e37660b2 and 548 on +// this branch — ZERO added, ZERO removed, ZERO renamed. Neither side carries a +// duplicate identity, so that pairing is one-to-one. +// +// The ENTIRE content delta across all five roots is seven added lines inside one +// document — the flux-operator HelmRelease — and nothing is removed anywhere: +// +// - op: add /spec/template/spec/containers/0/securityContext/seLinuxOptions {level: s0} +// - op: add /spec/template/spec/securityContext/fsGroupChangePolicy OnRootMismatch +// +// Neither op is grant-bearing: no rule, verb, resource, group, subject, +// ServiceAccount, role reference or policy moved on either side. The surface digest +// moved only because that HelmRelease is itself a selected authorization-capable +// document, so its text is covered by the aggregate — which is the tripwire working +// as designed, not a privilege change. +// +// The value below is CI's own computed digest at head 5dc2d805, taken from the +// failing run rather than recomputed locally: this validator pins the renderer to +// kubectl v1.36.2 and refuses any other, and the host that prepared this change has +// v1.36.1. The conservation proof above is renderer-robust because both sides were +// rendered with the SAME local renderer; the digest is not, so it comes from the +// pinned one. +// +// The previous approved aggregate digest was: +// +// a16fbd577f8155e64aacc02cb624f26d9628d75658ce804215d9d69a3884bf92 +const expectedRenderedSurfaceSHA = "ab05bc2c95924e372c038f878872aa94e3bd81380daa96a283bd7f74e2132a69" // authorizationOverlayPaths lists every independently reconciled production // layer where an object can grant privileges to the aws/aws service account.