Skip to content

fix(security): Kubescape control-level scan detail is 54 days stale while summaries stay current #3275

Closed as not planned

Description

@devantler

🤖 Generated by the Agentic Engineer

Evidence

Kubescape's two configuration-scan surfaces disagree, and only one of them is current. Measured
against admin@prod on 2026-08-20:

Resource Count Newest object State
workloadconfigurationscansummaries 2358 2026-08-20T10:07:15Z (today) current, fresh objects daily
workloadconfigurationscans (detail) 1542 2026-06-27T14:50:44Z 54 days stale

Every detail object falls in a single 2026-06-17 → 2026-06-27 window. Nothing has been written to
that surface since, while summaries have continued daily and carry real severity data — today's
newest reads {"critical":0,"high":0,"low":0,"medium":3,"unknown":0}.

The scan itself is healthy: the scheduler Job succeeded at 2026-08-20T09:12:30Z and the summaries
appeared ~55 minutes later. So this is not the outage described in #2933 — that one is resolved.
This is one surface of a working scanner having gone quiet.

Impact

workloadconfigurationscansummaries carries severity counts. workloadconfigurationscans
carries the control-level detail — which control failed, on which rule. Any analysis that needs
to name a control reads the detail surface, and therefore reads June data.

Concretely, rule-level questions like "is C-0211 still failing, and on which rules" cannot be
answered correctly today: the answer describes the cluster as it was two months ago. A control fixed
since then still reads as failing, and a control that started failing since is invisible. Posture
work that graduates "zeroed" controls to enforcement is exactly the consumer that would be misled.

This is the same class of defect as #2933 — a surface that looks populated while being wrong — which
is why it is Security rather than a Chore. 1542 objects is a healthy-looking count.

Expected behaviour

Either the detail surface is written on every scan alongside the summaries, or — if the platform
deliberately retains only summaries — that is stated, and anything reading detail objects checks
their age instead of trusting their presence.

Acceptance criteria

  • Determine why detail objects stopped being written on 2026-06-27 while summaries continued:
    deliberate retention/cleanup, a storage or RBAC change, or a genuine regression.
  • If it is a regression: detail objects are written on each scheduled scan, evidenced by an
    object newer than the fix.
  • If it is deliberate: record it where a reader of the detail surface will find it, and state
    what the supported source of control-level detail is instead.
  • Either way, a consumer of control-level detail can tell stale data from current data without
    already knowing this
    — an age check, not a presence check.

Notes

Found while re-verifying #2933, which is a different failure on the summaries surface and is now
resolved. Read control detail with a single-object GET: a LIST strips .spec.controls, so a
list-based check reports empty content for every object regardless of freshness.

Part of #2933's family of posture-integrity defects.

Activity

  1. moved this to 📥 Backlog in 🌊 Project Boardon Aug 20, 2026
  2. added a commit that references this issue on Aug 20, 2026
  3. devantler commented on Aug 20, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Re-verified independently against admin@prod on 2026-08-20T22:07Z, and advanced acceptance
    criterion 1. The freeze reproduces, the premise survives a check it could have failed, and the
    cause is narrowed to a single change — but not yet determined.

    The freeze reproduces, and it is not an artifact of creationTimestamp

    Resource Count Oldest Newest State
    workloadconfigurationscans (detail) 1542 2026-06-17T20:09:12Z 2026-06-27T14:50:44Z frozen, now 54 days
    workloadconfigurationscansummaries 2358 2026-06-17T20:09:12Z 2026-08-20T10:07:15Z current

    creationTimestamp alone could not settle this: an object updated in place keeps its original
    creation time, so a frozen-looking detail surface could equally have been a fresh one being rewritten.
    It is not — checked on a single-object GET:

    • a detail object from 2026-06-17 carries resourceVersion: 1 and no managedFields — created
      once, never updated since;
    • the instrument discriminates: a summaries object from the same 2026-06-17 batch carries
      resourceVersion: 2, so this storage layer does increment on in-place update, and a summaries
      object created today carries resourceVersion: 1.

    So the detail surface is frozen on two independent signals, with the control proving the signal is
    capable of showing movement. Both surfaces start on the same day, so this is one surface of a
    working scanner stopping — not a surface that never worked.

    What is ruled OUT

    • The scan. It is healthy: the most recent scheduled run completed, evaluating 227 of 243
      controls
      with an overall compliance score of 83, and wrote its results. fix(security): a Kubescape framework-load failure still reports zero findings instead of failing #2933's cause is
      fixed and stays fixed.
    • The storage layer. Other kinds served by the same aggregated API are current to within hours.
    • Retention/cleanup. The storage cleanup task runs on a 6h interval and the June detail objects
      survive it, so nothing is deleting them — new ones simply stop being written.
    • The exception path erroring. Today's scan logs Loading exceptions / Loaded exceptions
      with no error.
    • Repo consumers being misled. Nothing in this repository reads the stale surface: CI runs its
      own scan (and ci.yaml already warns never to read a posture number off the stored resources),
      and kubescape-backlog-bridge consumes the summary kind. The exposed consumer is a human or
      agent doing rule-level analysis — which is exactly criterion 4's concern.

    Strongest lead: a 12-minute causal window

    The last detail object was written at 2026-06-27T14:50:44Z. c199323d
    "fix(kubescape): enable riskAcceptance so SecurityException CRs are applied (#2298)" is authored
    2026-06-27T15:02:48Z — 12 minutes later — and no detail object has been written since. The
    surface's first object (2026-06-17T20:09:12Z) likewise coincides with 0928f65c, which enabled
    offline scanning that day. Both boundaries of the surface's entire lifetime land on a
    kubescape capability change.

    That correlation is tight enough to lead with and not sufficient to call a cause: exceptions
    load cleanly today, so if riskAcceptance: enable is responsible it is by changing which write path
    the scan takes, not by failing visibly.

    Where the remaining question actually sits

    helm-release.yaml documents that an offline continuous scan "persists NO
    workloadconfigurationscan CRs"
    because it dies before completing, and that the daily keepLocal
    scheduler scan "DOES work". Today's scheduler scan completes and saves results — yet persists no
    detail CRs while summaries continue. So the open question is narrow and specific:

    On the current configuration, which component writes workloadconfigurationscansummaries, and why
    does that path still run while the detail write does not?

    Answering that decides criterion 1's branch (regression vs. deliberate), and everything after it
    follows. Logs cannot answer it — the freeze predates every current pod's log window by weeks, which
    is why this is reasoned from object metadata and change history instead.

    Note on the staleness alert

    The alert added in #3237 is deployed and working correctly — its most recent run reports
    Posture scan data is stale: Newest stored scan is 2026-06-27T14:50:44Z — 54 day(s) old, across 1542 object(s). Detection is not the gap here; the gap is the cause.

  4. self-assigned this
    on Aug 28, 2026
  5. added 2 commits that reference this issue on Aug 28, 2026
  6. moved this from 📥 Backlog to 👀 In Review in 🌊 Project Boardon Aug 28, 2026
  7. devantler commented on Aug 28, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Root cause is now confirmed and fixed in draft PR #3423 at 2a2dd71791139a3ea3d951a7496faa3104cc7c75.

    The scanner is pinned to v4.0.10. In a self-hosted deployment with no SaaS backend, that release can auto-enable submission, enter account-ID generation, and treat chmod /home/nonroot/.kubescape: operation not permitted as fatal before the in-cluster report receiver persists detailed posture results. This explains the observed split: scans still compute a compliance score and the scheduler Job succeeds, while the detailed result surface remains stale. Risk-acceptance-triggered rescans reach the aborting request path because they do not carry the scheduler's keepLocal override.

    Upstream fixed the full chain in kubescape/kubescape#2556: no-backend deployments skip SaaS initialization, explicit local-only intent is preserved, and permission tightening is best-effort rather than scan-fatal. The fix first ships in stable v4.0.12.

    #3423 pins v4.0.12 without weakening the non-root security context, and adds an executable CI contract that rejects vulnerable or malformed scanner versions while preserving local-only scheduling and policy-artifact downloads. The regression was RED on v4.0.10 and is GREEN on v4.0.12; both repository overlays validate.

  8. moved this from 👀 In Review to ✅ Done in 🌊 Project Boardon Aug 28, 2026
  9. devantler commented on Aug 28, 2026

    @devantler
    ContributorAuthor

    The merged scanner upgrade removed the v4.0.10 local-mode abort and the exact recovery scan reached policy evaluation and saved results. That recovery exposed a second, independent persistence defect: the live scanner configuration has continuousPostureScan: false.

    Kubescape v4.0.12 writes detailed WorkloadConfigurationScan objects only when that flag is true, while it always writes summary objects. This exactly matches production: summaries are current, but 1,542 detailed objects still stop at 2026-06-27.

    I am reopening this incident and preparing a follow-up fix that enables detailed persistence while rendering an empty continuous-scanning resource match set. The pinned operator source confirms an empty set creates zero Kubernetes resource watches, so scheduled scans can persist locally without restoring the event-triggered scan flood that led to the capability being disabled.

    Production recovery remains in progress until a post-deploy scheduled scan makes the detailed objects current and the exact scan reaches a terminal success state.

  10. moved this from ✅ Done to 🫴 Ready in 🌊 Project Boardon Aug 28, 2026
  11. 16 remaining items

  12. moved this from 👀 In Review to ✅ Done in 🌊 Project Boardon Aug 28, 2026
  13. moved this from ✅ Done to 🫴 Ready in 🌊 Project Boardon Aug 28, 2026
  14. moved this from 🫴 Ready to 👀 In Review in 🌊 Project Boardon Aug 29, 2026
  15. moved this from 👀 In Review to ✅ Done in 🌊 Project Boardon Aug 29, 2026
  16. devantler commented on Aug 29, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Reopening because the live acceptance test after #3444 failed; that deployment slice auto-closed the issue before production recovery was proved.

    The controlled post-deploy scan completed local evaluation, but foreground detailed-result persistence exceeded its request deadline. The detailed storage surface remained unchanged at 1,597 objects, newest 2026-08-28T21:16:45Z, with zero hydrated control maps.

    The immediate-transaction fix removed the two-maintenance-worker lock-promotion race, but it reserved SQLite's sole writer throughout minutes of profile reads. Background workers could hand off that reservation long enough to starve the foreground REST write. #3448 is now merged with the corrected design: retain deferred transactions, serialize profile maintenance to one worker, keep the 60-second busy timeout, and prove foreground write availability with an executable regression.

    The signed immutable image is awaiting deployment through #3450. This incident remains open until that PR merges, Flux reconciles it, and a new controlled live scan advances the detailed-object timestamp with nonempty controls and no correlated persistence failure.

  17. moved this from ✅ Done to 🫴 Ready in 🌊 Project Boardon Aug 29, 2026
  18. devantler commented on Sep 2, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Re-measured live today — the headline is no longer true, and the proxy behind it does not hold

    This issue's evidence is from 2026-08-20. Re-measured against prod on 2026-09-02, read-only:

    surface count newest object
    workloadconfigurationscansummaries 2539 2026-09-02T10:21:01Z
    workloadconfigurationscans (detail) 2343 2026-09-02T10:21:01Z

    The two surfaces now carry the same newest timestamp. The detail surface is being written: 44 objects created in the last 24 h, 802 in the last 7 days. So "54 days stale, nothing written since 2026-06-27" is no longer the state of the cluster.

    The June cohort persists — but it is not what it looks like

    1541 objects still carry June creation stamps, all in the original 2026-06-17 → 06-27 window (the issue counted 1542). Two hypotheses, both tested:

    Orphans left by deleted workloads? — NO. Sampled six June-cohort Deployment objects; all six workloads still exist (actual-budget, ascoachingogvaner ×2, backstage, cert-manager ×2). They are live workloads, not residue.

    Frozen because never updated? — the evidence does not support that reading either. A June object carries resourceVersion: "1", which looks conclusive until you run the control: an object created today at 07:01Z also carries resourceVersion: "1", and across the whole surface resourceVersion varies (1, 2, 3, 8, 9 …). So rv: "1" means "not rewritten since it was created", which is equally true of a fresh object — it is not a staleness signal.

    Every object also carries a kubescape.io/sync-checksum annotation, which is what a checksum-based sync looks like: an object is rewritten when the scanned workload's configuration changes, not on every scan. Under that model a June creation stamp on an unchanged workload is the expected steady state, not a defect — and creationTimestamp is not a freshness proxy for this surface at all. That proxy is what the original measurement rests on.

    What this does and does not settle

    Settled: the detail surface is live, both surfaces agree on their newest object, and the June cohort is not orphaned.

    Not settled — and this is the decisive test: whether a June object's content still describes its workload correctly. Take a June-cohort workload whose spec has demonstrably changed since June, single-object GET its scan object (a LIST returns .spec.controls null, so a list sweep answers this wrongly), and compare the controls against the current spec. If they disagree, the sync is genuinely missing updates and this issue stands with a corrected diagnosis. If they agree, the surface is behaving as designed and the issue should close.

    I did not run that comparison — it needs a workload with a known spec change since June, which I could not establish read-only in the time available.

    Recommendation

    Do not "fix" this on the current evidence: the measurement it rests on does not distinguish a stale object from a correctly-unchanged one. Re-frame the acceptance criteria around content correctness rather than object age, then re-decide. Leaving this open and unclaimed with the corrected framing so the next run starts from the right question.

  19. added a commit that references this issue on Sep 2, 2026
  20. devantler commented on Sep 2, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Ran the decisive test this issue asked for — the surface is behaving as designed

    The previous comment named the test it could not run:

    Take a June-cohort workload whose spec has demonstrably changed since June, single-object GET its
    scan object [...] and compare the controls against the current spec. [...] If they agree, the
    surface is behaving as designed and the issue should close.

    It blocked on "it needs a workload with a known spec change since June, which I could not establish
    read-only"
    . That gap is closable: a Deployment's rollout time is readable read-only from
    status.conditions[type=Progressing].lastUpdateTime. 60 of the 64 June-cohort Deployment scan
    objects belong to workloads that rolled out after their scan object was created.

    The content is current — proven by a dated marker inside it, not by object age

    Rather than compare control verdicts field-by-field (which needs a per-control oracle), I used a
    dated marker: k8s/bases/infrastructure/cluster-security-exceptions/pod-security-mutations-unscoped.yaml
    was added 2026-08-18 (c8c2197e, #3225 — git log --diff-filter=A, not -1, which returns the
    newest commit and would have dated it 08-30).

    Every June-cohort object cites it:

    measurement result
    June-cohort Deployment scan objects examined 64
    ...carrying appliedIgnoreRules: [pod-security-mutations-unscoped/…] 64
    ...unreadable / no controls 0
    negative control (a name that must not match) 0 matches — matcher discriminates

    Content that cannot predate 2026-08-18 sits inside objects stamped 2026-06-17. The objects are
    rewritten in place; creationTimestamp records when the CR was first created, never when its
    content was last written. That is a complete census of the cohort, not a sample.

    Both other claims in this thread also re-test clean

    claim live result today
    detail surface stale vs summaries detail newest 2026-09-02T10:21:01Z · summaries newest 2026-09-02T10:21:01Z — identical
    continuousPostureScan: false (2026-08-28) true in the live ks-cloud-config, and deliberately declared in helm-release.yaml

    The original incident's root cause — the v4.0.10 local-mode abort — was fixed by the v4.0.12 pin in
    #3423.

    Decision: closing

    Every premise this issue rests on has been re-measured and none survives. Per the previous comment's
    own criterion, the agreeing case is the close case.

    The reusable finding — creationTimestamp is not a freshness proxy for this surface. The sync is
    checksum-driven: an object is rewritten when the scanned configuration changes, so an old creation
    stamp on an unchanged workload is the designed steady state. A freshness monitor on this surface must
    key on the newest object across the surface, or on dated content, never on per-object age.

    Two method notes for whoever measures here next:

    • A LIST returns .spec.controls as null. Only a single-object GET populates it, so a list
      sweep answers content questions wrongly.
    • resourceVersion on this surface is per-object (it starts at 1), not the cluster's global etcd
      counter — but it is still not a staleness signal on its own, for the reason the previous comment
      established.

    Not re-litigating the separate, already-documented caveat that a suppressed control reports
    status: passed with subStatus: "w/exceptions" — that fail-open is recorded in the exception file
    itself and tracked in #2824.

    Reopen if a content-level disagreement is ever demonstrated; object age alone is not that evidence.

  21. moved this from 🫴 Ready to ✅ Done in 🌊 Project Boardon Sep 2, 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