Skip to content

Commit 2a4ffb1

Browse files
Redact Secret values from Helm diffs for AI callers and bind radar:ai for background Diagnose (#1949)
Linear: RAD-657 ## Why Radar Cloud's AI Diagnose is blind on clusters whose role bindings are off (`cloud.defaultRbac.create=false`, IdP groups decide access), because it reads as `radar:viewer`, which is unbound there. After this release: - **Manual** Diagnose runs read as the person who started them (skyhook-dev/radar-hub#292), so the AI never exceeds that person's access. A person may be able to read Secrets, so Helm diffs handed to the model must not carry Secret values. - **Background** (alert-triggered) runs read as one view-only identity, the `radar:ai` group (skyhook-dev/radar-hub#288), which this chart binds. MCP clients are not affected: they keep reading with the user's own identity. ## What changes **1. Secret redaction in `get_helm_release` (every MCP caller)** - `include=diff` (the manifest diff) and `include=notes_diff` were returned unredacted; only `values_diff` went through `RedactSecrets`. - Each `kind: Secret` document now has its `data` / `stringData` values replaced by `[REDACTED]` before diffing. The keys stay, so added and removed keys still show. Secrets inside `kind: List` items are covered, the `last-applied-configuration` annotation (a full copy of the object) is dropped, and a document that doesn't parse is withheld whole. Other documents are left byte-for-byte untouched. - `RedactSecrets` then runs over both diffs as a second layer. - `include=values` and `include=values_diff` now use key-aware redaction built for Helm values (`aicontext.RedactHelmValues`). Chart authors name credentials freely (`dbPassword`, `auth.postgresPassword`), so any key ending in `password`, `token`, `apiKey`, `secretKey`, `clientSecret`, `credentials` and similar is masked, numbers included. Secret *references* (`existingSecret`, `*SecretName`, `*Ref`) keep their values. The values diff masks both revisions before diffing. CRD specs keep the existing exact-key matching. - The REST/UI Helm diff is unchanged: it is the user's own view. **2. `radar:ai` chart binding** - New `templates/cloud-rbac-ai.yaml`: binds `radar:ai` to `view`. No Secret role, no writes. - `radar:ai` is added to the cluster-read and integration-read add-ons. - New value `cloud.aiRbac`, default `true`, independent of `cloud.defaultRbac`. As with `cloud.systemRbac`, an absent value means off, so a `--reuse-values` upgrade never gains the binding silently. - Docs: the chart README and `docs/authentication.md`. The binding does nothing until Radar Cloud sends the group. Background runs send `radar:ai` with no tier group, so the AI's access is exactly this binding: `cloud.aiRbac=false` really turns it off, and a widened viewer role doesn't widen the AI. Clusters on older charts get no background Diagnose until they upgrade. Radar Cloud refuses manual runs on a Radar older than this release, rather than reading with less protection. ## Tests - Go unit tests for Helm values redaction: nested keys, numeric credentials, references kept under `credentials`, both revisions of a values diff. - Go unit tests for the manifest-diff redaction: Secret values gone, keys and non-Secret changes kept, last-applied copies, Secrets inside Lists, and unparseable documents. Plus a check that `resource_diff` masks Secret values. - `TestIntegrationReadBindings` expects the `radar:ai` bindings alongside `radar:system`, with cases for `aiRbac=false`. - New `tests/cloud_rbac_ai_test.yaml`: renders with `defaultRbac.create=false`, absent or `false` means off, bound to `view` only, add-ons present whatever the tier settings. - `scripts/test-chart.sh`: new render cases; the "no orphan role" cases now also set `cloud.aiRbac=false`. - `helm unittest`: the `service_test.yaml` failures also fail on `main` locally; CI's Helm chart job passes. ### Live test on EKS (proxy auth, simulated Diagnose identities) - **Background identity** (`radar:ai,radar:org:<org>`) with the chart's `radar:ai` binding: pods in every namespace, no Secrets. - **Manual run as a contractor** whose IdP group has `view` in one namespace: pods in that namespace only; listing all namespaces returns only that one. - **Helm release with a Secret, a token in its notes, and `dbPassword` / `apiKey` values**, read through `get_helm_release` by a user who can read Secrets: no sentinel value in `values`, `diff`, `notes_diff` or `values_diff`. The REST diff (the user's own view in the UI) still shows them, as intended. ### End to end on kind - **Role bindings off, `aiRbac=true`:** the AI identity reads pods, events and nodes; Secrets are forbidden. - **`aiRbac=false`:** it reads nothing. - **`get_helm_release include=diff,notes_diff`, called by an owner who can read Secrets:** no Secret values in the output. The #1899 binary returns the plaintext values and a `ghp_` token from the notes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes cluster RBAC defaults and how credentials appear in MCP Helm diffs; misconfiguration could block background Diagnose or leak secrets if redaction regresses, but grants stay read-only and UI paths are unchanged. > > **Overview** > Adds **default Kubernetes RBAC for automatic Radar Cloud Diagnose** via new `cloud.aiRbac` (default on): chart-owned aggregated **view** role for `radar:ai`, plus cluster-read and integration-read add-ons—**no Secret access**. Refactors `radar:system` the same way (owned aggregated view role instead of binding directly to `view`), with Secret read unchanged for alerts/timeline. Docs and Helm tests cover independence from `cloud.defaultRbac`, `--reuse-values` absent-key behavior, and installer `escalate` requirements. > > **Hardens Helm MCP output for AI callers** while leaving REST/UI diffs as the user’s own view: manifest diffs strip Secret `data`/`stringData` (and unparseable Secret docs) before diffing; values diffs use new **`RedactHelmValues`** (suffix/key-aware credentials, preserved secret *references*); notes diffs get pattern redaction. `get_helm_release` `include=diff`, `values_diff`, and `notes_diff` route through these paths. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 16be4f7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Nadav Erell <nadaverell@gmail.com>
1 parent 228c7c3 commit 2a4ffb1

22 files changed

Lines changed: 1293 additions & 100 deletions

‎deploy/helm/radar/README.md‎

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -161,26 +161,47 @@ Radar binary (including Radar Cloud self-upgrade) does not update RBAC. Missing
161161
on an older `--reuse-values` installation default to enabled; set explicit false
162162
before upgrading if the added visibility is unwanted.
163163

164-
### Radar Cloud background identities (`radar:system`)
165-
166-
The hub's alerts worker and timeline puller call Radar
167-
as the `radar:system` group, not as a user. `cloud.systemRbac` (default `true`)
168-
binds that group to a read-only set: `view`, the cluster-read and
169-
integration-read add-ons above, and `get/list/watch` on Secrets. Secret read is
170-
what Helm release alerts and Secret changes in the hub timeline need, because
171-
Helm stores each release as a Secret.
172-
173-
It is independent of `cloud.defaultRbac`, so turning the role bindings off
174-
(`cloud.defaultRbac.create=false`, for example when IdP groups decide cluster
175-
access) leaves alerts and the hub timeline working. It is all or nothing:
176-
`cloud.systemRbac=false` removes the whole grant, and alerts and the hub
177-
timeline then see nothing on clusters without the role bindings unless
178-
you bind `radar:system` yourself. Change it through this value: a binding
179-
edited or deleted with `kubectl` comes back on the next upgrade.
180-
181-
An upgrade with `--reuse-values` from a release that predates the key leaves it
182-
off, so an existing install never gains Secret read without choosing it. Set
183-
`cloud.systemRbac=true` on those installs.
164+
### Radar Cloud background services (`radar:system`)
165+
166+
Radar Cloud's alerts worker and timeline puller read as `radar:system`.
167+
`cloud.systemRbac` (default `true`) creates the chart's default read grant:
168+
a chart-owned role aggregated to match `view`, the cluster-read and
169+
integration-read add-ons, and cluster-wide `get/list/watch` on Secrets.
170+
Helm release alerts and Secret changes in the timeline need Secret read;
171+
Helm stores releases as Secrets. The grant includes no writes.
172+
173+
### Radar Cloud automatic Diagnose (`radar:ai`)
174+
175+
Manual Diagnose turns read as the person who started them. Radar Cloud forwards
176+
that person's identity and groups; `radar:ai` is not involved. MCP clients also
177+
read with the user's own permissions.
178+
179+
Automatic (alert-triggered) Diagnose runs read as `radar:ai` only, a permanent
180+
read-only background diagnostic reader. Never grant this group write
181+
permissions; a future write-capable feature needs its own group.
182+
`cloud.aiRbac` (default `true`) creates the chart's default grant: a chart-owned
183+
role aggregated to match `view` plus the cluster-read and integration-read
184+
add-ons. With the standard `view` role this grant includes no Kubernetes Secret
185+
permission; it also inherits anything your cluster adds to `view` (ClusterRoles
186+
labelled `rbac.authorization.k8s.io/aggregate-to-view`). It does include pod
187+
logs and ConfigMaps, which may contain sensitive data.
188+
189+
Both values are independent of `cloud.defaultRbac`. They control only the
190+
chart's default grants, not whether features run; Radar Cloud controls whether
191+
Diagnose runs. False or absent creates no default grant and does not remove
192+
customer-created bindings. To narrow the default grants, set the matching value
193+
to false and supply your own read-only bindings for that group. With no grant,
194+
the group has no cluster access; automatic Diagnose has no viewer fallback.
195+
196+
A plain `--reuse-values` upgrade from a chart predating these keys leaves them
197+
absent and creates neither default grant. Set `cloud.aiRbac=true` and/or
198+
`cloud.systemRbac=true` explicitly to create them. Changes made directly to
199+
chart-managed bindings are restored on the next Helm upgrade.
200+
201+
Creating or updating these aggregated roles requires cluster-admin-equivalent
202+
authority or `escalate` on ClusterRoles, in addition to normal chart installation
203+
permissions. Restricted installers can set `cloud.aiRbac=false` and
204+
`cloud.systemRbac=false` and supply their own read-only group bindings.
184205

185206
### Connecting to Argo CD (GitOps deep diff)
186207

‎deploy/helm/radar/templates/_helpers.tpl‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Create the name of the service account to use
7373
{{- end -}}
7474

7575
{{/*
76-
Whether the read-only binding for Radar Cloud's own background identities
76+
Whether the default read grant for Radar Cloud's background services
7777
(radar:system) renders. It includes cluster-wide Secret read, so an absent
7878
value means OFF: a `--reuse-values` upgrade from a release that predates the
7979
key renders with the previous release's tree and never gains Secret read
@@ -87,3 +87,17 @@ true
8787
false
8888
{{- end -}}
8989
{{- end -}}
90+
91+
{{/*
92+
Whether the default read grant for automatic Diagnose (radar:ai) renders.
93+
An absent value means OFF, as with radar.cloudSystemRbac: a
94+
`--reuse-values` upgrade from a release that predates the key never gains a
95+
new reader without someone choosing it.
96+
*/}}
97+
{{- define "radar.cloudAiRbac" -}}
98+
{{- if and .Values.cloud.enabled (eq (toString .Values.cloud.aiRbac) "true") -}}
99+
true
100+
{{- else -}}
101+
false
102+
{{- end -}}
103+
{{- end -}}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{{- /*
2+
Read-only access for automatic (alert-triggered) Radar Cloud Diagnose runs.
3+
4+
Automatic runs read as radar:ai only. Manual turns read as the person who
5+
started them, with that person's identity and groups forwarded by Radar Cloud;
6+
radar:ai is not involved. Keep radar:ai permanently read-only; any future
7+
write-capable feature needs its own group.
8+
9+
The default grant is view plus the cluster-read and integration-read add-ons.
10+
With the standard view role it grants no Kubernetes Secret permission; it
11+
also inherits anything a cluster adds to view (roles labelled
12+
aggregate-to-view). view includes pod logs and ConfigMaps, which may contain
13+
sensitive data.
14+
15+
cloud.aiRbac controls only this chart's default grant, independently of
16+
cloud.defaultRbac. False or absent creates none and leaves customer bindings
17+
untouched. Radar Cloud controls whether Diagnose runs.
18+
19+
A chart-owned role lets its rules change later without replacing the binding,
20+
whose roleRef is immutable. Aggregation keeps it equal to view with no curation.
21+
*/}}
22+
{{- if and (include "radar.cloudAiRbac" . | eq "true") .Values.rbac.create -}}
23+
apiVersion: rbac.authorization.k8s.io/v1
24+
kind: ClusterRole
25+
metadata:
26+
name: {{ include "radar.fullname" . }}-cloud-ai-read
27+
labels:
28+
{{- include "radar.labels" . | nindent 4 }}
29+
aggregationRule:
30+
clusterRoleSelectors:
31+
- matchLabels:
32+
rbac.authorization.k8s.io/aggregate-to-view: "true"
33+
# Omit rules because the aggregation controller owns them.
34+
---
35+
apiVersion: rbac.authorization.k8s.io/v1
36+
kind: ClusterRoleBinding
37+
metadata:
38+
name: {{ include "radar.fullname" . }}-cloud-ai-read
39+
labels:
40+
{{- include "radar.labels" . | nindent 4 }}
41+
roleRef:
42+
apiGroup: rbac.authorization.k8s.io
43+
kind: ClusterRole
44+
name: {{ include "radar.fullname" . }}-cloud-ai-read
45+
subjects:
46+
- kind: Group
47+
name: radar:ai
48+
apiGroup: rbac.authorization.k8s.io
49+
{{- end -}}

‎deploy/helm/radar/templates/cloud-rbac-cluster-read.yaml‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ from being truthy.
4949
{{- $rbacDefaults := .Values.cloud.defaultRbac }}
5050
{{- $anyTier := and $rbacDefaults.create (or (and $rbacDefaults.viewer (get $csrOn "viewer")) (and $rbacDefaults.member (get $csrOn "member")) (and $rbacDefaults.owner (get $csrOn "owner"))) }}
5151
{{- $systemOn := include "radar.cloudSystemRbac" . | eq "true" }}
52-
{{- if and .Values.cloud.enabled .Values.rbac.create (or $anyTier $systemOn) -}}
52+
{{- $aiOn := include "radar.cloudAiRbac" . | eq "true" }}
53+
{{- if and .Values.cloud.enabled .Values.rbac.create (or $anyTier $systemOn $aiOn) -}}
5354
apiVersion: rbac.authorization.k8s.io/v1
5455
kind: ClusterRole
5556
metadata:
@@ -191,4 +192,21 @@ subjects:
191192
name: radar:system
192193
apiGroup: rbac.authorization.k8s.io
193194
{{- end }}
195+
{{- if $aiOn }}
196+
---
197+
apiVersion: rbac.authorization.k8s.io/v1
198+
kind: ClusterRoleBinding
199+
metadata:
200+
name: {{ $fullname }}-cloud-ai-cluster-read
201+
labels:
202+
{{- $labels | nindent 4 }}
203+
roleRef:
204+
apiGroup: rbac.authorization.k8s.io
205+
kind: ClusterRole
206+
name: {{ $fullname }}-cluster-read
207+
subjects:
208+
- kind: Group
209+
name: radar:ai
210+
apiGroup: rbac.authorization.k8s.io
211+
{{- end }}
194212
{{- end -}}

‎deploy/helm/radar/templates/cloud-rbac-integration-read.yaml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
{{- $integration := $rbac.integrationRead | default dict -}}
33
{{- $cluster := $rbac.clusterScopedRead | default dict -}}
44
{{- $systemOn := include "radar.cloudSystemRbac" . | eq "true" -}}
5-
{{- if and .Values.cloud.enabled .Values.rbac.create (or $rbac.create $systemOn) -}}
5+
{{- $aiOn := include "radar.cloudAiRbac" . | eq "true" -}}
6+
{{- if and .Values.cloud.enabled .Values.rbac.create (or $rbac.create $systemOn $aiOn) -}}
67
{{- $root := . -}}
78
{{- $baseline := .Files.Get "files/integration-read-baseline.yaml" | fromYaml -}}
89
{{- range $scope := list "Namespaced" "Cluster" -}}
@@ -16,6 +17,9 @@
1617
{{- if $systemOn -}}
1718
{{- $tiers = append $tiers "system" -}}
1819
{{- end -}}
20+
{{- if $aiOn -}}
21+
{{- $tiers = append $tiers "ai" -}}
22+
{{- end -}}
1923
{{- $rules := list -}}
2024
{{- range $entry := $baseline.entries -}}
2125
{{- if and (eq $entry.decision "grant") (eq $entry.scope $scope) (or $root.Values.rbac.crdGroups.all (index $root.Values.rbac.crdGroups $entry.collection)) -}}
@@ -49,7 +53,7 @@ subjects:
4953
- kind: Group
5054
name: radar:{{ $tier }}
5155
apiGroup: rbac.authorization.k8s.io
52-
{{- if ne $tier "system" }}
56+
{{- if not (has $tier (list "system" "ai")) }}
5357
- kind: Group
5458
name: cloud:{{ $tier }}
5559
apiGroup: rbac.authorization.k8s.io

‎deploy/helm/radar/templates/cloud-rbac-system.yaml‎

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,41 @@
11
{{- /*
2-
Read-only access for Radar Cloud's own background identities.
2+
Read-only access for Radar Cloud's alerts worker and timeline puller.
33

4-
The hub's background identities have no user or IdP groups; they assert the
5-
radar:system group. That is the alerts worker and the timeline puller (AI
6-
diagnose reads as radar:viewer). Without a binding of their own they would ride
7-
on radar:owner, which disappears when an org turns the role bindings off
8-
(cloud.defaultRbac.create=false) to let IdP groups decide cluster access —
9-
silently stopping alerts and the hub timeline.
4+
These background services read as radar:system. The default grant is view
5+
plus the cluster-read and integration-read add-ons and cluster-wide Secret
6+
read. Helm release alerts and Secret changes in the timeline need Secret read;
7+
Helm stores releases as Secrets. No writes.
108

11-
The grant is the viewer set (view here, plus the cluster-read and
12-
integration-read add-ons, which bind radar:system in their own templates) and
13-
Secret read. Secret read is what Helm release alerts and Secret changes in the
14-
hub timeline need: Radar lists Helm releases as the caller, and Helm stores
15-
each release as a Secret. Kubernetes RBAC cannot narrow that to Helm's
16-
Secrets, so it is all Secrets, read-only. No writes. All or nothing:
17-
cloud.systemRbac=false removes the whole grant.
9+
cloud.systemRbac controls only this chart's default grant, independently of
10+
cloud.defaultRbac. False or absent creates none and leaves customer bindings
11+
untouched. It is not a feature switch.
12+
13+
A chart-owned role lets its rules change later without replacing the binding,
14+
whose roleRef is immutable. Aggregation keeps it equal to view with no curation.
1815
*/}}
1916
{{- if and (include "radar.cloudSystemRbac" . | eq "true") .Values.rbac.create -}}
2017
apiVersion: rbac.authorization.k8s.io/v1
18+
kind: ClusterRole
19+
metadata:
20+
name: {{ include "radar.fullname" . }}-cloud-system-read
21+
labels:
22+
{{- include "radar.labels" . | nindent 4 }}
23+
aggregationRule:
24+
clusterRoleSelectors:
25+
- matchLabels:
26+
rbac.authorization.k8s.io/aggregate-to-view: "true"
27+
# Omit rules because the aggregation controller owns them.
28+
---
29+
apiVersion: rbac.authorization.k8s.io/v1
2130
kind: ClusterRoleBinding
2231
metadata:
23-
name: {{ include "radar.fullname" . }}-cloud-system-view
32+
name: {{ include "radar.fullname" . }}-cloud-system-read
2433
labels:
2534
{{- include "radar.labels" . | nindent 4 }}
2635
roleRef:
2736
apiGroup: rbac.authorization.k8s.io
2837
kind: ClusterRole
29-
name: view
38+
name: {{ include "radar.fullname" . }}-cloud-system-read
3039
subjects:
3140
- kind: Group
3241
name: radar:system

0 commit comments

Comments
 (0)