Skip to content

Six posture exceptions in controller-rbac.yaml are structurally inert, and the migrated four have not gone to zeroΒ #2823

Description

@devantler

πŸ€– Generated by the Agentic Engineer

⚠️ STATUS 2026-08-15 β€” read this before doing anything below

Step 1 is ANSWERED. Do not re-run it. The measurement is
in this comment:
on the CI scan β€” the only surface that applies these exceptions β€” the controller-rbac CR
suppresses 17 findings (C-0053 Γ—13, C-0037 Γ—3, C-0031 Γ—1), and C-0053's residual 10 are
100% out-of-scope (tenant namespaces absent from the selector). By this issue's own decision
rule, the mechanism works.

The sections below are preserved as dated evidence, and two of their conclusions are refuted.
The title's "structurally inert" is wrong for three of the six controls, and every count in
Problem and What is NOT yet established was taken from the kubescape-operator's stored scan
resources β€” which apply no exceptions at all (appliedIgnoreRules is empty, recorded in
ci.yaml since #2969). A non-zero count there is not evidence about an exception, so those tables
cannot answer the question this issue asks.

This banner exists because the original Direction sent 15 separate runs over 19 days to
re-measure the same refuted premise, twice ending in public self-correction. Measure exception
effect only by diffing two ksail workload scan --exceptions runs.

Problem

k8s/bases/infrastructure/cluster-security-exceptions/controller-rbac.yaml suppresses six controls through a match.namespaceSelector. That file already documents why such a selector cannot suppress an RBAC-anchored finding, and says to migrate the six "if they surface failing objects".

They do β€” all six. Measured 2026-07-27 against admin@prod:

Control Name Failing objects Anchored on an RBAC object Cluster-scoped (empty namespace)
C-0053 Access container service account 155 100% 103
C-0037 CoreDNS poisoning 41 100% 30
C-0188 CIS-5.1.4 minimize access to create pods 20 100% 13
C-0031 Delete Kubernetes events 18 100% 14
C-0063 Portforwarding privileges 9 100% 5
C-0035 Access restriction to infrastructure β€” admin access 6 100% 5

249 (control, object) pairs over 156 unique objects. Zero are ordinary namespaced workloads; every one carries kubescape.io/rbac-resource: "true". So the namespaceSelector is not merely imprecise on these β€” it can never match any of them, and all six exceptions are inert while appearing configured.

(Object names are deliberately not listed. An enumeration of which bindings can reach service-account tokens or create pods is a weakness map; the aggregate counts and the anchor analysis are the sanitized minimum needed to review a fix.)

The trap in the data

The summaries are labelled kubescape.io/workload-kind: ServiceAccount with a populated workload-namespace, which makes a namespaceSelector look viable. But matching keys on the kubescape.io/wlid annotation, which reads wlid://cluster-prod/namespace-/clusterrolebinding-<name> β€” binding kind, empty namespace. Any migration must name ClusterRoleBinding/RoleBinding kinds, not ServiceAccount.

Also note for anyone re-measuring: the aggregated storage API returns spec-stripped objects on LIST β€” controls reads null and severities read all-zero across every item. Those numbers are only real when each object is fetched by name. A LIST-based sweep here reports a clean cluster.

Historical evidence β€” NOT a current instruction (see STATUS above)

The four controls already migrated to explicit kind+name match.resources CRs have not gone to zero:

Migrated control Still failing
C-0015 (list secrets) 70
C-0007 (delete-capable roles) 42
C-0002 (exec into container) 9
C-0187 (wildcard RBAC) 6

They are anchored the same way. This is not evidence the migration approach is broken β€” the residue may simply fall outside those CRs' named scope, which would be correct behaviour. But it does mean "migrate to kind+name" is currently an unproven remediation, and migrating six more controls on that assumption risks writing six more exceptions that also do not take effect.

Direction β€” what actually remains

Step 1 is done; the remaining work is narrow and does not need another measurement of the operator CRDs.

  1. Answer the in-scope/out-of-scope question β€” done, see STATUS above.
  2. Correct controller-rbac.yaml's guidance so it stops advising migration of three working exceptions. Shipped in docs(kubescape): correct the controller-rbac migration guidanceΒ #3142.
  3. Decide C-0063 / C-0035 / C-0188 on evidence. They produce zero findings in both arms, so they are unfalsifiable on the gating surface, not proven inert. Either keep them with that stated, or remove them β€” but record the basis.
  4. If any control is migrated, re-measure with the paired-arm oracle and record before/after.

Acceptance criteria

  • The in-scope/out-of-scope question is answered for at least one migrated control, with evidence
  • controller-rbac.yaml's guidance reflects the measured per-control effect (docs(kubescape): correct the controller-rbac migration guidanceΒ #3142)
  • C-0063 / C-0035 / C-0188 each carry a recorded keep-or-remove decision with its basis
  • Any before/after counts are measured with the two-arm CI-scan oracle, never off the operator CRDs

Rough size: S β€” what is left is a decision to record, not an investigation.

Evidence freshness

Scanner proven alive rather than assumed: kubescape v4.0.10, scheduler CronJob last job Complete, newest summary objects written 2026-07-27T10:08–10:09Z, aggregate scan summary non-zero (212 high / 840 medium / 89 low), and 2206/2206 objects returned populated control maps on direct GET.

Related

Activity

  1. moved this to πŸ“₯ Backlog in 🌊 Project Boardon Jul 27, 2026
  2. coderabbitai commented on Jul 27, 2026

    @coderabbitai
    Contributor
    πŸ”— Related PRs

    #3037 - fix(cilium): refuse a zero-count gate release and correct the runbook order [merged]
    #3085 - security(kubescape): except the operator-bundle RBAC the CI scan actually sees [merged]
    #3086 - security(minio): read the root credential from a file instead of the environment [merged]
    #3113 - fix(coredns): disposition CKV_K8S_11 as a stored-spec blind spot [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!

  3. devantler commented on Jul 27, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Step 1 is done, and the result is NEGATIVE β€” kind+name matching is not working either

    I ran the check this issue proposed as its first step: sweep all 2200 summaries by name, list every
    object still failing C-0187, and ask whether each is inside or outside
    wildcard-rbac-by-design's named scope.

    All six failures, with their wlid anchors:

    Failing anchor Named in the CR?
    clusterrolebinding-cluster-reconciler-flux-system (kustomize-controller) βœ… yes
    clusterrolebinding-cluster-reconciler-flux-system (helm-controller) βœ… yes
    clusterrolebinding-flux-operator βœ… yes
    clusterrolebinding-velero-server βœ… yes
    rolebinding-velero-server ⚠️ the CR names kind: Role, the finding anchors on the RoleBinding
    clusterrolebinding-longhorn-support-bundle ❌ no β€” and deliberately so, per the CR's own comment

    The CR declares four matchers. Five of the six failures are on objects it names, three of them by
    exact kind and exact name (ClusterRoleBinding / cluster-reconciler-flux-system, flux-operator,
    velero-server). They are still failing.

    So the residue is not out-of-scope. It is almost entirely in-scope. That falsifies the assumption
    behind this issue's proposed direction: migrating the other six controls to explicit kind+name
    match.resources would not have suppressed anything, because that mechanism is not suppressing
    anything today.

    Corroborating signal

    No object in the sweep carries subStatus: w-exceptions β€” the marker the operator is supposed to
    write when it applies an exception at scan time. Combined with the above, the likeliest reading is
    that ClusterSecurityException CRs are not being applied at scan time at all, rather than that
    the match syntax is subtly wrong. Scanner is v4.0.10 and provably alive (scan completed, 2206/2206
    objects carry populated control maps, aggregate non-zero).

    The one genuinely suspicious matcher is the fifth row: the CR names kind: Role for
    velero-server, but the finding anchors on the RoleBinding of that name. Worth fixing regardless,
    but it cannot explain the three exact ClusterRoleBinding matches.

    What this changes

    The direction in the issue body is superseded. Migrating six more controls is now the wrong next
    step β€” it would produce six more exceptions that do not take effect, which is exactly the state this
    issue was opened to end.

    Revised order:

    1. Determine why an exact kind+name match is not suppressing C-0187. Candidates: the operator does not
      consume ClusterSecurityException for posture at all; the matcher schema differs from what these
      CRs use; or exceptions are applied at a layer these stored summaries bypass.
    2. Fix the Role vs RoleBinding matcher for velero-server β€” cheap, and correct either way.
    3. Only once (1) is answered, decide whether migrating the six is worth doing at all.

    Knock-on for #2586

    That issue asks whether the hand-maintained headlamp-exceptions ConfigMap can simply be deleted,
    on the reasoning that with the scanner at β‰₯ v4.0.10 the operator applies the CRs at scan time and the
    plugin renders the result. The absence of w-exceptions anywhere is evidence against that
    premise. The mirror should not be deleted on the strength of the version alone.

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

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Ran step 1 (the in-scope/out-of-scope question for a migrated control). The answer is that the
    question cannot be answered from the surface this issue measures
    β€” and that finding invalidates the
    evidence base for both halves of the issue, so it needs recording before any migration proceeds.

    First, the method reproduces

    Measured against admin@prod today, RBAC-anchored objects only, per-object GET as this issue
    instructs (scanner proven alive first: 2164 summaries, scheduler job Complete 4h ago, kubescape +
    node-agent pods Running):

    Control This issue (07-27) Today
    C-0015 70 70
    C-0007 42 42
    C-0002 9 9
    C-0187 6 6
    C-0053 155 156
    C-0037 / C-0188 / C-0031 / C-0063 / C-0035 41 / 20 / 18 / 9 / 6 identical

    So the numbers are reproducible and my read of the data matches yours.

    The decisive finding: workloadconfigurationscansummaries is a PRE-exception surface

    No ClusterSecurityException is reflected in it β€” neither a namespaceSelector one nor a kind+name
    one. The discriminating control:

    infrastructure-privileged suppresses C-0057 via a namespaceSelector naming longhorn-system and
    observability. C-0057 is evaluated on namespaced workload kinds, so unlike the RBAC controls
    that selector can match here β€” this is the case where a working exception must show. It does not:

    Namespace In that selector? C-0057 in summaries
    longhorn-system yes 4 failed, 7 passed
    observability yes 1 failed, 10 passed
    kyverno no 0 failed, 5 passed

    The exempted namespaces still carry failed. Stored summaries also expose only passed/failed β€”
    there is no exception/skipped state in the schema at all.

    Consequence: "the migrated four have not gone to zero" is expected, not evidence that
    kind+name migration is broken. Nothing anyone writes in a CSE CR can move a count on this surface.
    By the same token, this surface cannot show that the six controller-rbac.yaml entries are inert
    either β€” a working and a broken exception look identical here. The structural argument about a
    namespaceSelector never matching an empty-namespace wlid may still hold, but the counts in this
    issue do not demonstrate it.

    A second, independent finding β€” and it inverts this issue's "trap in the data" note

    C-0187 is evaluated only on subject kinds, never on the RBAC object:

    Kind carrying C-0187 Count
    ServiceAccount 156 (150 passed, 6 failed)
    Group 17 (all passed)
    User 7 (all passed)
    ClusterRoleBinding 0 of 182
    ClusterRole / Role 0

    The issue says "any migration must name ClusterRoleBinding/RoleBinding kinds, not
    ServiceAccount"
    , reasoning from the wlid string. But the wlid and the workload-kind label
    disagree: the failing objects carry wlid://…/clusterrolebinding-<name> while their
    kubescape.io/workload-kind is ServiceAccount. wildcard-rbac.yaml already names
    ClusterRoleBinding + Role by exact name β€” the kinds that never carry this control at all.

    Which of those two fields the scanner matches an exception against is now the open question, and
    it decides how all six should be written. I did not guess it.

    Revised direction

    Step 1 as written cannot be executed. The replacement first step is to establish an oracle where
    exceptions demonstrably bite
    before authoring any more of them:

    1. Use the compliance-score / "excluding N controls" surface, and the CI path
      (scripts/generate-kubescape-exceptions β†’ ksail workload scan --exceptions …), which is where
      this repo already has evidence exceptions apply β€” the β‰ˆ98.9% floor is measured with them.
    2. With that oracle, determine whether the scanner matches on workload-kind or on the wlid kind.
    3. Only then migrate the six, and re-measure on the oracle rather than on the summaries.

    I have deliberately not touched controller-rbac.yaml this run: changing six exceptions while
    the matching semantics are unknown is how six more inert entries get written, which is the exact
    failure this issue exists to prevent.

    (Object names withheld as before β€” an enumeration of which subjects hold wildcard or token-reaching
    grants is a weakness map. Aggregate counts and the kind analysis are the sanitized minimum.)

  6. devantler commented on Aug 4, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Followed my own recommendation and built the oracle. It works β€” and it completes the answer, because
    it shows the two surfaces evaluate different object populations, not just apply exceptions
    differently.

    The oracle discriminates

    Ran the CI path locally (static, no cluster), identical command both times except the --exceptions
    flag:

    go run ./scripts/generate-kubescape-exceptions -o /tmp/kse.json     # 21 policies
    ksail workload scan --framework nsa --compliance-threshold 0 --format sarif ...
    
    Run SARIF results
    without --exceptions 71
    with --exceptions 4

    So exceptions demonstrably bite on the CI surface β€” 71 β†’ 4 from one flag. That is the oracle this
    issue needs, and it is cheap and offline.

    But C-0187 is absent from that surface entirely

    Per-control breakdown of the unexcepted run β€” the population before anything is suppressed:

    39  C-0054      7  C-0034      2  C-0013      2  C-0271      1  C-0012
    14  C-0030      3  C-0017      2  C-0270      1  C-0002
    

    C-0187 does not appear at all, and neither do C-0053 / C-0037 / C-0188 / C-0031 / C-0063 /
    C-0035 — the six this issue is about. A manifest scan has no live subject→role bindings to flag; the
    ServiceAccount Γ— ClusterRole Γ— ClusterRoleBinding triples that carry these findings only exist in a
    running cluster.

    What that means β€” there are three surfaces, and the six controls appear on only one

    Surface Do exceptions apply? Are these six controls evaluated?
    CI manifest scan (ksail workload scan) yes β€” proven 71β†’4 no β€” zero findings for any of them
    In-cluster stored summaries no β€” proven by the longhorn-system/observability control in my previous comment yes β€” this is where the 249 pairs live
    In-cluster compliance score / "excluding N controls" claimed (needs scanner β‰₯ v4.0.10 + the supplemental RBAC this repo already added) yes

    So the six controls are only ever evaluated on the live cluster, and the only surface that could
    show an exception working on them is the third one β€” which is precisely the one nobody has measured.
    wildcard-rbac-by-design suppresses nothing in CI because there is nothing there to suppress; that
    is not evidence it is broken, and my previous comment's kind analysis is not evidence it is correct
    either. Both remain open against surface 3.

    Revised acceptance criteria I'd propose

    • Read surface 3 directly β€” the scanner's own "excluding N controls" / compliance output β€” and
      confirm it reflects the CRs at all. This is the gating step; nothing else can be judged
      without it.
    • With that oracle, settle whether the scanner matches an exception on workload-kind
      (ServiceAccount) or on the wlid kind (ClusterRoleBinding) β€” they disagree for every one
      of these findings.
    • Only then migrate the six, re-measuring on surface 3 rather than on stored summaries.
    • Keep controller-rbac.yaml unchanged until the above resolves.

    One observation, flagged not claimed

    The excepted run printed All controls passed. No issues found while its SARIF carried 4 results
    (3Γ— C-0034, 1Γ— C-0017, both low). That is probably a severity threshold in the CLI summary rather
    than a gate defect β€” the gate reads the compliance score, not this line β€” but a run that prints
    "no issues" over four real findings is worth a second look by someone who knows the scoring, so I am
    recording it rather than acting on it.

    Still no change to controller-rbac.yaml this run, for the reason in my previous comment.

  7. devantler commented on Aug 5, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Step 1 measured (2026-08-05, admin@prod) β€” with a method correction and one honest caveat

    ⚠️ First, a selection bug worth pinning, because it silently halves the population

    Selecting the RBAC objects by the kubescape.io/workload-kind label finds 201 objects. Selecting them by the kubescape.io/wlid annotation β€” as this issue's "trap in the data" section says to β€” finds 326, a strict superset (overlap is exactly 201). The 125 extra are the ones labelled workload-kind: ServiceAccount whose wlid is …/clusterrolebinding-<name>.

    I ran the label-based sweep first and it reported one failing pair across the whole set. That is a near-perfect false all-clear, and it is only distinguishable from a genuinely clean cluster by running the wlid-based sweep next to it. Anyone re-measuring: filter on the wlid, never on the kind label.

    Two further gotchas confirmed: spec.controls is null on LIST (control status is real only on a GET by name), and the aggregated API silently ignores -l label selectors β€” -l kubescape.io/rbac-resource=true returned all 1542 objects, including ones carrying no such label.

    Current counts (wlid-selected, GET-by-name, 326 objects)

    344 failed (control, object) pairs over 97 unique objects. All ten controls have moved down since 2026-07-27:

    Control Then Now Migrated control Then Now
    C-0053 155 106 C-0015 70 50
    C-0037 41 25 C-0007 42 26
    C-0188 20 12 C-0002 9 6
    C-0031 18 11 C-0187 6 3
    C-0063 9 5
    C-0035 6 3

    The C-0187 residue, classified against the CR's named scope

    wildcard-rbac.yaml names four objects. The three still-failing objects fall out as:

    Object Verdict
    clusterrolebinding-cluster-reconciler-flux-system IN scope β€” matched by ^cluster-reconciler-flux-system$, kind ClusterRoleBinding
    clusterrolebinding-flux-operator IN scope β€” matched by ^flux-operator$, kind ClusterRoleBinding
    rolebinding-velero-server (ns velero) OUT of scope β€” the CR names kind Role velero-server, but the failing object is a RoleBinding

    πŸ”΄ Why this is NOT yet the decisive answer this issue asked for

    The issue's decision rule is "in-scope residue means the mechanism does not work". Two objects are in scope, which points that way β€” but I could not confirm the premise it rests on, so I am not calling the mechanism broken yet.

    Across a 300-control sample the only statuses workloadconfigurationscans ever reports are failed and passed β€” there is no ignored/excepted state in this resource at all. So a raw failed here is consistent with both explanations: the exception is inert, or the exception is applied when the posture summary/report is computed and this resource simply records the pre-exception scan. Those two have opposite consequences for the migration, and nothing measured so far separates them.

    The next concrete step

    Determine which layer applies a ClusterSecurityException β€” compare an in-scope object's status in workloadconfigurationscans against its representation in the posture summary/report layer. If the summary also still counts it, the mechanism is inert and the six-control migration must not proceed on this design. If the summary drops it, the mechanism works, this resource is just the wrong place to measure, and the real gap is the kind mismatch below.

    One finding that stands either way

    wildcard-rbac.yaml exempts Role velero-server, but the object failing C-0187 in namespace velero is a RoleBinding. Whichever layer applies exceptions, that entry cannot match the object that is actually failing β€” a kind mismatch of exactly the class this issue's "trap in the data" section warns about.

    Releasing my assignment β€” this is a clean hand-off point, and nothing here is blocked.

    Reproduction

    kubectl --context admin@prod get workloadconfigurationscans -A -o json \
      | jq -r '.items[] | select(.metadata.annotations["kubescape.io/wlid"]
          | test("/(clusterrolebinding|rolebinding|clusterrole|role)-"))
          | "\(.metadata.namespace)\t\(.metadata.name)"'
    # then GET each by name and read .spec.controls[<id>].status.status
  8. removed their assignment
    on Aug 5, 2026
  9. devantler commented on Aug 5, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    The inertness is not confined to the namespaceSelector/RBAC mismatch

    This issue's diagnosis is that the six controls are inert because a namespaceSelector
    cannot match a cluster-scoped RBAC object. That mismatch is real. But it is not sufficient to
    explain the data, and a fix built only on it may not change the numbers.

    Control (measured 2026-08-05, admin@prod). readonly-rootfs-pending ignores C-0017
    for namespaces opencost and wedding-app. That is the easy case in every respect: an
    ordinary namespaced Deployment, matched by namespace, no RBAC anchoring, no kind mismatch.

    workloadconfigurationscans/deployment-opencost β†’ C-0017 status=failed
    workloadconfigurationscansummaries/deployment-opencost β†’ C-0017 status=failed

    A correctly-scoped exception on a correctly-shaped object is equally inert.

    The scan result has a dedicated slot for this, and it is empty everywhere

    Each rule entry carries an appliedIgnoreRules field β€” the mechanism by which an applied
    exception would be recorded:

    "rules": [{"appliedIgnoreRules": null, "controlConfigurations": null, ...}]
    

    Swept across 9 exception-covered namespaces, batching by name:

    Namespaces Scan objects Rule entries appliedIgnoreRules set
    kubescape, longhorn-system, flux-system, velero, observability, opencost, wedding-app, cnpg-system, kyverno 225 1448 0

    The zero is ablation-verified, not assumed: the same jq expression run against a synthetic
    object carrying appliedIgnoreRules: ["synthetic-rule"] returns nn=1, so it does detect a set
    value when one exists.

    Two corrections to earlier notes on this issue

    1. A third status exists. The surfaces report passed/failed/skipped β€” the earlier
      note that only passed/failed exist is wrong. But skipped is not an exception state:
      every instance found carries subStatus=configuration, info="Control configurations are empty" (e.g. C-0078, C-0236). Exceptions and skipped are unrelated.
    2. Unqualified LIST strips spec.controls; a multi-name GET does not. The same object
      returns 0 controls via get workloadconfigurationscans -o json and 50 via
      get workloadconfigurationscans <name1> <name2> -o json. Any sweep built on the collection
      LIST silently measures nothing β€” worth knowing before re-deriving counts here.

    Deployment context

    ks-cloud-config reads "keepLocal": true (no external backend consumes these results) and
    "riskAcceptance": true (the feature is configured on). With keepLocal, these CRDs are the
    only observable consumer in this deployment β€” there is no higher posture layer here that could
    be applying the exceptions out of view.

    What this does and does not establish

    Established: no in-cluster surface reflects any ClusterSecurityException, including for
    exceptions whose scope matches correctly. Every count taken over these CRDs β€” including this
    issue's 249 pairs / 156 objects β€” therefore counts exception-covered findings as genuine
    failures, and is an over-count relative to intended posture.

    Not established: whether the deployed operator consumes the kubescape.io/v1beta1 ClusterSecurityException kind at all. A headlamp-exceptions ConfigMap (3 keys) also exists
    in kubescape, which hints the consumption path may not be the CRD. Distinguishing
    "consumed but not recorded" from "not consumed" is the next step, and it decides the fix:

    • if not consumed, correcting the selector to name ClusterRoleBinding/RoleBinding
      kinds will not move any number, and this issue's premise needs reframing;
    • if consumed but unrecorded, the selector fix stands and the surfaces simply cannot be
      used to verify it β€” which changes how "have the migrated four gone to zero?" can be answered
      at all.

    I have not claimed this issue. Recording the measurement so whoever picks it up does not
    build the selector fix on an unverified premise.

  10. devantler commented on Aug 5, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Closing the open question above: there are three surfaces, and only one of them ignores the exceptions

    My previous comment left it open whether the exceptions are "consumed but not recorded" or "not
    consumed". Resolved β€” and the answer is neither, exactly:

    Surface Applies exceptions? Evidence
    CI scan Yes .github/workflows/ci.yaml:431-432 β€” generate-kubescape-exceptions converts cluster-security-exceptions/ to Kubescape's native format, passed as ksail workload scan --exceptions
    Headlamp view Yes kubescape/headlamp-exceptions ConfigMap is populated β€” exceptionPolicies is 34043 bytes and contains C-0017
    Operator CRDs (workloadconfigurationscans, …summaries, configurationscansummaries) No appliedIgnoreRules null across 1448 rule entries / 225 objects / 9 namespaces (ablation-verified); correctly-scoped C-0017 still failed

    So the exceptions are not inert β€” the two surfaces a human or CI actually looks at do apply them.
    What is wrong is the assumption that the operator CRDs reflect them.

    ci.yaml:399-404 states the exceptions directory is "the single source of truth, also consumed
    in-cluster by the kubescape-operator
    ". That parenthetical is the inaccurate part: whatever the
    operator does with the CRD, it does not surface in any of its three stored scan resources.

    What this changes here

    This issue's counts β€” 249 (control, object) pairs over 156 unique objects β€” were taken from the
    operator CRDs, so they are pre-exception figures. That does not weaken the finding about the
    namespaceSelector/RBAC-kind mismatch, which is a static property of the YAML and stands on its own.
    It does mean two things:

    1. The CRDs cannot verify the fix. "Have the migrated four gone to zero?" is unanswerable from
      that surface, because the number there will not move when an exception starts matching β€” it never
      reflected exceptions in the first place. Verify against the CI scan output instead, which is
      the surface that applies them and already gates on --compliance-threshold 95.
    2. Any before/after comparison must name its surface. A pre-exception CRD count compared against
      a post-exception CI figure will show a difference produced entirely by the surface change.

    Correcting my own earlier framing: I wrote that these counts are "an over-count relative to intended
    posture". Accurate as far as it goes, but the useful statement is narrower β€” they are the
    pre-exception view, and the exception-aware view already exists in CI.

  11. 21 remaining items

  12. devantler commented on Aug 11, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    The C-0007 residue is diagnosed β€” the exception names the wrong object population

    The 08-10 differential left exactly one survivor of this issue's original suspicion: delete-rbac
    moved C-0007 by 0. That is now root-caused, and it is not a defect in the kind+name mechanism.

    There are two disjoint object populations, and an exception only suppresses in the one it names.

    delete-rbac.yaml's 26 entries were derived from a live cluster scan, so they name the bindings
    the operators create at runtime. The CI gate runs ksail workload scan over the rendered
    manifests
    , where those runtime bindings do not exist at all. Measured against the scanned resource
    set (ksail 7.178.20, --framework nsa,mitre):

    named in delete-rbac.yaml present in the manifest scan
    cdi-sa, kubevirt-apiserver, kubevirt-controller, cloudnative-pg, longhorn-bind, velero-server, flagger … 0

    And what does fail C-0007 there is each vendored operator bundle's own RBAC β€” four resources,
    named by none of the 26 entries: the cdi-operator ClusterRoleBinding and RoleBinding, the
    kubevirt-operator ClusterRoleBinding, and the kubevirt-operator-rolebinding RoleBinding.

    The control that proves the mechanism, not just the correlation

    Ablating exec-into-container-rbac (the CR that demonstrably works) leaves exactly one C-0007
    sibling finding β€” ClusterRoleBinding/kubevirt-operator. That CR has seven entries; only that one is
    manifest-visible, and it is the entire C-0002 delta. So the working CR and the non-working CR differ
    in precisely one property: whether any entry names an object the scanner can see. delete-rbac is
    not broken β€” it is aimed at a population this surface does not contain.

    Fix

    #3085 adds the four operator-bundle objects. C-0007 goes 4 β†’ 0 and the combined score 97 β†’ 98.
    All four carry genuine delete verbs from the pinned upstream bundles (the CDI cluster role alone on
    12 rule blocks, the KubeVirt one on 50), so they are the by-design class this CR exists to except, and
    C-0007 still flags anything new.

    The live-derived entries are deliberately left in place. They are correct for the cluster surface,
    and removing them would be a posture change resting on the operator's stored resources applying no
    exceptions today β€” a separate defect, not a licence to drop them.

    What this leaves on this issue

    • Acceptance criterion 1 β€” answered (08-10, out-of-scope direction; this comment adds the why).
    • Criterion 4 (controller-rbac.yaml carries entries that cannot match anything) β€” the premise was
      refuted
      on 08-10: that selector suppresses 17 findings. It needs rewording or dropping.
    • The genuinely open residue is the framework coverage already recorded in AGENTS.md: 52 of the 76
      excepted controls are in neither gated framework, so their exceptions are unmeasurable here
      whether or not they are correct. That is the part worth keeping this issue open for.
  13. devantler commented on Aug 12, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Step-1 measurement note β€” 2026-08-12. Partial, and it changes how step 1 must be measured.

    I took step 1 (the C-0187 in-scope/out-of-scope question) and did not finish it, but three of the things I hit invalidate the measurement approach itself, so they are worth more than the answer would have been.

    1. configurationscansummaries LIST is spec-stripped too β€” the freshness check in this issue can read as a dead scanner

    The issue's evidence-freshness section cites "aggregate scan summary non-zero (212 high / 840 medium / 89 low)" as proof the scanner is alive. Taken via LIST, that same check now returns high=0 medium=0 low=0 across all 40 summaries, total findings 0. Taken by name, the same objects return real numbers:

    namespace by-name GET via LIST
    kubescape 166 high / 658 medium / 71 low 0 / 0 / 0
    velero 2 high / 159 medium 0 / 0
    flux-system 0 high / 17 medium 0 / 0

    The known trap was documented for workloadconfigurationscans. It applies to configurationscansummaries as well. I nearly filed a live "posture scanning is dead" incident off that zero β€” the cluster reads exactly like a silently-broken scanner, and every corroborating signal agrees with the false reading (scheduler CronJob Complete in 19s, no pod restarts, newest object creationTimestamp 46 days old because these objects are updated in place rather than recreated). Only the by-name GET separates the two.

    2. A multi-name GET silently returns NOTHING above some batch size

    get workloadconfigurationscans <n1> <n2> works. The same call with 20 names, and with 138, returns no output and no error, and a jq over it yields a clean empty result. My first sweep concluded "0 of 201 RBAC objects carry C-0187" on exactly that silent empty, and it was wrong-shaped evidence, not a finding.

    This compounds badly with the LIST trap: the batching is the documented workaround for the LIST trap, so the recommended fix has its own silent-zero mode. Any sweep here needs a positive control β€” a known-populated object fetched in the same call shape β€” or it cannot distinguish "no findings" from "no output".

    3. What the reliable reading actually says β€” C-0187 is no longer evaluated on the excepted bindings

    At a batch size that is provably working (2 names, verified against a populated control map):

    clusterrolebinding-flux-operator                    controls=1  keys=C-0262
    clusterrolebinding-cluster-reconciler-flux-system   controls=1  keys=C-0262
    

    Both are named explicitly in wildcard-rbac.yaml. On 2026-07-27 this issue measured C-0187 failing on 6 RBAC-anchored objects; today these two carry only C-0262 and no C-0187 at all. Not passed, not failed β€” absent.

    That is not the exception working: absence is not suppression, and appliedIgnoreRules reads null. Nor is it a scanner upgrade β€” the image is still v4.0.10, the same version this issue measured. The one thing that did change is a deployment rollout on 2026-08-08T21:31:40Z, after this issue's measurement and after the follow-up measurements of 08-05/08-07.

    Consequence for this issue

    The 249 (control, object) pairs and the six-control table were measured on 07-27 and the evaluated control set has moved since. Re-measure before migrating anything β€” migrating six exceptions to match a control that is no longer evaluated on those objects would write six more inert entries, which is precisely the failure this issue exists to stop.

    Step 1 as written is still the right first step, but it needs a prior half: establish which controls are currently evaluated on RBAC-kind objects at all, with a positive control in the query shape, before asking whether residue is in or out of scope.

    Not established

    The full in/out-of-scope answer (my chunked sweep timed out), and why C-0187 disappeared. 201 RBAC-kind objects is the current population, which matches the previously recorded figure exactly.

    Unassigning myself β€” I am not carrying this further this run, and on this repo a stale assignment cannot be shown expired (monorepo#2798), so leaving it assigned would park the issue rather than reserve it.

  14. devantler commented on Aug 15, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Step 1 is answered: the mechanism works, and the residue is entirely out-of-scope

    Measured 2026-08-15 on the CI scan β€” the surface that actually applies ClusterSecurityException
    β€” with a paired control, offline, no cluster. This is the first measurement in this thread taken on
    the correct surface with an A/B arm, and it settles the question this issue has been circling.

    Method

    Identical command both arms, ksail 7.178.20, at platform@44f14fab:

    go run ./scripts/generate-kubescape-exceptions -o base.json          # 21 policies
    jq '[.[] | select(.name != "controller-rbac")]' base.json > armB.json # 20 policies
    ksail workload scan --framework nsa,mitre --exceptions <arm> --compliance-threshold 0 --format sarif
    

    The two arms differ by exactly one policy (diff of the policy-name lists returns only
    controller-rbac).

    Result β€” the exception suppresses 17 findings

    Control A: exception applied B: exception removed Suppressed
    C-0053 10 23 13
    C-0037 0 3 3
    C-0031 0 1 1
    C-0063 / C-0035 / C-0188 0 0 0
    C-0034 (other CR) 3 3 0
    C-0015 (other CR) 2 2 0
    Total SARIF results 15 32 17

    This refutes the title. The six entries are not "structurally inert" β€” three of them are load-bearing
    on the gating surface, and removing the CR would add 17 findings to the CI posture gate. Acceptance
    criterion 4 as written ("no longer carries exception entries that cannot match anything") would, if
    applied literally to all six, delete three working exceptions.

    The other three (C-0063, C-0035, C-0188) suppress nothing here β€” but note that is because they produce
    zero findings in both arms, i.e. they are unfalsifiable on this surface, not proven inert.

    The in-scope/out-of-scope question, answered

    All 10 residual C-0053 findings are ServiceAccount-anchored and fall in 7 distinct tenant
    application namespaces
    . None of the seven appears in the CR's namespaceSelector list, which
    enumerates infrastructure-controller and platform-middleware namespaces only.

    (Namespaces and object names deliberately withheld, per this issue's own precedent β€” an enumeration of
    which service accounts hold broad token access is a weakness map. The aggregate and the scope verdict
    are the sanitized minimum needed to review a fix.)

    So the residue is 100% out-of-scope. By this issue's own decision rule β€”

    In-scope residue means the mechanism does not work and the whole approach needs rethinking;
    out-of-scope residue means it works and the CRs are simply incomplete.

    β€” the mechanism works. And the second half does not follow here either: the residue is tenant
    workloads, not infrastructure controllers, so those findings are correctly left flagged. The CR is
    not incomplete; it is scoped exactly right.

    Why this took 19 days and 15 comments β€” the body is the trap

    Every run that picked this up ran step 1 against workloadconfigurationscans/…summaries, discovered
    those surfaces apply no exceptions at all, and posted that finding. That has now happened
    repeatedly, including two self-corrections. The cause is not the runs: the body still prescribes the
    refuted method
    , and the body is what a fresh run reads.

    ci.yaml has recorded the correct surface since #2969 and cites this very issue:

    The kubescape-operator's stored scan resources … do NOT [apply exceptions] β€” their
    appliedIgnoreRules is empty, so they carry pre-exception verdicts. Never read a posture number off
    those to judge an exception's effect; use this scan's output (#2823).

    The codebase knew. The issue body did not. I am updating the body so the next run does not make this
    the sixteenth attempt.

    What actually remains

    1. Answer the in-scope/out-of-scope question β€” done above.
    2. Correct controller-rbac.yaml's NOTE. It currently tells the reader all six controls "share this
      limitation; migrate them the same way if they surface failing objects." Three of them do not share it
      β€” migrating those would remove 17 suppressions. This is the actively harmful part and I am opening a
      PR for it.
    3. Decide C-0063/C-0035/C-0188 on evidence rather than assumption: they are unfalsifiable on the CI
      surface today. Either keep them as forward-looking guards with that stated, or drop them β€” but the
      basis must be recorded, not inferred.
  15. added a commit that references this issue on Aug 18, 2026
  16. moved this from πŸ“₯ Backlog to πŸ‘€ In Review in 🌊 Project Boardon Aug 18, 2026
  17. devantler commented on Aug 18, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    AC 3 answered β€” decision: keep C-0063 / C-0035 / C-0188

    Recorded in controller-rbac.yaml via #3212. The basis, and why the opposite conclusion is the tempting one:

    Both arms show zero findings for these three, which reads as "this exception suppresses nothing, so drop it". That reading is wrong here. They sit outside NSA-CISA and MITRE, so the gating scan never evaluates them β€” the silence is the gate's blind spot, not a property of the exception. AGENTS.md states this directly for the 52 excepted controls in neither framework: never read "the score did not move" as "the exception is broken".

    The discriminator, measured this run (ksail paired-arm oracle, --framework nsa,mitre, baseline C-0015 2 Β· C-0034 3 Β· C-0053 11):

    Ablation Effect Reading
    drop C-0053 11 β†’ 25 positive control β€” oracle is sensitive
    drop C-0037 0 β†’ 3 a zero baseline still responds when the control is gated
    drop C-0063 / C-0035 / C-0188 no change no response at all β‡’ not evaluated

    C-0037 is the load-bearing row. It shares the zero baseline that made these three look inert, yet it responds β€” so a zero baseline is not evidence of redundancy, and the absence of response is what distinguishes the two cases. Removing the three on this scan's silence would have stripped a justified exception governing findings that are real on the surfaces which do evaluate them.

    Re-decide only if one of them comes under a gated framework, at which point the same two-arm oracle gives a real answer.

    Acceptance criteria status

    Note the numbers moved slightly since the 2026-08-15 measurement in the file (C-0053 now 11 β†’ 25, suppressing 14 rather than 13) β€” expected drift from manifest and ksail changes, and it does not affect any conclusion. #3212 updates the recorded figures.

  18. moved this from πŸ‘€ In Review to βœ… Done in 🌊 Project Boardon Aug 18, 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