Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions deploy/helm/radar/templates/cloud-rbac-cluster-read.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{{- /*
Cluster-scoped READ for the Cloud tiers.

The built-in admin/edit/view ClusterRoles the tiers bind to (see cloud-rbac.yaml)
are namespace roles — by design they grant nothing on cluster-scoped resources
(Nodes, PersistentVolumes, StorageClasses, IngressClasses, PriorityClasses). For a
cluster-wide observability tool that means owners and viewers can't see core
infrastructure. This add-on grants get/list/watch on those kinds via EXPLICIT
bindings to the cloud:* groups — deliberately NOT via `aggregate-to-view` labels,
which would widen the shared built-in `view`/`edit` roles cluster-wide for every
other subject bound to them (mirrors the radar-helm add-on pattern).

No Secrets and no RBAC objects here — those stay gated behind the tier roles
(view excludes Secrets; RBAC visibility is rbac.viewRBAC) so this is a pure
infrastructure-read widening.

Scoping — cluster-scoped read is its own axis, per tier:
- cloud.defaultRbac.clusterScopedRead.{viewer,member,owner} (each default true).
Set one false to make that tier namespaced-only — e.g. clusterScopedRead.viewer
=false gives viewers the stock built-in `view` (namespaced read) and nothing
cluster-scoped.
- This is INDEPENDENT of which namespaced ClusterRole the tier binds to. Swapping
in a custom {viewer,member,owner}ClusterRole changes namespaced scope only; it
does not touch cluster-read (the two are separate ClusterRoleBindings). If you
want a custom namespaced role AND no infra-read, set its clusterScopedRead false.
- A disabled tier (cloud.defaultRbac.<tier>=false) gets nothing here either — you
own that tier's RBAC entirely.
- Want a DIFFERENT cluster-read set for a tier (e.g. Nodes but not PVs)? Don't
fork this role: set clusterScopedRead.<tier>=false and bind your own ClusterRole
to the cloud:<tier> group. The cloud:* groups are injected by Cloud regardless,
so your binding composes with the built-in namespaced one.
*/}}
{{- $csr := .Values.cloud.defaultRbac.clusterScopedRead }}
{{- if and .Values.cloud.enabled .Values.cloud.defaultRbac.create .Values.rbac.create (or (index $csr "viewer") (index $csr "member") (index $csr "owner")) -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "radar.fullname" . }}-cluster-read
labels:
{{- include "radar.labels" . | nindent 4 }}
rules:
# Namespaces are intentionally NOT here — the built-in view/edit/admin roles
# already grant namespace read, so this add-on stays scoped to the resources
# the built-ins actually exclude.
- apiGroups: [""]
resources: ["nodes", "persistentvolumes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["scheduling.k8s.io"]
resources: ["priorityclasses"]
verbs: ["get", "list", "watch"]
{{- if .Values.rbac.metrics }}
- apiGroups: ["metrics.k8s.io"]
resources: ["nodes"]
verbs: ["get", "list"]
{{- end }}
{{- $fullname := include "radar.fullname" . }}
{{- $labels := include "radar.labels" . }}
{{- $rbac := .Values.cloud.defaultRbac }}
{{- range $tier := list "viewer" "member" "owner" }}
{{- if and (index $rbac $tier) (index $csr $tier) }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ $fullname }}-cloud-{{ $tier }}-cluster-read
labels:
{{- $labels | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ $fullname }}-cluster-read
subjects:
- kind: Group
name: cloud:{{ $tier }}
apiGroup: rbac.authorization.k8s.io
{{- end }}
{{- end }}
{{- end -}}
49 changes: 49 additions & 0 deletions deploy/helm/radar/templates/cloud-rbac-node-ops.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{{- /*
Node operations for the owner tier — cordon / uncordon / drain from Radar.

These are the exact verbs Radar's node-ops path uses (pkg/k8score/node_ops.go):
- patch nodes → cordon / uncordon (spec.unschedulable strategic merge)
- list pods (all ns) → enumerate pods on the node to drain
- create pods/eviction → evict them (PodDisruptionBudgets are enforced by the
eviction API itself, so no PDB read is needed)

`patch nodes` is what cordon/uncordon needs, but K8s RBAC can't scope a verb to a
single field — so this grant also permits editing any other node field (labels,
taints, etc.). That breadth is inherent to `patch nodes`; it's why this is a
cluster-scoped WRITE, owner-only, and behind its own toggle (cloud.defaultRbac
.nodeOps). Still far short of cluster-admin: no node delete, no RBAC/secret access.
Set nodeOps=false to keep owners read-only on cluster-scoped resources.
*/}}
{{- if and .Values.cloud.enabled .Values.cloud.defaultRbac.create .Values.rbac.create .Values.cloud.defaultRbac.nodeOps .Values.cloud.defaultRbac.owner -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "radar.fullname" . }}-node-ops
labels:
{{- include "radar.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["list"]
- apiGroups: [""]
resources: ["pods/eviction"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "radar.fullname" . }}-cloud-owner-node-ops
labels:
{{- include "radar.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "radar.fullname" . }}-node-ops
subjects:
- kind: Group
name: cloud:owner
apiGroup: rbac.authorization.k8s.io
{{- end -}}
12 changes: 11 additions & 1 deletion deploy/helm/radar/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,17 @@
"viewer": { "type": "boolean" },
"ownerClusterRole": { "type": "string" },
"memberClusterRole": { "type": "string" },
"viewerClusterRole": { "type": "string" }
"viewerClusterRole": { "type": "string" },
"clusterScopedRead": {
"type": "object",
"additionalProperties": false,
"properties": {
"viewer": { "type": "boolean" },
"member": { "type": "boolean" },
"owner": { "type": "boolean" }
}
},
"nodeOps": { "type": "boolean" }
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions deploy/helm/radar/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,30 @@ cloud:
ownerClusterRole: admin
memberClusterRole: edit
viewerClusterRole: view
# Grant cluster-scoped READ (Nodes, PVs, StorageClasses, IngressClasses,
# PriorityClasses) on top of the built-in view/edit/admin roles, which
# exclude cluster-scoped resources. Per tier, each default true — Radar is a
# cluster-wide observability tool, so by default every tier (incl. viewer)
# sees core infrastructure. No Secrets or RBAC objects.
#
# Set a tier false to make it namespaced-only. Common for security-conscious
# orgs: clusterScopedRead.viewer=false → viewers get the stock `view`
# (namespaced read) and cannot enumerate cluster infra.
#
# Independent of *ClusterRole above: a custom namespaced role does NOT disable
# cluster-read (they're separate bindings). Want a tier to read a DIFFERENT
# cluster-scoped set (e.g. Nodes but not PVs)? Set its flag false and bind your
# own ClusterRole to the cloud:<tier> group — the cloud:* groups exist
# regardless of this chart, so your binding composes cleanly.
clusterScopedRead:
viewer: true
member: true
owner: true
# Grant the owner tier node cordon/drain (patch nodes + evict pods).
# Cluster-scoped WRITE, so OFF by default — set true to let owners cordon/
# drain nodes from Radar. Not cluster-admin (no node delete, no RBAC/secret
# access).
nodeOps: false

# MCP (Model Context Protocol) server for AI tools
mcp:
Expand Down
1 change: 1 addition & 0 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ Under cloud-mode (`RADAR_CLOUD_MODE=true`, set automatically by the chart when `

- Forces `--auth-mode=proxy` with pinned `X-Forwarded-User` / `X-Forwarded-Groups` headers — the Cloud tunnel is the trust boundary.
- Ships three default ClusterRoleBindings mapping Cloud's `cloud:owner` / `cloud:member` / `cloud:viewer` groups to the standard K8s `admin` / `edit` / `view` ClusterRoles. Configurable via `cloud.defaultRbac.*` in `values.yaml`.
- Adds a **cluster-read add-on** (`cloud.defaultRbac.clusterScopedRead.{viewer,member,owner}`, each default on) granting `get/list/watch` on the cluster-scoped infra the built-in `view`/`edit`/`admin` roles exclude — Nodes, PersistentVolumes, StorageClasses, IngressClasses, PriorityClasses. No Secrets, no RBAC objects. It's an independent axis per tier: set a tier `false` to make it namespaced-only (e.g. `clusterScopedRead.viewer: false`). Owner node cordon/drain is a separate cluster-scoped *write*, off by default (`cloud.defaultRbac.nodeOps`).
- Hardens the listener (no `/debug/pprof/*`, narrower exempt paths).

<a id="cloud-mode-helm-bindings"></a>
Expand Down
Loading