From 5dc2d805617bc9d79618b25c85ff08c6ce678cf3 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Fri, 4 Sep 2026 11:40:47 +0200 Subject: [PATCH 1/2] fix(security): supply C-0211's two non-privilege fields to flux-operator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flux-system is excluded from add-pod-security-context and add-container-security-context because its workloads need elevated privilege by design. fsGroupChangePolicy and seLinuxOptions are not privilege fields, so that exclusion suppressed two CIS-5.7.3 (C-0211) rules it never needed to. The namespace-scoped add-baseline-context-optin-* rules cannot close this: 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, which is why #3541 (velero) and #3544 (the four FluxInstance controllers) were done this way. Measured against live PROD stored specs 2026-09-04: of flux-system's six workloads, four already carry both fields from #3544, leaving flux-operator and tofu-controller; the latter is retired and pending removal in #3480. flux-operator is therefore the namespace's last workload needing this. Both fields are inert for this workload — it sets no fsGroup, so the kubelet's volume ownership pass never runs, and its only volumes are an emptyDir and the projected service-account token (no PVC, no hostPath). seLinuxOptions {level: s0} is the documented AppArmor-cluster no-op kept as CIS-5.7.3 defense-in-depth. Declared posture moves; runtime behaviour does not. Neither field carries the runAsGroup-without-runAsUser coupling that can stop a sandbox building. The pod-level op adds a leaf into the chart's pod securityContext, which renders as `{}`. A leaf add is deliberate: adding the whole object would replace it and silently discard any field the chart starts setting later. Proven with a before/after control through the same kyaml engine helm-controller uses, over a fixture built from the live Deployment with the post-rendered fields stripped back to chart shape: at base both fields read ABSENT; with this change they read OnRootMismatch and {level: s0}, with runAsUser 65532 preserved in both arms. Part of #3239 Co-Authored-By: Claude Opus 5 (1M context) --- .../flux-operator/helm-release.yaml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) 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 From 28840ac29abe7ead5b574e822c23a34a5651e46e Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Fri, 4 Sep 2026 11:50:32 +0200 Subject: [PATCH 2/2] chore(security): re-approve the authorization surface digest for the flux-operator fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rendered authorization surface fingerprint is a change-detection tripwire over every selected authorization-capable document. The flux-operator HelmRelease is one of them, so editing its postRenderer patch moves the aggregate digest even though the edit grants nothing. 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, and no duplicate identity on either side, so the pairing is one-to-one. The entire content delta across all five roots is seven added lines inside that one HelmRelease, with nothing removed anywhere: the two securityContext ops this branch adds. No rule, verb, resource, group, subject, ServiceAccount, role reference or policy moved. The value 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 preparing host has v1.36.1. The conservation proof is renderer-robust because both sides used the same local renderer; the digest is not, so it comes from the pinned one. Part of #3239 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/validate-eks-ci-role-policy/main.go | 34 ++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) 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.