Repository navigation
security: the cluster-wide pod-security exception suppresses controls where its own compensating control does not runΒ #2824
Description
π€ Generated by the Agentic Engineer
Evidence
pod-security-mutations (k8s/bases/infrastructure/cluster-security-exceptions/pod-security-mutations.yaml)
suppresses C-0013, C-0016, C-0055 and C-0211 cluster-wide β it carries no match: block, and
scripts/generate-kubescape-exceptions/main.go maps a missing match to kind: ".*" ("No match => the
exception applies to every resource").
Its stated justification is that the compensating control runs at admission:
Kyverno mutate policy "add-security-context" injects these controls β¦ at pod admission. The
Deployment spec does not contain them and Kubescape scans the stored spec, not the live pod.
That justification does not hold for its own scope. add-security-context
(cluster-policies/best-practices/add-security-context.yaml)
excludes ten namespaces from both add-pod-security-context and add-container-security-context.
In those namespaces Kyverno injects nothing β so the exception suppresses the controls in exactly the
places where the compensating control is absent.
The two are inverses: the exception is justified by a mutation whose scope is the complement of where
the exception matters most.
Impact
Suppression is silent and indistinguishable from compliance. A workload in an excluded namespace that
genuinely runs as root reads the same as one that is correctly hardened, so the posture score cannot
be used to detect a regression there β which is the failure mode the 100%-and-hold programme (#2447)
exists to prevent. This is the "growing exceptions set is a smell" case from the fix-vs-except ladder:
the exception is doing more work than its rationale claims.
Concretely, #2435 spent effort hardening two workloads on the belief the score would move; it could
not have, and neither the CI gate nor the in-cluster scan would have shown the difference (measured
while reviewing #2822 β both trees "All controls passed").
Expected
The exception should cover only what its rationale covers: workloads where Kyverno actually injects
the fields. Everything else should either genuinely pass, or carry its own narrow, workload-scoped
exception with a reason that is true of it β the kind+name designator pattern
kubescape-privileged.yaml already uses.
Acceptance criteria
-
pod-security-mutationsno longer suppresses C-0013/C-0016/C-0055/C-0211 for namespaces excluded
fromadd-security-context(scope it to the mutated set, however expressed). - Every workload that consequently surfaces is either hardened in its own spec or carries a
workload-scoped exception whose reason is true of that workload. - The compliance floor is re-baselined from CI upward as gaps close β never lowered to absorb
newly-surfaced findings. - A regression in an affected namespace is visible in the score afterwards (the point of the change).
Size
Medium, and decompose before implementing β the first child should be measurement: enumerate what
actually surfaces once the blanket suppression is narrowed, so the remediation is sized from evidence
rather than guessed. Expect a mix of genuine hardening (like #2822) and legitimately-privileged
infrastructure that earns a scoped exception.
Part of #2447
Metadata
Metadata
Assignees
Labels
Type
Fields
Projects
- StatusShow more project fieldsπ§ Icebox
Activity
coderabbitai commentedon Jul 27, 2026
π Related PRs
#2316 - fix(kubescape): make ClusterSecurityException posture exceptions take effect [merged]
#2434 - fix(kubescape): except GitOps-managed CronJobs from C-0026 posture control [merged]
#2440 - fix(security): make C-0002 exec-into-container exception effective for infra controllers [merged]
#2442 - fix(kubescape): make C-0015 secret-list exceptions take effect [merged]
#2446 - fix(kubescape): except C-0007 delete-capable RBAC by object, not namespace [merged]
#2455 - fix(security): harden containers with read-only root filesystems (Kubescape C-0017) [merged]
π Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.π§ͺ Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:π¬ Have feedback or questions? Drop into our discord!
devantler commentedon Jul 27, 2026
Carrying over the unclassified C-0013 residual from #2435, which a concurrent run closed as
completed at 15:53Z. That close was right for its two named deliverables (both shipped and verified
live), but it leaves this measured residual without a home β recording it here so it is not lost.
Measured live on
admin@prod(read-only), all workloads whose pod template lacks an effectiverunAsNonRoot. Most are the genuinely-excepted set β cilium-agent/envoy, spire, tetragon, hcloudCSI/CCM, cluster-autoscaler, coredns, longhorn manager/CSI/engine-image, and the eBPF node-agents
(velero, coroot, kubescape). Two are unclassified rather than cleared, and I did not guess them:
longhorn-system/longhorn-driver-deployeruid=0(root)longhorn-system/csi-{attacher,provisioner,resizer,snapshotter}kubectl exec β¦ idcannot report the runtime uidOnce this issue narrows the blanket suppression, these are exactly the workloads that will surface,
so classifying them is natural first-child work: either harden them in spec, or give each a
workload-scoped exception whose reason is true of it.
Note for whoever measures the distroless four: read the uid from the image config rather than the
container β
kubectl execis a dead end there.longhorn-ui, the one clearly-safe case, is already handled in #2822.devantler commentedon Aug 5, 2026
The measurement child must run against the CI scan, not the operator CRDs
This issue's size note says to decompose first and make the first child a measurement β
"enumerate what actually surfaces once the blanket suppression is narrowed". Measured today on
admin@prodwhile working #2823, here is the constraint that measurement has to respect.Only two of the three surfaces apply exceptions at all:
ksail workload scan --exceptions,ci.yaml:431-432)headlamp-exceptionsConfigMap, 34043 bytes)workloadconfigurationscans,β¦summaries,configurationscansummaries)appliedIgnoreRulesnull across 1448 rule entries / 225 objects / 9 namespaces, ablation-verifiedSpot-check consistent with this issue: on
deployment-opencost, C-0013 and C-0211 both readfailedin the operator CRDs even thoughpod-security-mutationssuppresses them cluster-wide.That is the CRD surface ignoring the exception, not the exception failing.
Consequence for the measurement child. Narrowing
pod-security-mutationswill not change anynumber in the operator CRDs β those controls already read
failedthere today, suppressed or not.Enumerating "what surfaces" from that surface would report the same set before and after and read
as "the change did nothing".
Run it on the CI surface instead, which is where the suppression is real and where the
--compliance-threshold 95gate lives:go run ./scripts/generate-kubescape-exceptions -o /tmp/exceptions-before.json # then with pod-security-mutations narrowed to the mutated namespace set: go run ./scripts/generate-kubescape-exceptions -o /tmp/exceptions-after.json ksail workload scan --framework nsa --exceptions /tmp/exceptions-before.json --format sarif -o before.sarif ksail workload scan --framework nsa --exceptions /tmp/exceptions-after.json --format sarif -o after.sarifThe diff of those two SARIFs is the enumeration this issue asks for, and it is obtainable
without touching the cluster.
This also confirms the issue's own impact claim rather than weakening it: #2435's hardening could
not move the CI score precisely because the CI scan is exception-aware and the exception is
cluster-wide. The premise holds; only the surface to measure on needed pinning.
Not claiming this issue β recording it so the first child is not built against a surface that
cannot show the effect.
devantler commentedon Aug 7, 2026
Re-verified this issue's evidence against current
mainbefore starting. The finding holds, with two corrections and one new constraint that changes what the fix can be.Corrections to the evidence
add-pod-security-contextandadd-container-security-context, not ten:kube-system,kube-public,kube-node-lease,flux-system,longhorn-system,local-path-storage,kubescape,kubevirt,cdi,observability,velero,chaos-mesh.cert-manager,keda,opencostand the tofu-controller runner pods are not part of the gap. They are excluded from the pod rule only, andadd-imageuid-pod-security-contextre-supplies pod-levelseccompProfile,runAsNonRoot,fsGroupandfsGroupChangePolicy; the container rule still applies to them.The gap is smaller than it looks, and uneven
infrastructure-privileged.yamlalready excepts C-0013, C-0016 and C-0055 β deliberately and with a scope β for six of the twelve:kube-system,longhorn-system,observability,velero,kubevirt,cdi.So the genuinely-unaccounted-for suppression is:
infrastructure-privileged.yamldoes not carry it at all.kube-public,kube-node-lease,flux-system,local-path-storage,kubescape,chaos-mesh(two of which hold no workloads, andkubescapehas its own narrowkubescape-privileged.yaml).Why the prescribed fix cannot be written as an In-list
The acceptance criteria ask the exception to cover only where Kyverno injects. That set is the complement of the exclusion list, and a Kubescape designator can only express a positive match. Enumerating the covered namespaces instead inverts the failure mode: a newly-onboarded namespace would be absent from the exception, Kubescape would flag its workloads for all four controls, and the CI gate would go red on every new namespace until someone remembered to extend the list.
That is precisely the "a control whose routine maintenance step causes outages gets widened back out the first time it fires" argument this repository already accepted when declining the SHA allow-list in #2818. Same shape, so the same answer.
A durable fix therefore needs one of:
operator: NotInsupport in the generator emitting a complement designator; or splitting the compensating mutation so its scope is expressible; or moving these controls off suppression entirely by setting the fields in-spec, the way C-0017 was already handled here.What shipped now
#3010 takes the step that is available without that decision: a cluster-wide exception must now declare itself, so this scope stops being reachable by omission.
pod-security-mutationsandvpa-managed-resources(same defect β VPA does not right-size every namespace either) now carry the marker recording that their scope is knowingly wider than their own rationale, and pointing here.This issue stays open for the narrowing itself.
devantler commentedon Aug 10, 2026
Measurement done β the remediation is three workloads, not an unknown
This issue's Size note asked for measurement as the first child: enumerate what actually surfaces
once the blanket suppression is narrowed, so the remediation is sized from evidence rather than
guessed. Measured against prod on 2026-08-10:
kube-public,kube-node-lease,local-path-storage,chaos-meshflux-system,cert-manager,keda,opencost,kubescape/alertmanagerkubescape,kubevuln,operatorcapabilities.drop, no containerseccompProfilekubescape/node-agentSo the prediction in this issue's Size note holds exactly β a small mix of genuine hardening plus one
legitimately-privileged component β and the remediation is far smaller than the blanket scope implied.
Two corrections to this issue's own premise, both now recorded in the exception's annotation
(#3063): the mutation is absent in fifteen namespaces rather than the ten stated here (its
pod-level rule also excludes
cert-manager,keda,opencost), and C-0211 is exempted in noother CR, so it is uncovered wherever the mutation is absent rather than merely under-covered.
Remediation split out as #3064. Narrowing the blanket exception itself stays blocked on a scope the
CSE designators cannot express β an In-list would fail every newly-onboarded namespace β which is
why it remains declared rather than fixed.
11 remaining items
chore: agent-claim #2824 nonce=5883e42ed1b8abe5636c93b50fc8338d
devantler commentedon Aug 18, 2026
Measurement child: what narrowing actually surfaces
This issue asks for measurement before remediation. Measured in prod 2026-08-18.
First, the surface question β because it decides what the evidence even means
ClusterSecurityExceptionis applied by the CI scan and the Headlamp view, never by thekubescape-operator's stored scan resources. So
workloadconfigurationscansreports pre-exceptionresults, which makes it the right oracle here: it shows exactly what a narrowed exception would stop
suppressing. Re-verified on this pass β
appliedIgnoreRulesis null across every rule entry read.ksail workload scanreadsk8s/, which is118 Kustomization + 60 HelmRelease + 49 Namespace and almost no rendered Pod specs. I ran it three
ways β baseline, narrowed, and with the pod-security exceptions deleted outright β and got 16
findings every time (C-0015 Γ2, C-0034 Γ3, C-0053 Γ11). The ablation not firing is the point: on
that surface these four controls evaluate nothing, so "no new findings" there is vacuous and must not
be read as evidence of compliance.
The scanned population among the twelve excluded namespaces
excludeNamespacesβ narrowing cannot change their resultinfrastructure-privileged.yamlalready excepts longhorn-system, observability, velero, kubevirt andcdi for C-0013/C-0016/C-0055 β leaving flux-system as the only namespace that narrowing exposes.
flux-system, pre-exception
Every C-0013 failure is the same single missing field,
spec.template.spec.containers[0].securityContext.runAsGroup.π΄ A
runAsNonRoot-only check reads these six as compliant. I checked the specs that way firstand concluded C-0013 was safe to narrow; the scanner refuted it. C-0013 also requires
runAsGroup.Worth recording because the cheap proxy and the real control disagree precisely here.
What this changes
have been non-zero.
gap, which the third acceptance criterion forbids. Filed as Six flux-system controllers fail C-0013 on a missing runAsGroup, blocking the C-0013 narrowingΒ #3223 (sub-issue).
PR narrowing C-0016/C-0055 and splitting the rest out follows.
chore: agent-claim #2824 nonce=04f406b5d323dd0cbb58ffe43d6409a2
devantler commentedon Aug 19, 2026
Retyping this to Epic: it is decomposed, and its remaining criteria all live in children
Selected this as the oldest actionable
type:Securityissue after #2787, then re-measured itspremises against current
mainbefore building β and they have moved.The central evidence claim is no longer true. This body states the exception "carries no
match:block". It does now:
pod-security-mutations.yamlcarries a
namespaceSelectorwithNotInthe twelve namespacesadd-security-contextexcludes, andthe part that could not be scoped was split into
pod-security-mutations-unscoped.yamlwith anexplicit
platform.devantler.tech/cluster-wide: declaredannotation. So the inverse-scope defect thisissue was filed for is fixed for C-0016 and C-0055.
Acceptance criteria, measured rather than assumed:
set. C-0013 and C-0211 are knowingly left cluster-wide, because narrowing them today would
surface findings with no fix behind them β lowering the score to absorb a gap instead of
closing it. Each is blocked on measured evidence: C-0013 on six flux-system controllers
(Six flux-system controllers fail C-0013 on a missing runAsGroup, blocking the C-0013 narrowingΒ #3223), C-0211 on a 33-workload residual population sized 2026-08-19 (security(kubescape): C-0211's residual population behind the pod-security exception is unsized and unownedΒ #3217).
regression visible are owned by security(kubescape): C-0211's residual population behind the pod-security exception is unsized and unownedΒ #3217, Six flux-system controllers fail C-0013 on a missing runAsGroup, blocking the C-0013 narrowingΒ #3223 and Nothing detects the pod-security exception drifting from the mutation scope it claims to mirrorΒ #3224 respectively.
Why the type change. This issue's own Size note says "decompose before implementing", and it has
been β five sub-issues, two closed (#3064, #3083), three open. Nothing here is directly implementable
any more; every remaining criterion is delegated. Under this repo's issue-hierarchy convention that is
exactly what
Epicmeans ("decomposed into sub-issues, never implemented directly; closes when itschildren do"), whereas
Securityadvertises it as directly actionable work. Leaving it typedSecuritykeeps presenting it near the top of the security queue to runs that then find nothing tobuild β which is how I arrived here.
The security work is not hidden by this: all three open children are themselves
type:Security,so they stay in that queue where the actual work is. This is reversible β retype it back if the
programme wants the parent counted.
devantler commentedon Oct 5, 2026
The remaining upward rebaseline is delivered for review in #4522, based on three independent Linux CI scans. Fresh named control-detail reads also exposed an aggregate exception-marker discrepancy, now tracked in #4523. A bare passed status cannot discharge that evidence gap; the current exception declarations and stored workload were joined independently.
This parent remains open until the floor change is merged and measured and the control-detail discrepancy is resolved with a fresh, coherent named read. The new issue preserves the current exception and credential boundaries; no stored scan data was rewritten to manufacture clearance.