Deploy Radar to your Kubernetes cluster for shared team access.
Note: This guide covers deploying Radar as a pod in your cluster. If you're running Radar locally but need to understand cluster connection behavior (e.g., using
KUBECONFIGto override in-cluster detection), see the Configuration Guide.
helm repo add skyhook https://skyhook-io.github.io/helm-charts
helm repo update skyhook
helm upgrade --install radar skyhook/radar -n radar --create-namespaceAccess via port-forward:
kubectl port-forward svc/radar 9280:9280 -n radar
open http://localhost:9280# values.yaml
ingress:
enabled: true
className: nginx
hosts:
- host: radar.your-domain.com
paths:
- path: /
pathType: Prefixhelm upgrade --install radar skyhook/radar \
-n radar -f values.yamlIf your ingress forwards /radar/... to the Radar service as /radar/..., set basePath to the same prefix:
# values.yaml
basePath: /radar
ingress:
enabled: true
className: nginx
hosts:
- host: tools.your-domain.com
paths:
- path: /radar
pathType: PrefixThen open https://tools.your-domain.com/radar/. Do not set basePath when your ingress already rewrites /radar to /.
Path segments accept letters, digits, -, _, . and ~. Radar serves the app
only under the prefix — requests to unprefixed paths get a 404, so update any
external health checks or scrapers that hit /api/health or /metrics directly
(the chart's own probes follow basePath automatically). basePath is not
supported together with Radar Cloud (--cloud-url), which owns the URL path
itself.
Every URL you hand to an external system must include the prefix. With OIDC that means the values you register with your identity provider:
basePath: /radar
auth:
oidc:
redirectURL: https://tools.your-domain.com/radar/auth/callback # not /auth/callback
postLogoutRedirectURL: https://tools.your-domain.com/radar/ # not /
# With backchannelLogout, the URI registered at the IdP is likewise
# https://tools.your-domain.com/radar/auth/backchannel-logoutRadar's callback route lives at {basePath}/auth/callback, so a redirect URL
without the prefix sends the IdP to a path the ingress doesn't route to Radar and
login ends in a 404.
Give each instance its own hostname. Two Radars behind subpaths on one
hostname (/radar-a, /radar-b) are the same browser origin, so they share
browser state: the session cookie is set at Path=/, and localStorage — which
holds the theme, log-viewer preferences and similar per-instance settings — is
scoped per origin with no path-scoped equivalent in the platform at all. Logging
into one can end the other's session, and preferences set in one show up in the
other. A hostname each keeps them properly separate.
Separately from that: a Radar per cluster gives you a view per cluster. Each watches only its own cluster and carries its own upgrades, ingress and auth config, with no cross-cluster search or combined issue list across them. If you want several clusters in one view, that is what Radar Cloud is for, and its agent dials out so there is no per-cluster ingress to wire up.
-
Create the auth secret:
# Install htpasswd if needed: brew install httpd (macOS) or apt install apache2-utils (Linux) # Generate credentials (replace 'admin' and 'your-password') htpasswd -nb admin 'your-password' > auth # Create the secret kubectl create secret generic radar-basic-auth \ --from-file=auth \ -n radar rm auth # Clean up local file
-
Configure ingress:
# values.yaml ingress: enabled: true className: nginx annotations: nginx.ingress.kubernetes.io/auth-type: basic nginx.ingress.kubernetes.io/auth-secret: radar-basic-auth nginx.ingress.kubernetes.io/auth-realm: "Radar" hosts: - host: radar.your-domain.com paths: - path: / pathType: Prefix
-
Deploy:
helm upgrade --install radar skyhook/radar \ -n radar -f values.yaml
Requires cert-manager installed in your cluster.
# values.yaml
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: radar.your-domain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: radar-tls
hosts:
- radar.your-domain.com-
Get your ingress IP:
kubectl get ingress -n radar
-
Create a DNS A record pointing your domain to the ingress IP.
Multi-cluster naming convention:
radar.<cluster-name>.<domain>
Example: radar.prod-us-east1.example.com
Radar uses its ServiceAccount to access the Kubernetes API. The Helm chart creates a ClusterRole with read-only access to common resources by default:
- Pods, Services, ConfigMaps, Events, Namespaces, Nodes, ServiceAccounts, Endpoints
- Deployments, DaemonSets, StatefulSets, ReplicaSets
- Ingresses, NetworkPolicies, Jobs, CronJobs, HPAs, PVCs
- Pod logs (enabled by default)
Some features require additional permissions. Most are disabled by default for security:
| Feature | Value | Default | Description |
|---|---|---|---|
| Secrets | rbac.secrets: true |
false |
Show secrets in resource list |
| Terminal | rbac.podExec: true |
false |
Shell access to pods |
| Port Forward | rbac.portForward: true |
false |
Port forwarding to pods/services |
| Logs | rbac.podLogs: true |
true |
View pod logs |
| Helm Write | rbac.helm: true |
false |
Install/upgrade/rollback/uninstall Helm releases (grants broad write access; auto-enables secrets). When auth or cloud is on, also emits a split helm add-on: radar-helm (CRDs/storage/PDBs/namespaces, bound to owner+member) and radar-helm-admin (RBAC/webhooks/APIServices, owner-only) — see authentication.md |
| RBAC view | rbac.viewRBAC: true |
false |
Show ClusterRoles, ClusterRoleBindings, Roles, RoleBindings in the resource browser. Off by default: cache-served reads bypass per-user RBAC, so granting this exposes the cluster's authorization graph to every authenticated Radar user |
| Traffic TLS | rbac.traffic: true |
true |
Read Hubble relay TLS certs for Cilium traffic observation |
Node management (cordon, uncordon, drain) is available via the MCP server and API. These operations require
patchon nodes,liston pods, andcreateonpods/eviction, which are not included in the default ClusterRole. Add them viarbac.additionalRulesor use per-user authentication so each user's own RBAC governs node operations.
Enable features as needed:
# values.yaml
rbac:
secrets: false # Keep disabled unless needed
podExec: true # Enable terminal feature
podLogs: true # Enable log viewer (default)
portForward: true # Enable port forwarding
helm: false # Enable Helm write operations (broad permissions)The terminal's Debug action launches a throwaway container (ephemeral container on a pod, or a privileged pod on a node) using busybox:latest by default. If the built-in restricted Pod Security Standard rejects the default pod debug container, Radar retries with a restricted-compatible Linux security context using the target/pod non-root UID, or UID 65532 by default, so custom images used in restricted namespaces must work as a non-root user. In air-gapped or private-registry clusters where the default image can't be pulled, point it at a reachable mirror:
# values.yaml
debug:
image: my-registry.internal/busybox:1.36Radar doesn't attach image-pull secrets to debug containers or pods — ephemeral containers inherit the target pod's, and node debug pods rely on the default namespace's ServiceAccount / node registry config — so the image must be pullable without Radar supplying credentials.
Radar reads CRDs from many popular tools. Each CRD group can be toggled individually:
rbac:
crdGroups:
all: false # Wildcard — grant read access to ALL API groups
# Individual groups (all default to true):
argo: true # argoproj.io
calico: true # projectcalico.org, crd.projectcalico.org
certManager: true # cert-manager.io
flux: true # *.toolkit.fluxcd.io
istio: true # networking.istio.io, security.istio.io
karpenter: true # karpenter.sh, karpenter.k8s.aws, karpenter.azure.com, karpenter.k8s.gcp
keda: true # keda.sh
knative: true # serving, eventing, sources, messaging, flows, networking.internal (.knative.dev)
prometheus: true # monitoring.coreos.com
traefik: true # traefik.io
velero: true # velero.io
# ... and 25+ more (see values.yaml for full list)
additionalCrdGroups: [] # Add custom API groups
additionalRules: [] # Arbitrary extra ClusterRole rulesCalico access is read-only (get, list, and watch) for both API groups and is enabled by default. Set rbac.crdGroups.calico=false to disable it. If chart-managed Calico access is disabled, grant only the resources you need through rbac.additionalRules; both groups may be required while a cluster exposes modern and legacy Calico APIs.
You see what you have access to — Radar doesn't require cluster-admin. Whatever your ServiceAccount (or the impersonated user, when auth is enabled) can list, Radar shows. Resource types you can't list show an actionable denied-state instead of a misleading "0 / None found": for a core cluster-scoped kind (Node, PV, StorageClass, and the like) your identity can't read, Radar shows a copyable ClusterRole + ClusterRoleBinding request to hand to a cluster admin (reason rbac_denied), and distinguishes that from a kind Radar's own ServiceAccount can't read, where a user-level grant wouldn't help (reason unavailable, no snippet). The "Your access on this cluster" dialog lists the core cluster-scoped kinds being hidden alongside your effective rules. Namespaces you can't access don't appear.
A namespace-scoped ServiceAccount (RoleBinding without a ClusterRole) is fully supported — Radar detects this at startup and works within the permitted namespace.
RBAC granularity (auth enabled):
- Namespaced resources (Pods, Deployments, Services, …) are filtered by namespace: read access is granted in any namespace where the user has list-pods or list-deployments. Secrets are gated independently — a user sees Secret objects (including Helm release storage) only in namespaces where their own RBAC grants
list secrets, even when they have broader access in that namespace. RBAC objects and webhook configurations are likewise re-checked per user. Other cached resource types are visible within a namespace once the user has workload-read access there. - Cluster-scoped resources (Nodes, PVs, StorageClasses, ClusterRoles, cluster-scoped CRDs, …) are gated per-kind via SubjectAccessReview. Cluster-wide pod visibility does NOT imply Node visibility — every cluster-scoped read goes through its own RBAC check, with results cached per user.
The same RBAC boundary applies to MCP — read tools intersect with each user's allowed namespaces, write tools impersonate the user against the apiserver, and cluster-scoped reads run the same per-kind SAR. The pod ServiceAccount's permissions are the upper bound for both REST and MCP; per-user RBAC narrows that to what each user can see.
Example: Namespace-scoped deployment
# Custom Role granting access to a single namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: radar-viewer
namespace: my-team
rules:
- apiGroups: ["", "apps", "batch", "networking.k8s.io"]
resources: ["pods", "services", "deployments", "daemonsets", "statefulsets",
"replicasets", "jobs", "cronjobs", "configmaps", "events",
"ingresses", "persistentvolumeclaims", "resourcequotas"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: radar-viewer
namespace: my-team
subjects:
- kind: ServiceAccount
name: radar
namespace: radar
roleRef:
kind: Role
name: radar-viewer
apiGroup: rbac.authorization.k8s.ioSet rbac.create: false in the Helm values and apply the custom Role/RoleBinding above. Radar will detect the namespace-scoped permissions and work within my-team only.
For shared team access, enable authentication so each user gets per-user permissions via Kubernetes RBAC. See the Authentication & Authorization Guide for full setup instructions.
Quick start with proxy auth:
# values.yaml
auth:
mode: proxyThen deploy an auth proxy (e.g., oauth2-proxy) in front of Radar. Users authenticate through the proxy, and Radar uses K8s impersonation so each user's actions are governed by their own K8s RBAC bindings.
Quick start with OIDC:
# values.yaml
auth:
mode: oidc
oidc:
issuerURL: https://accounts.google.com
clientID: your-client-id
clientSecret: your-client-secret
redirectURL: https://radar.example.com/auth/callbackWhen deploying Radar in-cluster:
The Helm chart explicitly sets --listen-address=0.0.0.0 so its ClusterIP
Service can reach the pod. Unlike the native CLI's loopback-only default, this
makes Radar reachable on the pod network; the following controls are therefore
part of the deployment boundary.
-
Authentication: Always enable authentication when exposing via ingress. Use built-in auth (proxy or OIDC mode) or basic auth (shown above) at minimum.
-
RBAC scope: The default ClusterRole grants cluster-wide read access. For namespace-restricted access, set
rbac.create: falseand create a custom Role/RoleBinding. Radar will gracefully adapt to the available permissions. -
Privileged features: Terminal (
podExec) and port forwarding grant significant access. Only enable these in trusted environments or when using per-user authentication. -
Network access: Consider using NetworkPolicies to restrict which pods can reach Radar.
Radar's timeline records every cluster change. Two backends:
memory(default): events live in-process, lost on pod restart. Lowest footprint; pick this if you only need recent activity (last few hours).sqlite: events persist to a PVC across restarts. Multi-day audit trail; pick this for long-running in-cluster deployments where you care about history surviving pod cycles.
Timeline volume depends on cluster size and controller churn. Tune timeline.retention (Go duration; 0 disables age cleanup), timeline.maxSize, and persistence.size together. Keep timeline.maxSize below the PVC size so Radar prunes oldest events before the volume fills.
Cleanup runs hourly + once at startup. Confirm it's keeping up via /api/diagnostics — the timeline.maxStorageBytes, timeline.lastCleanupAt, timeline.lastCleanupDeletedRows, timeline.lastCleanupError, and timeline.storageBytes fields surface the state without requiring kubectl logs.
See Helm Chart README for all available values.
| Parameter | Description | Default |
|---|---|---|
image.repository |
Container image | ghcr.io/skyhook-io/radar |
image.tag |
Image tag | Chart appVersion |
ingress.enabled |
Enable ingress | false |
ingress.className |
Ingress class | "" |
service.port |
Service port | 9280 |
basePath |
URL prefix Radar serves under, e.g. /radar for no-strip-prefix subpath ingress |
"" |
mcp.enabled |
Enable MCP server for AI tools | true |
debug.image |
Image for ephemeral debug containers and node debug pods. In built-in restricted PodSecurity namespaces, pod debug containers may retry as the target/pod non-root UID, or UID 65532 by default; point at a compatible mirror for air-gapped / private-registry clusters. |
"" (busybox:latest) |
listPageSize |
Paginate the initial LIST of high-cardinality kinds (Pods, ReplicaSets) on very large clusters that fail to sync; 0 = off, try 2000. Only used when the apiserver lacks WatchList streaming. |
0 |
timeline.storage |
Event storage (memory/sqlite) | memory |
timeline.dbPath |
SQLite database path | /data/timeline.db |
timeline.historyLimit |
Max events to retain (memory only) | 10000 |
timeline.retention |
SQLite retention (Go duration; 0 disables) |
168h |
timeline.maxSize |
SQLite max DB + WAL size before oldest events are pruned (0 disables) |
800Mi |
traffic.prometheusUrl |
Manual Prometheus/VictoriaMetrics URL | "" (auto-discover) |
traffic.prometheusHeadersFromEnv |
Prometheus headers sourced from environment variables, for secret-backed auth headers | {} |
persistence.enabled |
Enable PVC for SQLite storage | false |
persistence.size |
PVC size | 1Gi |
rbac.podLogs |
Enable log viewer | true |
rbac.podExec |
Enable terminal feature | false |
rbac.portForward |
Enable port forwarding | false |
rbac.secrets |
Show secrets in resource list | false |
rbac.helm |
Enable Helm write operations | false |
rbac.viewRBAC |
Show RBAC objects in resource browser | false |
rbac.traffic |
Read Hubble TLS certs | true |
rbac.crdGroups.all |
Wildcard CRD read access | false |
Response compression: Radar gzip-compresses HTTP responses by default (streaming endpoints like SSE are excluded). The level defaults to 1 (best speed), since on large clusters peak response size coincides with peak CPU. Set the RADAR_COMPRESS_LEVEL environment variable (via the chart's pod env) to 0 to disable, or 2-9 to trade CPU for smaller bodies on bandwidth-bound deployments.
kubectl logs -n radar -l app.kubernetes.io/name=radar
kubectl describe pod -n radar -l app.kubernetes.io/name=radarkubectl get ingress -n radar -o yaml
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginxVerify the secret format:
kubectl get secret radar-basic-auth -n radar -o jsonpath='{.data.auth}' | base64 -d
# Should show: username:$apr1$...helm repo update skyhook
helm upgrade radar skyhook/radar -n radar -f values.yamlhelm uninstall radar -n radar
kubectl delete namespace radar