Skip to content

Establish an oracle that shows whether an in-cluster posture exception applies #2946

Description

@devantler

🤖 Generated by the Agentic Engineer

Evidence

Kubescape posture exceptions in this platform have three consumer surfaces, and measurement on
2026-08-04 shows they behave differently:

Surface Exceptions applied? RBAC-subject controls evaluated?
CI manifest scan (ksail workload scan --exceptions) yes — 71 SARIF results without the flag, 4 with it no — C-0187/C-0053/C-0037/C-0188/C-0031/C-0063/C-0035 produce zero findings
In-cluster workloadconfigurationscansummaries no — exempted longhorn-system/observability still read failed for C-0057, identical to non-exempted kyverno yes — this is where those findings live
In-cluster compliance score / "excluding N controls" claimed, never measured yes

So every RBAC-anchored exception this repo maintains is judged today on a surface that cannot show
whether it works. k8s/bases/infrastructure/controllers/kubescape/cluster-role.yaml documents the
long fight to make the scanner read these CRs (image ≥ v4.0.10 plus supplemental RBAC), but nothing
currently reads back the result.

Affected audience and impact

Platform maintainers and any agent doing posture work. Without this oracle an exception cannot be
told from a no-op, in either direction: a correct exception looks broken, and an inert one looks
configured. #2823 stalled on exactly this — its planned migration of six controls rests on counts
that cannot move regardless of whether the exceptions work.

Expected behaviour

A repeatable, documented read of the surface where in-cluster posture exceptions actually apply, so
an exception change can be verified before and after.

Acceptance criteria

  • A documented, repeatable command reads the scanner's own exception-applied output (the
    "excluding N controls" / compliance figure), not the raw stored summaries.
  • It is shown to discriminate: a deliberately removed or broken exception changes the number, and
    restoring it changes it back. An oracle that reads the same either way is not an oracle.
  • The result settles whether the scanner matches exceptions on the kubescape.io/workload-kind
    label or on the wlid kind — these disagree for every RBAC-subject finding.
  • controller-rbac.yaml's standing instruction ("migrate these if they surface failing objects")
    is corrected to name the surface that actually decides it, so the next reader is not sent back
    to the non-discriminating count.

Rough size

M. Blocks #2823 and any further posture-exception authoring.

Part of #2627.

Activity

  1. moved this to 📥 Backlog in 🌊 Project Boardon Aug 4, 2026
  2. coderabbitai commented on Aug 4, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #2316 - fix(kubescape): make ClusterSecurityException posture exceptions take effect [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]
    #2837 - fix(kubescape): stop the Headlamp mirror granting exceptions the CRs never granted [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. self-assigned this
    on Aug 6, 2026
  4. devantler commented on Aug 6, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Measurement: the planned oracle cannot be built on either in-cluster summary surface

    Read-only measurement against the live cluster today. Two findings, both with their controls run.

    1. The exempted controls appear on the workload surface in NO namespace — exempted or not

    controller-rbac exempts C-0053, C-0037, C-0031, C-0063, C-0035, C-0188 for 21 namespaces
    (longhorn-system among them). None of those six control IDs appears in a
    WorkloadConfigurationScanSummary in longhorn-system — and none appears in github-config or
    crossview either, which that exception does not cover.

    So the absence in an exempted namespace is not evidence the exception applies. Enumerating every
    control result in github-config (10 objects, 28 results) gives 11 distinct control IDs and none of
    the six. This surface simply does not emit RBAC-subject controls, which makes AC-3's
    kubescape.io/workload-kind-vs-wlid question unanswerable here — there is no result to match
    either way.

    2. The two in-cluster surfaces disagree systematically

    For longhorn-system: all 97 WorkloadConfigurationScanSummary objects report
    critical/high/medium/low = 0/0/0/0, and every control result sampled reads passed. The namespace
    ConfigurationScanSummary for the same namespace reports high=104, medium=87, low=50.

    That aggregate's summaryRef lists exactly those 97 objects and all 97 resolve in the list, so this
    is the same object set read two ways. It reproduces: keda agg high=12 / per-workload sum 0;
    homepage 6 / 0; dex 4 / 0.

    Consequence for this issue: ConfigurationScanSummary is the only surface carrying non-zero
    numbers, but it holds severity totals plus refs and nothing else — no control ID, no per-object
    verdict. So it cannot attribute a change to a specific exception, which is exactly what AC-2 requires
    of an oracle. WorkloadConfigurationScanSummary can attribute, but reads zero everywhere.

    Method note

    kubectl get workloadconfigurationscansummaries strips .spec.controls on LIST — all 97 came
    back null. Control data requires a per-object GET. My first pass read those nulls as "0 failures for
    the exempted controls", which looked like a clean confirmation; the positive control (a query that
    should have found some failing control and found none either) is what exposed it. Any oracle built
    here must GET per object, and must carry a control that fails when the oracle is broken.

    What this leaves

    AC-1 is not satisfiable from these two CRs. The remaining candidate is the scanner's own
    compliance/"excluding N controls" output rather than the stored summaries — which is what the issue
    suspected, now with the alternatives eliminated rather than assumed. I did not measure that surface:
    reaching it needs a port-forward this host is not granted, so it is a named gap, not a guess.

    Nothing changed on the cluster; this was read-only.

  5. moved this from 📥 Backlog to 🏃🏻‍♂️ In Progress in 🌊 Project Boardon Aug 6, 2026
  6. devantler commented on Aug 6, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Measured: which surface applies exceptions, and why every prior oracle read the same either way

    Read-only measurement against admin@prod today. Two API behaviours of the Kubescape storage
    aggregated APIserver turn out to explain the non-discriminating reads this issue describes — and
    both are invisible unless you run a negative control.

    1. The store does not apply exceptions — confirmed, with a control group

    infrastructure-privileged ignores C-0045/C-0048 (among others) in longhorn-system. kyverno is
    not in that exception's namespace list, so it is the control group.

    Namespace Exempted for C-0045/C-0048? Stored status
    longhorn-system yes C-0045 failed, C-0048 failed
    kyverno no all seven sampled controls passed

    The exempted namespace reads failed; the non-exempted one reads passed. So the reads work, and
    the stored workloadconfigurationscansummaries are the raw, pre-exception surface. This
    confirms the table in the issue body.

    Consistent with that, the scan trigger carries no exception argument at all —
    cm/kubescape-scheduler posts exactly {"commands":[{"CommandName":"kubescapeScan","args":{"scanV1":{"keepLocal":true}}}]}
    to operator:4002/v1/triggerAction.

    2. Two traps that make a naive oracle non-discriminating

    Both were found by running a deliberately-wrong input and getting the same answer as the right one.

    LIST silently trims spec.controls to null. All 97 summaries in longhorn-system return
    spec.controls: null from a list call; only an individual GET of one object populates it. A
    kubectl get wcss -o json | jq '.spec.controls…' pipeline therefore yields nothing regardless of
    posture.

    Label selectors are silently ignored. This is the more dangerous one:

    kubectl get workloadconfigurationscansummaries -n longhorn-system                                  -> 97
    kubectl get workloadconfigurationscansummaries -n longhorn-system -l …/workload-kind=Deployment    -> 97
    kubectl get workloadconfigurationscansummaries -n longhorn-system -l …/workload-kind=NoSuchKindXYZ -> 97
    

    A bogus selector returns the full set, identical to no selector. The objects carry the labels
    correctly (longhorn-system really is 23 CSIStorageCapacity / 16 Secret / 6 Deployment / …), but
    server-side selection never happens — so filtering must be done client-side on the returned objects.

    Together these two produce exactly the symptom in the issue title: a query filtered by workload
    label and read for control status returns an empty/uniform result whether or not the exception
    works
    , which is the definition of a non-discriminating oracle.

    What this implies for the oracle (criterion 2)

    Because exceptions are applied by consumers rather than baked into the store, the discriminating
    read does not require a prod deploy to ablate. The oracle can be built read-only and client-side:
    raw finding from the store (individual GET, client-side filtering) × the exception set evaluated
    against that workload. Ablating it then means perturbing the exception set locally and
    re-evaluating the matcher, which is cheap and repeatable — a much better fit than the
    change-an-exception-and-redeploy loop this issue was scoped around.

    Criterion 3 (label vs wlid) — not settled yet

    Still open. Note the finding above narrows it usefully: since server-side label selection does not
    happen at all, any answer that relies on selecting by kubescape.io/workload-kind is measuring
    nothing, and the question has to be settled on the matcher's own code path.

    Not a defect: the 21-vs-19 exception count

    Worth recording so the next reader does not re-derive it as drift. The cluster has 21
    ClusterSecurityException CRs while the Headlamp mirror ConfigMap has 19. That gap is
    deliberate — talos-cis-control-plane-false-positives and talos-cis-worker-false-positives both
    carry platform.devantler.tech/headlamp-mirror: exclude, which
    scripts/generate-kubescape-exceptions honours. The mirror is generated, not hand-maintained.

    Part of #2627; this also unblocks the measurement #2823 stalled on.

    (Claim released at the end of this run — no code was written, so the issue is left unassigned and open for the next run to implement against the corrected scope in the follow-up comment.)

  7. devantler commented on Aug 6, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Criterion 1's premise does not hold — proposing a re-scope

    Following the measurement above, I checked for the surface criterion 1 assumes ("the scanner's own
    exception-applied output — the excluding N controls / compliance figure"). It does not exist in
    this cluster.

    • No compliance-, framework-, posture- or report-shaped resource is served by any Kubescape API
      group. The only posture surfaces are configurationscansummaries (a per-namespace severity
      rollup, currently all zeros) and the raw per-workload workloadconfigurationscansummaries.
    • cm/ks-cloud-config has serviceDiscovery: false with local storage: true, and the scheduled
      scan runs keepLocal: true. Nothing is posted to a backend that would compute a compliance score.

    The "excluding N controls" figure is a Kubescape CLI/SaaS artifact, not something this
    deployment produces. So criterion 1 is not merely unmet — as written it is unsatisfiable, and any
    work item derived from it would be building toward a surface that is never going to appear.

    Proposed re-scope (making the same guarantee reachable)

    The intent — "an exception cannot be told from a no-op" — is right and still worth closing. What
    changes is where the answer comes from. Given the store is provably pre-exception (measured above),
    the oracle is a client-side evaluation, not a server-side read:

    1. Raw finding per workload, obtained the only way that actually works: individual GET (LIST
      trims spec.controls to null) with client-side filtering (server-side label selectors are
      silently ignored).
    2. The exception set, evaluated against that workload/control pair.
    3. The oracle's answer = whether the pair is matched. Ablation = perturb the exception set locally
      and re-evaluate — no prod deploy, no scan cycle, and it discriminates by construction.

    This is strictly cheaper than the original framing and it removes the dependency on a redeploy to
    prove discrimination, which is what made the original criterion 2 expensive.

    I have not implemented this yet — this run's output is the measurement and this scoping
    correction. Recording it now so the next run (or another instance) does not spend its time hunting
    a compliance figure that is not there.

    Two notes on sequencing, both honest rather than convenient:

  8. removed their assignment
    on Aug 6, 2026
  9. self-assigned this
    on Aug 21, 2026
  10. added a commit that references this issue on Aug 21, 2026
  11. moved this from 🏃🏻‍♂️ In Progress to 👀 In Review in 🌊 Project Boardon Aug 21, 2026
  12. moved this from 👀 In Review to ✅ Done in 🌊 Project Boardon Aug 21, 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