Skip to content

security: the cluster-wide pod-security exception suppresses controls where its own compensating control does not runΒ #2824

Open
9 of 10 issues completed

Description

@devantler

πŸ€– 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-mutations no longer suppresses C-0013/C-0016/C-0055/C-0211 for namespaces excluded
    from add-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

Activity

  1. coderabbitai commented on Jul 27, 2026

    @coderabbitai
    Contributor
    πŸ”— 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 plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    πŸ§ͺ 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:

    issue_enrichment:
      auto_enrich:
        enabled: false

    πŸ’¬ Have feedback or questions? Drop into our discord!

  2. devantler commented on Jul 27, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    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 effective
    runAsNonRoot. Most are the genuinely-excepted set β€” cilium-agent/envoy, spire, tetragon, hcloud
    CSI/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:

    Workload Measured Why unclassified
    longhorn-system/longhorn-driver-deployer uid=0(root) genuinely runs as root; whether it needs to is unverified
    longhorn-system/csi-{attacher,provisioner,resizer,snapshotter} not measurable this way distroless, no shell, so kubectl exec … id cannot report the runtime uid

    Once 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 exec is a dead end there.

    longhorn-ui, the one clearly-safe case, is already handled in #2822.

  3. moved this to πŸ“₯ Backlog in 🌊 Project Boardon Jul 27, 2026
  4. devantler commented on Aug 5, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    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@prod while working #2823, here is the constraint that measurement has to respect.

    Only two of the three surfaces apply exceptions at all:

    Surface Applies exceptions?
    CI scan (ksail workload scan --exceptions, ci.yaml:431-432) yes
    Headlamp view (headlamp-exceptions ConfigMap, 34043 bytes) yes
    Operator CRDs (workloadconfigurationscans, …summaries, configurationscansummaries) no β€” appliedIgnoreRules null across 1448 rule entries / 225 objects / 9 namespaces, ablation-verified

    Spot-check consistent with this issue: on deployment-opencost, C-0013 and C-0211 both read
    failed
    in the operator CRDs even though pod-security-mutations suppresses them cluster-wide.
    That is the CRD surface ignoring the exception, not the exception failing.

    Consequence for the measurement child. Narrowing pod-security-mutations will not change any
    number in the operator CRDs β€” those controls already read failed there 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 95 gate 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.sarif

    The 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.

  5. self-assigned this
    on Aug 7, 2026
  6. devantler commented on Aug 7, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Re-verified this issue's evidence against current main before starting. The finding holds, with two corrections and one new constraint that changes what the fix can be.

    Corrections to the evidence

    • The mutation excludes twelve namespaces from both add-pod-security-context and add-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, opencost and the tofu-controller runner pods are not part of the gap. They are excluded from the pod rule only, and add-imageuid-pod-security-context re-supplies pod-level seccompProfile, runAsNonRoot, fsGroup and fsGroupChangePolicy; the container rule still applies to them.

    The gap is smaller than it looks, and uneven

    infrastructure-privileged.yaml already 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:

    • C-0211 in all twelve β€” infrastructure-privileged.yaml does not carry it at all.
    • C-0013/C-0016/C-0055 in the other six: kube-public, kube-node-lease, flux-system, local-path-storage, kubescape, chaos-mesh (two of which hold no workloads, and kubescape has its own narrow kubescape-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: NotIn support 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-mutations and vpa-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.

  7. devantler commented on Aug 10, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    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:

    Namespace group Workloads Would surface?
    kube-public, kube-node-lease, local-path-storage, chaos-mesh 0 nothing runs there
    flux-system, cert-manager, keda, opencost, kubescape/alertmanager 21 already hardened in-spec
    kubescape, kubevuln, operator 3 genuinely fail C-0055 β€” no capabilities.drop, no container seccompProfile
    kubescape/node-agent 1 legitimately privileged, already covered by name

    So 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 no
    other 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.

  8. 11 remaining items

  9. added a commit that references this issue on Aug 18, 2026
  10. devantler commented on Aug 18, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    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

    ClusterSecurityException is applied by the CI scan and the Headlamp view, never by the
    kubescape-operator's stored scan resources. So workloadconfigurationscans reports pre-exception
    results, which makes it the right oracle here: it shows exactly what a narrowed exception would stop
    suppressing. Re-verified on this pass β€” appliedIgnoreRules is null across every rule entry read.

    ⚠️ The CI scan cannot answer this question at all. ksail workload scan reads k8s/, which is
    118 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

    Group Namespaces Consequence
    Scanned workloads flux-system (72 objs), observability (84), longhorn-system (58), velero (41), kubevirt (6), cdi (4) where narrowing could bite
    Not scanned kube-system, kube-public, kube-node-lease, kubescape operator excludeNamespaces β€” narrowing cannot change their result
    No namespace in prod local-path-storage, chaos-mesh opt-in stacks

    infrastructure-privileged.yaml already excepts longhorn-system, observability, velero, kubevirt and
    cdi for C-0013/C-0016/C-0055 β€” leaving flux-system as the only namespace that narrowing exposes.

    flux-system, pre-exception

    Workload C-0013 C-0016 C-0055 C-0211
    flux-operator failed passed passed failed
    helm-controller failed passed passed failed
    kustomize-controller failed passed passed failed
    notification-controller failed passed passed failed
    source-controller failed passed passed failed
    tofu-controller failed passed passed failed

    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 first
    and 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

    PR narrowing C-0016/C-0055 and splitting the rest out follows.

  11. added a commit that references this issue on Aug 18, 2026
  12. devantler commented on Aug 19, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Retyping this to Epic: it is decomposed, and its remaining criteria all live in children

    Selected this as the oldest actionable type:Security issue after #2787, then re-measured its
    premises against current main before 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.yaml
    carries a namespaceSelector with NotIn the twelve namespaces add-security-context excludes, and
    the part that could not be scoped was split into pod-security-mutations-unscoped.yaml with an
    explicit platform.devantler.tech/cluster-wide: declared annotation. So the inverse-scope defect this
    issue was filed for is fixed for C-0016 and C-0055.

    Acceptance criteria, measured rather than assumed:

    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 Epic means ("decomposed into sub-issues, never implemented directly; closes when its
    children do"), whereas Security advertises it as directly actionable work. Leaving it typed
    Security keeps presenting it near the top of the security queue to runs that then find nothing to
    build β€” 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.

  13. changed the issue type fromtoon Aug 19, 2026
  14. devantler commented on Oct 5, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Fields

No fields configured for Epic.

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions