Skip to content

security(kubescape): patch the two non-privilege C-0211 fields into workload templates, so the exception can narrowΒ #3522

Closed
3 of 3 issues completed

Description

@devantler

πŸ€– Generated by the Agentic Engineer

Evidence

add-baseline-context-optin-* supplies fsGroupChangePolicy and an effective SELinux level to pods in opted-in namespaces, and it demonstrably works β€” velero's pods carry both fields and none failed to start. But Kyverno mutates the pod at admission, while Kubescape scores the stored workload spec. Settled with evidence on #3239 (2026-09-02): each scan object is keyed to the workload, not a pod (deployment-velero carries wlid://cluster-prod/namespace-velero/deployment-velero, kubescape.io/workload-kind: Deployment, relatedObjects: null), and the exception's own reason field has always said so β€” "Kubescape scans the stored spec, not the live pod."

velero is the natural experiment: opted in, its pods measurably carry both fields, its template carries neither, and the scan object is the Deployment's.

So no amount of widening the namespace opt-in can move a template-scored verdict. Re-measured live against prod stored specs on 2026-09-02:

field (pod level, stored spec) absent on
fsGroupChangePolicy 35 / 35
effective SELinux level on every container 35 / 35
fsGroup 20 / 35
runAsGroup 26 / 35
runAsNonRoot 19 / 35

Population is 35 workloads across the four scanned excluded namespaces β€” 17 Deployments, 8 DaemonSets, 7 CronJobs, 3 StatefulSets (flux-system 6, longhorn-system 12, observability 15, velero 2).

Audience and problem

The pod-security-mutations-unscoped exception suppresses C-0211 across this population, so every workload reports passed w/exceptions regardless of its actual state. That exception is waiting to be narrowed, and it cannot narrow while the underlying templates still fail β€” which, on the scanned surface, all 35 do. The admission-time mutation improves runtime posture and can never discharge the exception.

Hypothesis

Patching the two non-privilege fields into the workload templates at source β€” chart values where the chart exposes them, kustomize patches where it does not β€” moves the scanned verdict for those two controls, after which C-0211's residual shrinks to the workloads that still fail on the genuinely privilege-adjacent fields (fsGroup, runAsGroup, runAsNonRoot) and narrowing the exception becomes sizable rather than all-or-nothing.

Deliberately not in scope: fsGroup, runAsGroup, runAsNonRoot. Those are the fields the namespace exclusions exist to protect, and supplying them is a privilege change, not a compliance one.

Success signal

  • Baseline: 35 / 35 templates missing fsGroupChangePolicy; 35 / 35 missing an effective SELinux level.
  • Target: 0 / 35 for both, read with the subStatus discriminator rather than the bare status β€” a suppressed control reads passed either way, so the naive read returns a clean result over an unfixed population (this is exactly the fail-open feat(security): document every suppressed control's fail-open, and fail CI when one is notΒ #3517 generalised).
  • Guardrail: no workload regressed on privilege, user or group fields, and no pod sandbox failing to start.

Smallest useful change

One namespace, smallest first: velero (2 workloads). It is already opted in, so the pod-scope and template-scope effects can be compared directly on the same workloads β€” which also proves the measurement method before it is applied to the other 33.

Acceptance criteria

  • The two non-privilege fields are present in the stored spec of the targeted workloads, via chart values or kustomize patches, not via admission mutation.
  • Verified by a single-object GET of the workload's workloadconfigurationscans object β€” a LIST returns .spec.controls null and would read as a clean sweep.
  • subStatus is used to distinguish a genuine pass from a suppressed one.
  • No regression on privilege, user or group fields; pods still start.
  • The C-0211 residual recorded in pod-security-mutations-unscoped.yaml is updated to the new figure, re-measured rather than inherited.

Size

Medium, and naturally decomposable per namespace. The investigation is already done β€” #3239 carries it β€” so what remains is the mechanical work plus its measurement.

Follow-up to #3239, which established that admission-time mutation cannot move this surface. The two are complementary, not alternatives: the mutation stays for runtime posture, this is the narrowing path.

Activity

  1. moved this to πŸ“₯ Backlog in 🌊 Project Boardon Sep 2, 2026
  2. self-assigned this
    on Sep 2, 2026
  3. devantler commented on Sep 3, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Post-merge verification: the fix is live, but C-0211 cannot be its success signal

    Checked in prod after #3541 merged. Two findings, and the second one changes how this issue should be closed.

    1. The fix is in the stored specs β€” confirmed

    Both workloads now carry the two non-privilege fields directly in the template, which is the thing Kubescape actually scans:

    Workload fsGroupChangePolicy container seLinuxOptions.level runAsUser
    Deployment/velero OnRootMismatch s0 65532 (unchanged)
    DaemonSet/node-agent OnRootMismatch s0 0 (unchanged β€” keeps its host-path root)

    2. C-0211 reads passed, and that reading is worthless here

    It would be easy to read C-0211: passed on both workloads as success. It is not evidence of anything, for two independent reasons:

    It passes by exception, not by compliance. The two rules this issue targeted both list the suppression explicitly:

    rule=set-seLinuxOptions             status=passed  ignore=["pod-security-mutations-unscoped/C-0211"]
    rule=set-fsgroupchangepolicy-value  status=passed  ignore=["pod-security-mutations-unscoped/C-0211"]
    

    pod-security-mutations-unscoped ignores C-0211 cluster-wide, so these rules report passed whether or not the fields are present. The control is insensitive to this change by construction. (For contrast, set-fsgroup-value passes with no ignore and no fix path β€” that one is a genuine pass.)

    The scan is also stale. Both rules still emit a fixPath for the very fields that are now present:

    fixPath: spec.template.spec.containers[0].securityContext.seLinuxOptions
    fixPath: spec.template.spec.securityContext.fsGroupChangePolicy
    

    A fix path is only emitted when the rule found the field missing. So the scan evaluated a pre-#3541 spec. That is corroborated independently: the newest workloadconfigurationscan anywhere in the cluster was created 2026-09-02T10:21Z, while the velero rollout completed 2026-09-03T00:19Z. Nothing has re-scanned these workloads since the change.

    What this means for closing this issue

    This issue's own title is the right frame β€” the goal is "so the exception can narrow", not "so C-0211 turns green". C-0211 was always going to read passed here. So:

    • Do not close this on a C-0211 reading, in either direction.
    • The success signal is the stored spec (verified above) plus the residual population behind the exception shrinking β€” the count of 35 that the exception's own reason cites.
    • The exception's stated premise, "The Deployment spec does not contain them", is now false for velero specifically. That is exactly the narrowing this issue was opened to enable.

    The remaining work is therefore to re-measure the residual population once a scan cycle has actually run against the new specs, and narrow pod-security-mutations-unscoped by whatever that measurement licenses. I have not attempted the narrowing in this run, because doing it against a stale scan would size it wrong.

    Worth noting for anyone auditing posture more broadly: 11 rules across this one workload pass only via an applied ignore rule. An exception-suppressed passed and a complied passed are indistinguishable in a summary count, so posture percentages built from control status alone will read better than the cluster is.

  4. devantler commented on Sep 14, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Status 2026-09-14: the template slices shipped; narrowing waits on #3239

    I picked this up as the oldest unclaimed Security issue and checked it against what's on main now.

    I've marked this issue blocked by #3239, so the queue skips it for a real reason until the rollout lands. Once it does: re-measure the residual against the live stored specs (not the scan objects, which read passed under the exception either way), then narrow pod-security-mutations-unscoped by what that measurement supports.

  5. devantler commented on Sep 26, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Re-measured 2026-09-26: the 2026-09-25 sizing holds, and #3239 no longer blocks this

    The blocker is gone. #3239 closed as completed on 2026-09-25, so the blocked by #3239 dependency recorded above is satisfied.

    I re-measured read-only against the live prod stored specs, the surface Kubescape scans. The numbers match the sizing that #4200 recorded in pod-security-mutations-unscoped.yaml:

    • Population: 38 workloads. By namespace: flux-system 5, longhorn-system 10, observability 21, velero 2. By kind: 16 Deployments, 6 DaemonSets, 13 CronJobs, 3 StatefulSets.
    • The two non-privilege fields are done on all 38. Every workload carries a pod-level fsGroupChangePolicy, and an effective seLinuxOptions object reaches every container, init containers included. The hypothesis this issue was opened for is delivered.
    • 19 of 38 still miss a privilege-adjacent field. fsGroup is missing on 19, runAsGroup on 11, and runAsNonRoot on 11. So C-0211 still cannot narrow without surfacing unfixed findings, and the exception correctly stays cluster-wide.

    Next slice for the narrowing this issue now tracks. Seven of the 19 already run as non-root and miss only fsGroup: flux-operator, longhorn-ui, the longhorn stale-node-cleanup CronJob, and four Coroot CronJobs. They are the cheapest to move at source. The other 12 genuinely run with elevated privileges: CSI sidecars, storage and node agents, and the log forwarder. Each of those needs a scoped, name-level exception with a recorded reason, not a field change.

    Once both groups are handled, C-0211 can leave the cluster-wide exception.

    I filed #4210 for this narrowing before I saw that #4200 had already pointed the exception at this issue. I've closed #4210 as a duplicate so the work stays tracked here.

  6. added 3 commits that reference this issue on Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Fields

No fields configured for Security.

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions