Skip to content

Authorize audit Secret and Crossplane subjects before evaluation - #1753

Merged
nadaverell merged 4 commits into
mainfrom
fix/audit-subject-authorization
Sep 14, 2026
Merged

nadaverell merged 4 commits into
mainfrom
fix/audit-subject-authorization

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Radar can cache more resources than an authenticated viewer may read. Previously, audit could evaluate cached Secrets and cluster-scoped Crossplane resources without checking that viewer’s grants, exposing findings and evaluation counts about hidden subjects. This PR applies those checks before evaluation and carries incomplete-input warnings through the audit surfaces.

Authorization and behavior

Secret subjects require list secrets in their namespace. Namespace-local grants remain valid when cluster-wide listing is denied. Cluster-scoped Crossplane subjects require list on their exact API group/resource. Other namespaced subjects retain Radar’s existing namespace-visibility policy.

HTTP audit (including raw and per-resource reads), Home, AI context, MCP audit, and MCP resource summaries use the same subject scope. The existing five-second HTTP memo includes authorization scope and typed/dynamic cache identity, preventing one caller’s results from satisfying another caller’s differently scoped request.

Local and fully authorized scans retain their findings and evaluation counts. Restricted callers lose unauthorized subjects and their counts. Existing missingInputs metadata now reaches Home, Checks, MCP and AI/resource context, including when no findings remain. Partial Secret grants are disclosed; namespace-local grants covering the whole ready namespace inventory do not produce a false warning.

Home and Checks show neutral empty states when nothing was evaluated, and qualify results when inputs are missing. Fully evaluated clean scans retain their positive state. These presentation changes also affect full-access users when their scan evaluates nothing. The shared UI gains optional coverage props; downstream hosts need to supply them to display missing-input warnings. Per-resource REST audit responses remain arrays.

Scope and risk

This is the subject-authorization prerequisite for #1741. It does not solve whether orphan checks have enough consumer evidence to assert that a resource is unused. That broader coverage work remains separate. No discovery, informer lifecycle, Kubernetes grant, or cluster-write behavior changes.

Risk is medium and concentrated in audit reads: incorrect scoping could hide valid findings, disclose hidden subjects through counts, or reuse another caller’s results. Tests cover these boundaries and live requests verify identity isolation. Full-access backend results are preserved. Cold permission checks still fan out across candidate namespaces and watched resource types with bounded concurrency; the existing single-slot memo can recompute across differing grant profiles. Large-cluster cold latency is not benchmarked here. Existing permission-cache invalidation timing and other kinds’ namespace-based authorization policy remain unchanged.

Coverage warnings remain scan-wide: namespaces later excluded by local settings can still trigger them, and a terminating namespace can briefly trigger one after its grants disappear. The absence of a warning does not certify that Radar’s own cache covers every namespace; that existing limitation belongs to the separate coverage work in #1741.

Validation

  • Full Go QA, shared audit/resource-context package tests, TypeScript checks, and the full application build passed before the final warning-only correction. Affected audit tests and race checks were rerun for that correction.
  • Full frontend suites passed: 3,695 shared UI tests (one existing skip) and 1,152 web tests.
  • Race tests covered audit, audit-context, HTTP and MCP packages. Regression cases include denial, namespace-local grants, exact cluster grants, group collisions, scope/cache isolation, warning propagation and neutral empty states.
  • Live-tested against the existing Kubernetes 1.36.1 GitOps demo cluster using explicit contexts. Local and authenticated full-access responses had identical summaries and check counts. A real Secret-denied identity received no Secret findings, with missing-input warnings across HTTP, Home and MCP. Alternating identities and 12 concurrent requests retained their own results.
  • Live MCP resource context preserved a missing-input warning at zero findings. Browser checks covered partial findings, partial empty states, authorized zero-evaluation states, and light/dark presentation in seven screenshots.
  • Namespace-local positive grants and cluster-scoped Crossplane grant combinations have automated regression coverage, not live fixture coverage. The final all-namespace warning correction is regression-tested; the earlier live/browser run predates that correction.

All live checks were read-only against the cluster. The current kubeconfig context was preserved, temporary Radar/proxy processes were stopped, and no grants or fixtures were changed.

@nadaverell
nadaverell requested a review from hisco as a code owner September 13, 2026 23:00
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Authorize audit subjects before evaluation

🐞 Bug fix 🧊 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

â€Ē Authorizes Secret and cluster-scoped Crossplane subjects before audit evaluation.
â€Ē Applies one read scope across HTTP, Home, AI, and MCP audit surfaces.
â€Ē Keys audit memoization by authorization scope and cache identity to prevent cross-user reuse.
Diagram

sequenceDiagram
    actor Caller
    participant Surface as HTTP / MCP
    participant Scope as Scope Resolver
    participant RBAC as Kubernetes RBAC
    participant Cache as Resource Caches
    participant Runner as Audit Runner
    Caller->>Surface: Request audit data
    Surface->>Scope: Resolve caller scope
    Scope->>RBAC: Check subject grants
    RBAC-->>Scope: Return permissions
    Scope-->>Surface: Provide ReadScope
    Surface->>Runner: Run scoped audit
    Runner->>Cache: Read watched subjects
    Cache-->>Runner: Return cached resources
    Runner-->>Surface: Authorized findings
    Surface-->>Caller: Findings and summaries
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Post-filter generated findings
  • ➕ Requires fewer changes to audit execution.
  • ➕ Keeps authorization concerns in transport handlers.
  • ➖ Unauthorized subjects still affect evaluation counts and summaries.
  • ➖ Every consumer must duplicate filtering correctly.
  • ➖ Empty namespaces cannot distinguish authorized cluster-scoped findings from unauthorized ones.
2. Run impersonated live API scans
  • ➕ Lets the Kubernetes API server enforce authorization directly.
  • ➕ Avoids exposing shared-cache objects to audit evaluation.
  • ➖ Introduces additional API traffic and latency.
  • ➖ Duplicates or bypasses the existing cache-based audit architecture.
  • ➖ Could trigger discovery and informer lifecycle changes outside this PR's scope.

Recommendation: Keep the centralized pre-evaluation ReadScope approach. It closes findings, counts, summaries, and context leaks at their common source while preserving cache-based auditing, local-mode behavior, namespace policy, and public audit contracts; transport-level post-filtering is incomplete, while live impersonated scans are substantially more expensive and disruptive.

Files changed (18) +740 / -45

Bug fix (13) +221 / -43
runner.goFilter audit subjects before running checks +23/-3

Filter audit subjects before running checks

â€Ē Adds ReadScope to RunOptions, intersects selected and readable namespaces, and removes unauthorized Secrets before evaluation. Filters cached Crossplane resources by exact namespaced or cluster-scoped grants.

internal/audit/runner.go

scope.goIntroduce caller-specific audit read scopes +100/-0

Introduce caller-specific audit read scopes

â€Ē Defines namespace, Secret, and cluster-resource authorization scope semantics, including the distinction between nil and empty access. Resolves exact grants concurrently from already-watched dynamic resource kinds without refreshing discovery.

internal/audit/scope.go

summary.goPropagate authorization into resource audit summaries +2/-2

Propagate authorization into resource audit summaries

â€Ē Accepts audit run options when generating resource summaries so callers cannot derive findings from unauthorized subjects.

internal/auditcontext/summary.go

audit_scope.goResolve MCP audit authorization scopes +22/-0

Resolve MCP audit authorization scopes

â€Ē Builds audit options from the MCP caller's readable namespaces, per-namespace Secret permissions, and exact cluster-resource list grants. Reuses MCP namespace candidates for global viewers lacking cluster-wide Secret access.

internal/mcp/audit_scope.go

resource_context.goAuthorize MCP resource audit summaries +2/-2

Authorize MCP resource audit summaries

â€Ē Passes request context into resource summary generation and applies the caller's resolved audit scope.

internal/mcp/resource_context.go

tools.goPass MCP caller context to audit summaries +1/-1

Pass MCP caller context to audit summaries

â€Ē Supplies context when building MCP resource audit summaries so agent context observes the same authorization boundary.

internal/mcp/tools.go

tools_audit.goApply scope in MCP audit execution +3/-18

Apply scope in MCP audit execution

â€Ē Runs MCP audits with caller-specific options and removes the namespace-only post-filter. This retains authorized cluster-scoped findings while relying on pre-evaluation subject filtering.

internal/mcp/tools_audit.go

ai_diagnose.goAuthorize diagnose audit context +1/-1

Authorize diagnose audit context

â€Ē Routes diagnose health audit summaries through the server's request-aware authorization scope.

internal/server/ai_diagnose.go

ai_handlers.goScope AI resource audit summaries +5/-5

Scope AI resource audit summaries

â€Ē Converts audit summary helpers to server methods that accept the HTTP request and pass caller-specific audit options into summary generation.

internal/server/ai_handlers.go

audit_handlers.goScope HTTP audits and isolate memoized results +31/-8

Scope HTTP audits and isolate memoized results

â€Ē Runs list and per-resource HTTP audits with caller-specific authorization. Expands the five-second memo key to include sorted namespace selection, audit options, and typed and dynamic cache identities, and adds explicit invalidation.

internal/server/audit_handlers.go

audit_scope.goResolve HTTP audit authorization scopes +27/-0

Resolve HTTP audit authorization scopes

â€Ē Builds HTTP audit options from namespace visibility, cluster-wide or namespace-local Secret grants, and exact grants for watched cluster resources.

internal/server/audit_scope.go

dashboard.goApply caller scope to Home audit summaries +3/-3

Apply caller scope to Home audit summaries

â€Ē Makes dashboard audit aggregation request-aware and reuses scoped cached results so Home counts cannot include unauthorized subjects.

internal/server/dashboard.go

namespace_scope.goInvalidate audit memo after context switches +1/-0

Invalidate audit memo after context switches

â€Ē Clears memoized audit results when the active Kubernetes context changes, preventing reuse across resource-cache contexts.

internal/server/namespace_scope.go

Tests (4) +518 / -2
scope_test.goTest audit scope resolution and subject filtering +143/-0

Test audit scope resolution and subject filtering

â€Ē Covers namespace intersections, Secret grants, cluster-resource grants, nil-versus-empty semantics, evaluation counts, MissingInputs behavior, and retained consumer evidence. Adds a benchmark for resolving 1,000 watched kinds.

internal/audit/scope_test.go

audit_scope_test.goVerify MCP subject authorization behavior +122/-0

Verify MCP subject authorization behavior

â€Ē Tests withholding Secret findings and counts, honoring namespace-local Secret grants, and preserving authorized cluster-scoped Crossplane findings across permission changes.

internal/mcp/audit_scope_test.go

tools_audit_test.goAdapt MCP finding filter tests +2/-2

Adapt MCP finding filter tests

â€Ē Updates finding-filter tests for the removal of the namespace allow-list parameter.

internal/mcp/tools_audit_test.go

audit_scope_test.goTest HTTP authorization across audit consumers +251/-0

Test HTTP authorization across audit consumers

â€Ē Covers raw and per-resource audits, Home and AI summaries, namespace-local Secret access, exact cluster-resource grants, full-access parity, cache-key isolation, context invalidation, and concurrent scopes.

internal/server/audit_scope_test.go

Documentation (1) +1 / -0
mcp.mdDocument audit subject authorization boundaries +1/-0

Document audit subject authorization boundaries

â€Ē Documents that Secret audit subjects require namespace-specific list permission and cluster-scoped Crossplane subjects require an exact resource grant. Clarifies that raw audits bypass settings, not authorization.

docs/mcp.md

@qodo-code-review

qodo-code-review Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Cluster-only readers lose audit results 🐞 Bug ≡ Correctness
Description
resolveReadScope returns immediately when namespaces is an allocated-empty slice, so it never
resolves ClusterResources, and RunFromCache also treats that namespace state as a reason to stop
the entire scan. This affects authenticated users with no namespaced-resource access but an exact
list grant on a watched cluster-scoped Crossplane resource, whose authorized findings disappear from
HTTP, Home and AI, and MCP audit surfaces.
Code

internal/audit/scope.go[R30-31]

+	if namespaces != nil && len(namespaces) == 0 {
+		return &ReadScope{Namespaces: []string{}, SecretNamespaces: []string{}}
Relevance

●●● Strong

Early empty-scope return independently suppresses authorized cluster subjects; accepted
authorization precedents favor preserving permitted findings.

PR-#1012
PR-#1694

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
AllowedNamespaces is expressly scoped only to namespaced resources, while cluster-scoped access
must be decided independently. The newly added early return prevents that independent decision, and
the runner plus both transports contain corresponding whole-audit exits that make the authorized
cluster subjects unreachable.

pkg/auth/permissions.go[17-30]
internal/audit/scope.go[29-55]
internal/audit/runner.go[31-40]
internal/server/audit_handlers.go[123-128]
internal/mcp/tools_audit.go[60-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Users with no namespaced-resource access but an exact list grant on a watched cluster-scoped resource receive no audit results. Empty namespace scope currently prevents cluster grants from being resolved and causes HTTP, MCP, and the runner to return before cluster-scoped subjects are evaluated.

## Fix Focus Areas
- internal/audit/scope.go[29-58]
- internal/audit/runner.go[31-40]
- internal/server/audit_handlers.go[123-128]
- internal/mcp/tools_audit.go[60-77]

## Recommended Fix
Resolve cluster-resource grants even when the namespaced scope is allocated-empty, and allow global audit requests to continue into cluster-scoped collection for that permission profile. Initialize namespaced audit input as empty instead of using the empty namespace slice with collectors where empty means all, while preserving the existing denial behavior for explicitly requested inaccessible namespaces.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Denied audits appear fully healthy ⊘ Outdated 🔗 Cross-repo conflict ≡ Correctness
Description
RunFromCache now sets denied Secret input to nil, but Radar Hub Web hides its missing-input
warning whenever the aggregate evaluated count is zero. This occurs when a connected scope has
denied Secret access and no other evaluated subjects, reaching the healthy “Nothing to remediate”
state despite incomplete coverage.
Code

internal/audit/runner.go[R40-41]

+	if !scope.hasSecretSubjects(namespaces) {
+		input.Secrets = nil
Relevance

●●● Strong

Denied inputs should remain visibly incomplete; accepted history prioritizes preventing false
healthy states from unavailable audit data.

PR-#1012
PR-#1694

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR converts complete Secret authorization denial into unavailable input. Radar's shared contract
says unavailable inputs are omitted from check counts and exposed through MissingInputs; Hub
forwards that field, but Hub Web only displays it when coverage.evaluated > 0 and otherwise treats
any connected zero-row result as healthy.

internal/audit/runner.go[35-49]
pkg/audit/types.go[123-136]
External repo: skyhook-dev/radar-hub, internal/server/fleet_handlers.go [1051-1062]
External repo: skyhook-dev/radar-hub, internal/server/fleet_handlers.go [1192-1208]
External repo: skyhook-dev/radar-hub-web, src/pages/fleet/checks/ChecksQueue.tsx [95-109]
External repo: skyhook-dev/radar-hub-web, src/pages/fleet/checks/ChecksQueue.tsx [228-255]
External repo: skyhook-dev/radar-hub-web, src/pages/fleet/checks/ChecksQueue.tsx [303-345]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Radar now reports denied Secret inventory through `MissingInputs` while omitting dependent check counts. Radar Hub Web suppresses that warning when no subjects were evaluated and consequently renders its healthy empty state.

## Fix Focus Areas
- src/pages/fleet/checks/ChecksQueue.tsx[228-255]
- src/pages/fleet/checks/ChecksQueue.tsx[303-345]

## Recommended Fix
In Radar Hub Web, show the coverage status whenever either evaluated subjects or missing inputs exist. When missing inputs are present, prevent the zero-row state from claiming every resource passed and render an incomplete-coverage state instead.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 44 rules
✅ Cross-repo context — repo relationships
  Explored: repo: skyhook-dev/radar-hub (sha: 6e33315d)
  Explored: repo: skyhook-dev/radar-e2e (sha: e4918cc2)
  Explored: repo: skyhook-dev/radar-hub-web (sha: 6e0f4055)
Review mode: 🧠 Deep: This is a security-sensitive, cross-cutting authorization change spanning audit, HTTP, MCP, AI, dashboard, caching, and dynamic-resource paths with substantial new logic and many independent edge cases.

Grey Divider

Tip of the day
ðŸ’Ą Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/audit/scope.go
Comment thread internal/audit/runner.go
@nadaverell
nadaverell force-pushed the fix/audit-subject-authorization branch from 3936ba8 to b8b766f Compare September 14, 2026 13:50
@qodo-code-review

Copy link
Copy Markdown

ⓘ Your Qodo trial ends soon. Ask your workspace admin to set up billing to keep reviews running after the trial. Manage billing

@nadaverell
nadaverell merged commit aea974b into main Sep 14, 2026
9 checks passed
@nadaverell
nadaverell deleted the fix/audit-subject-authorization branch September 14, 2026 14:36
nadaverell added a commit that referenced this pull request Sep 15, 2026
## Why

A ConfigMap or Secret can look unused because Radar cannot see its
consumer. For example, a workload can consume a Reflector mirror in
another namespace while the audit is displaying only the source
namespace. Suggesting deletion from that incomplete view is misleading.

This PR makes the unused ConfigMap/Secret check distinguish observed
use, supported evidence of no use, and unknown evidence. It builds on
the subject authorization and incomplete-scan presentation merged in
#1753, and reuses the Reflector metadata parser from #1740. It has no
remaining prerequisite on the deferred discovery work in #1750.

## Behavior

| Available evidence | Result |
|---|---|
| An observed supported reference uses the object | Count it as
evaluated and passing, even if other consumer inventories are
unavailable |
| No reference, with adequate readable and initially synced consumer
inventories | Report the object as unused |
| No reference, with a required inventory unavailable or incomplete for
the subject namespace | Exclude it from both findings and
passing/evaluated counts; disclose incomplete reference evidence |

The namespace picker selects subjects. For Reflector-annotated subjects,
the collector can inspect authorized consumers of remote mirrors. Use
propagates through observed same-kind links, including chains and
cycles. Unused automatic mirrors with visible sources are exempt, but
their existence does not establish source use.

Typed workload, ServiceAccount and Ingress inventories use the existing
per-kind readiness and namespace-scope APIs. The existing dynamic
reference handlers share a registry with their ConfigMap/Secret and
cross-namespace requirements, so discovery and collection agree about
which installed integrations matter. An installed but unsynced consumer
kind is unknown, not empty. Partial discovery of a relevant group
prevents an absence claim; unrelated discovery failures do not.

Cert-manager Issuer credential references cover ACME, CA, Vault and
Venafi fields. ClusterIssuer credentials use the namespace established
by an observed controller's explicit flag, including a literal or
downward-API environment value. Hidden controllers, unresolved values or
configuration files without an explicit namespace flag do not produce a
guessed namespace. Explicit flags take precedence over configuration
files. An authorized, synced, empty ClusterIssuer inventory needs no
controller namespace.

## Scope and tradeoffs

The runtime change stays in the audit collector and orphan check. It
adds no shared discovery lifecycle, informer failure latch, permission
grants, new watches, scan-cache redesign or coverage response schema.
Existing `missingInputs` carries `configmap-references` and
`secret-references` through REST, Home, AI context and MCP. Tests in the
HTTP/MCP adapters keep their authorization assertions while explicitly
supplying the core-only inventory they depend on.

Full-access users can also lose unsupported orphan findings while
required inventories are still syncing. Restricted users can see fewer
orphan evaluations when an installed integration can reference Secrets
across namespaces. This is deliberate: an omitted unknown evaluation is
preferable to an unsupported deletion suggestion. Known use still
counts. The check covers supported reference fields, not arbitrary
controller behavior. Existing Istio gateway workload-namespace and
Contour delegated TLS resolution limitations are outside this change.

Readiness here means initial synchronization, not continuous watch
freshness. The existing scan memo can lag evidence changes by up to five
seconds. The per-resource audit endpoint retains its findings-array
shape and has no completeness metadata; the scan and AI resource context
provide that distinction. Package callers that omit
`ConfigReferenceEvidence` can establish observed use but cannot
establish absence.

## Validation

- `make test`, `make tsc`, and `make build` passed.
- Race tests passed for `pkg/audit`, `pkg/configrefs`, and the affected
audit/server/MCP scenarios.
- Live tests used the existing GitOps demo cluster with an explicit
kubectl context. Local and authenticated full-access results matched
exactly; Secret-denied identities disclosed missing inputs without
Secret findings. Alternating and concurrent identities retained separate
results.
- A temporary ConfigMap-only Reflector fixture exercised the central
case. Complete visibility evaluated two subjects, with the remotely
consumed source passing and the unused ConfigMap reported. A
source-namespace-only cache evaluated only the unused ConfigMap and
reported `configmap-references` for the unknown source. Configured REST,
dashboard, MCP and the rendered Checks page preserved that warning.
- Live test fixtures and processes were removed. No Kubernetes grants or
current-context changes. One actual embedded-UI screenshot verified the
warning; no frontend source changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant