# Sign-in and access: which path applies Source: https://radarhq.io/docs/cloud/access-overview Before the SSO, members, domains and IdP-groups pages: which of them applies to your situation, and how the pieces combine. Two separate questions decide how someone gets into your Radar Cloud organization, and the pages in this section answer them one feature at a time. Keep them apart and the rest is simple: 1. **How does a person prove who they are?** The Cloud sign-in page, or your identity provider through SSO. 2. **How does that person become a member of your organization?** An invitation, auto-join on a verified email domain, or signing in through your organization's own link. The two questions are decided separately, and most organizations use more than one answer to each. Start here, then follow the link for the piece you need. ## First: hosted or self-managed? Everything below is about **hosted Radar Cloud** (app.radarhq.io). With [Radar Cloud Self-Managed](/docs/cloud/self-hosted), you run the control plane on your infrastructure. It does not use WorkOS: people sign in with **your OIDC provider directly**, plus a permanent break-glass admin account. The first person to sign in becomes owner, later people join with the default role you configure, and owners manage roles from there. See [Self-Managed authentication](/docs/cloud/self-hosted/auth) and stop reading this page. ## How people prove who they are * **The Cloud sign-in page** - the methods offered there (an emailed code, and the providers Radar Cloud enables). Nothing to set up on your side. * **Your identity provider through SSO** - SAML or OIDC, connected self-serve from Settings on the Enterprise and Custom plans. See [SSO](/docs/cloud/sso). Connecting SSO adds a door; it does not close the others. A **Require single sign-on** switch is not available yet, so a member who already has an account can still use the Cloud sign-in page. ## How people become members Three paths. They can all be on at once; only the third involves your identity provider. | Path | What has to be true | Role they get | Set up | | - | - | - | - | | **Invitation** | An owner invited their email, and they accepted the invitation with a matching address before it expired | The role on the invitation (an existing membership keeps its role) | [Members](/docs/cloud/members) | | **Domain auto-join** | The organization has verified the email's domain on the Domains page, that domain's auto-join switch and the organization-wide auto-join switch are both on, and the email is verified (see below) | Default invite role | [Members](/docs/cloud/members#domain-auto-join) | | **Signing in through your organization** | They authenticated in your organization's scope - usually by opening `app.radarhq.io/login/` with SSO configured - and WorkOS returned your organization as the one that admitted them | Default invite role on first sign-in; an existing membership keeps its role | [SSO](/docs/cloud/sso#per-organization-sign-in-link) | The third path is worth stating plainly: whoever WorkOS admits *to your organization* becomes a member - through your IdP's connection, or through any other sign-in method routed in your organization's scope. Radar adds no gate of its own there, and the organization-wide auto-join switch does not affect it (it governs domain-based joining only). Your identity provider's admission policy is the gate, which is exactly what makes this the right path for [contractors and guests](/docs/cloud/sso-external-users). ## What a verified domain does A domain you add under **Settings → Organization → Domains** and verify with one DNS-TXT record (at the domain you are verifying, not its parent) does three separate things: * **Routes SSO sign-ins**, once SSO is configured. A person who types an address at a verified domain on the sign-in page is sent to your IdP. Without a verified domain, your organization's sign-in link routes them instead. * **Makes auto-join possible** for addresses at that domain (with the switches above). * **Is where rewritten guest addresses land**, if you onboard external users through the subdomain alternative - a verified subdomain such as `ext.example.com`. A domain that is added but not yet verified is claimed and does nothing until the record is in place. ## Where "verified email" comes from Radar trusts WorkOS's verification status for the address. An address your IdP asserts through SSO at a domain you verified arrives verified, and so do sign-ins WorkOS can verify on its own (an emailed code you entered, or a provider that vouches for the address). When WorkOS decides an address still needs proving - for example an SSO assertion for an address outside your verified domains - it emails a code and Radar collects it. That is the normal experience for a guest who keeps their employer email; the guest flow's subdomain alternative avoids it, because an address at a verified domain, asserted by your connection, is not challenged. ## On top of any of the above * **Cluster access that follows your directory groups.** SSO plus a groups claim from your IdP, passed through to Kubernetes as `radar:idp:` subjects you bind yourself. Bindings only add permissions: the baseline Cloud role still grants what it grants unless you turn that tier's defaults off. See [IdP groups](/docs/cloud/idp-groups) and [RBAC](/docs/cloud/rbac). * **Guests who keep their employer email.** They sign in through your organization's link with their real address, confirm it with an emailed code, and join with the default role; OIDC or SAML, your choice. If you would rather they never see the code, the documented alternative rewrites guests onto a subdomain you verified and carries the real address as a separate claim; that recipe is written for a **SAML** connection. See [SSO for external users](/docs/cloud/sso-external-users). ## SAML or OIDC? Either: your IdP's choice, for employees and guests alike. Only the subdomain-rewrite alternative for guests is documented for SAML. And note that "OIDC" on a self-managed Hub is a different mechanism entirely - a direct connection with no WorkOS - so the hosted Cloud SSO pages do not apply there. # Alerts Source: https://radarhq.io/docs/cloud/alerts Rule-driven, durable alerting on top of the live Issues stream - get notified when something breaks, and keep a history of what fired after it clears. [Issues](/docs/features/issues) answers *what's broken right now* - a live stream that disappears the moment a problem clears. **Alerts** turn that stream into something durable and routable: you write rules that say *which* issues are worth a notification, Radar Cloud tracks each matched issue across its **open → resolved** lifecycle, and the **Alerts** page keeps the history so a problem still has a home after it's gone from the live view. Three surfaces, three jobs - keep them straight and the model is simple: | Surface | Question it answers | Lifetime | | - | - | - | | **[Issues](/docs/features/issues)** | What's broken *right now*? | Live - recomputed each poll, gone when it clears | | **Alerts** (this page) | What tripped my rules - open now, and what resolved? | Durable - persists across the open → resolved lifecycle | | **[Notifications](/docs/cloud/notifications)** | How am I told, and through which channels? | Per-delivery (inbox row, Slack message, webhook POST) | Alerts is **Cloud-only**. The Issues detection engine is part of the open-source binary; the durable lifecycle, rules, and routing are control-plane features. ## Notification rules A rule is a **filter over the Issues stream plus delivery settings**. Organization owners and members can create rules at **Settings → Organization → Notifications → Issue notification rules**, or straight from the Issues page with **Notify on these** - which pre-fills a rule from whatever filters you've got applied. Only an organization owner can approve or renew organization-wide consent for AI analysis. Managing alert rules does not grant that permission. ### What a rule matches Every rule narrows the stream by any combination of: * **Clusters** - specific clusters, or all of them. * **Namespaces** - with an opt-in for cluster-scoped issues (Nodes, PVs, …), which don't belong to a namespace. * **Severity** - `critical`, `warning`, or both. * **Detection source** - `problem`, `missing_ref`, `scheduling`, `condition` (see the [Issues catalog](/docs/features/issues#what-it-catches)). * **Category / category group** - the symptom axis (e.g. *GitOps render failed*, *Image pull failed*). * **Resource kind** - Deployment, Application, HelmRelease, … * **CEL** - an expression for anything the structured filters don't cover. Values within one dimension are OR'd, dimensions AND together, and CEL ANDs on top. Use **Preview** before saving - it evaluates the draft against your live clusters with the same matching code the worker runs, so what you see is what will alert. ### Delivery and timing | Setting | What it controls | | - | - | | **In-app inbox** | On by default - every rule drops matched alerts into the bell tray for free. | | **Destinations** | Slack channels and webhooks the rule routes to (see [Notifications](/docs/cloud/notifications)). | | **Minimum duration** | Wait this long before alerting, so a blip that self-heals never pages anyone. | | **Resolve grace** | Wait this long after an issue clears before calling the alert resolved (default 2 minutes), so a flapping resource doesn't open/close repeatedly. | | **Re-notify interval** | Optionally re-send while an alert stays open, so a long-running problem doesn't fall silent. Off by default. | | **Notify on resolve** | Send a follow-up when the alert clears (on by default), so "it's fixed" closes the loop. | ## Baseline silence - no alert storm on day one The single most important behavior: **alerts only fire for issues that appear *after* the rule exists.** When you create a rule - or connect a brand-new cluster - everything already broken at that moment is recorded as **baseline** and silently seeded as already-open. None of it notifies. That existing backlog is the Issues view's job; alerts are for *new* problems from here forward. Without this, turning on a rule against a cluster with 40 standing issues would fire 40 notifications at once. Baseline alerts still appear on the Alerts page (marked **baseline**) so the history is complete - they just never sent a notification. ## The alert lifecycle 1. An issue is detected and **matches a rule**. 2. It persists past the rule's **Minimum duration** - the alert **opens** and sends a notification. 3. While it stays open past the **Re-notify interval** (if set), Radar re-sends so it doesn't fall silent. 4. The issue clears and stays clear past the **Resolve grace** - the alert **resolves** and (if **Notify on resolve** is on) sends a follow-up. If the same resource breaks again later, that's a **new generation** of the alert - it reopens and notifies fresh. ## The Alerts page Open **Alerts** in the left rail (`/fleet/alerts`). It reads the durable records, so a resolved alert still has somewhere to live after it's gone from Issues. * **Window** - open and resolving alerts are *always* shown; resolved alerts are bounded to the **last 7 days** so the page stays a recent-history view, not an unbounded archive. * **Status filter** - `All` / `Open` / `Resolved`. * **Scope** - respects the global cluster scope picker, like every fleet view. * **Each row** shows the resource, cluster, namespace, category, and the rule that caught it. Open rows carry a **View live in Issues** link; resolved rows show **how long the alert fired** (`fired for 3m`). Baseline (seeded) rows are de-emphasized. * **Access** - all organization roles, including viewers, can read the Alerts page and history. Owners and members can create, edit, and delete rules; viewers cannot manage rules. ## Moving between surfaces The surfaces are deliberately cross-linked so a notification never dead-ends: * An **"issue opened"** notification deep-links to the **live Issues** queue with that row focused and highlighted - it's still broken, so the live view is where you act. * An **"issue resolved"** notification deep-links to the **Alerts** page with that alert focused - it's gone from Issues, so Alerts is its only home. The link hydrates the specific alert even if it's outside the 7-day window. * If you click an "opened" notification *after* the issue resolved, the Issues page recognizes the gap and **bridges you to Alerts** rather than showing an empty list. ## Suppressing alerts Alerts honor Issues triage. When someone **dismisses** an issue in the Issues queue (an org-wide action, with a reason), its alerts are suppressed too - you won't get re-notified for a problem the team has consciously set aside. Restoring the issue re-arms its alerts. ## See also * [Issues](/docs/features/issues) - the live detection stream alerts are built on. * [Notifications](/docs/cloud/notifications) - channels, the inbox, email preferences, and webhook payloads. * [Audit log](/docs/cloud/audit-log) - the underlying org event taxonomy. # Applications across clusters (fleet view) Source: https://radarhq.io/docs/cloud/applications Fleet Applications groups workloads by the app they belong to and folds each app across the environments and clusters it runs in - one row per app, with version skew and promotion lag at a glance. The fleet **Applications** view (`/fleet/applications`) groups deployable software by the app it belongs to, then folds the same app's instances across clusters and environments into one row. It answers "where does `payments-api` run, and is every environment on the same version?" without opening each cluster. > [!NOTE] > > This is the **fleet** Applications view, which folds each app across clusters. For the single-cluster view in open-source Radar (and per-cluster in Cloud), see [Applications](/docs/features/applications). ## How apps are resolved Radar resolves app rows from real evidence - workload ownership, GitOps Applications, Helm releases, and labels - rather than a single annotation you have to set. Each row carries a **Source** facet so you can tell where the grouping came from: `Argo CD`, `Flux`, `Helm`, `Label`, or `Ungrouped`. The instance is always the addressable thing. App identity is grouping evidence layered on top, never a replacement for the underlying workload. ## Grouping across environments Instances of the same app - the copies running in dev, staging, prod, and across clusters - fold into one row, with a cell per environment. A `koala-backend` running in staging and autopush on one cluster and prod on another collapses to a single row. * An **identity chip** shows how confident Radar is in the grouping - **emerald** when the app identity is declared, **neutral** when it's a heuristic match - with an evidence tooltip explaining the call. * Text search auto-expands a folded row into its hidden instances; instance rows are always one chevron away. * Clicking an environment cell drops you into that specific instance. Environment is resolved in order: an environment you've set explicitly on the cluster, then a namespace-name heuristic (rendered `~staging` and marked *inferred*), then unlabeled. ## Promotion lag and version skew * **Promotion lag** renders between ranked environments whose versions can be ordered - so you can see prod trailing staging at a glance. * A **per-instance skew** chip stays amber when the *same* app runs different versions within one environment across clusters - that's real drift. Different versions across *different* environments (dev ahead of prod) read as healthy promotion state, not an alert. ## Per-app detail Opening an app gives you an application shell - identity, context strip, and env / instance selectors - wrapped around the same runtime tabs you get on any single resource: **Overview, Topology, Timeline, Logs, Metrics, YAML**, served live through the cluster's own Radar. Related resources open in place in a drawer. ## Coverage and offline clusters Like every fleet page, Applications reports per-cluster status in the top bar. Scope to a cluster whose tunnel is down and the view says so plainly - "*cluster* isn't reporting - the tunnel is down" with a **View clusters** action - rather than implying the cluster runs nothing. ## See also * [Fleet views](/docs/cloud/fleet-views) - the other cross-cluster aggregates (Issues, Search, Checks, Packages). * [GitOps](/docs/features/gitops) - the Argo CD / Flux applications that feed app grouping. * [Connecting a cluster](/docs/cloud/connect-cluster) - what makes a cluster report into the fleet. # Audit log Source: https://radarhq.io/docs/cloud/audit-log Organization activity, sign-ins, credential changes, pod-terminal sessions, and MCP tool outcomes. JSON or CSV export. Owners can view the audit log under **Settings → Organization → Activity** or query `GET /api/orgs/{org_id}/audit`. ## What's recorded The log covers Cloud administration, authentication, pod-terminal sessions, and MCP tool outcomes. Representative actions include: | Activity | Actions | | - | - | | Clusters | `cluster.created`, `cluster.updated`, `cluster.deleted`, `cluster.token_rotated` | | Organization settings | `org.updated`, `org.sso.portal_opened` | | Invitations | `org_invitation.created`, `org_invitation.accepted`, `org_invitation.revoked` | | Membership | `org_member.joined`, `org_member.auto_joined`, `org_member.sso_joined`, `org_member.role_changed`, `org_member.removed` | | Personal access tokens | `pat.created`, `pat.revoked`, `pat.revoked_by_owner` | | Billing | `billing.checkout_started`, `billing.subscription_changed` | | Sign-in / sign-out | `auth.signin`, `auth.signout` | | Pod terminals | `pod.exec_started`, `pod.exec_ended` | | MCP grants | `mcp.grant.created`, `mcp.grant.revoked` | | MCP tools | `mcp.tool.call`, `mcp.tool.rejected`, `mcp.tool.forbidden`, `mcp.tool.error` | Self-managed OIDC and emergency-admin sign-ins use `auth.oidc_signin` and `auth.break_glass_signin`. MCP membership provisioned on first use emits `member.created_jit_oauth`. MCP rows identify the actor, tool, cluster, and outcome. Successful high-volume calls may be grouped; successful calls do not store tool arguments or response bodies. Pod-terminal events record the cluster, namespace, pod, optional container, and session duration. They do not record terminal input, output, or shell transcripts. Cluster connection and disconnection events appear in [Notifications](/docs/cloud/notifications), rather than the audit log. ## Event shape ```json theme={null} { "id": 1042, "org_id": "org_01HF...", "actor_user_id": "user_01HF...", "actor_email": "alice@acme.com", "actor_kind": "user", "action": "cluster.token_rotated", "target_type": "cluster", "target_id": "cl_01HF...", "metadata": { "name": "prod-us-east-1", "revoke_now": false }, "ip": "203.0.113.42", "user_agent": "Mozilla/5.0 ...", "created_at": "2026-04-22T12:34:56Z" } ``` Metadata varies by action. Invitation and SSO joins include the granted role; billing changes include the event type, plan, and status. PAT-authenticated actions use `actor_kind: "pat"`. They identify the user, not which token made the request. PAT creation and revocation events include the token's name in metadata. ## Filtering The UI filters by action, target type, and actor **user ID**, with a shortcut for MCP activity. Older events load in pages of 50. API callers can also set `since` and `until` as RFC3339 timestamps. Filters cannot retrieve events outside your retention window. ```http theme={null} GET /api/orgs/{org_id}/audit GET /api/orgs/{org_id}/audit?action=cluster.deleted GET /api/orgs/{org_id}/audit?actor=user_01HF... GET /api/orgs/{org_id}/audit?since=2026-04-01T00:00:00Z&until=2026-04-15T00:00:00Z ``` ## Export Use **Export CSV** in the UI, or add `format=csv` to the API request. Export honors the current filters. ```bash theme={null} curl -H "Authorization: Bearer rhp_..." \ "https://api.radarhq.io/api/orgs//audit?format=csv&since=2026-04-01T00:00:00Z" \ > audit.csv ``` PAT access requires the owner role, just like the UI. ## Retention The audit log is included on paid plans. Hosted Cloud exposes these history windows: | Plan | Visible history | | - | - | | Free | Not included | | Team | 30 days | | Enterprise | 1 year | The window limits what you can read, not when your organization's rows are deleted. A sweeper runs at startup and daily to delete hosted audit rows older than 365 days. [Self-managed Hubs](/docs/cloud/self-hosted/configuration#audit-retention) use their configured retention window for both visibility and deletion. ## What's NOT in the audit log The Cloud log is not a complete Kubernetes audit trail. Direct `kubectl` calls and ordinary Kubernetes reads and writes through Radar do not produce one Cloud event per API call. Use your Kubernetes API server's audit log for that record. ## See also * [Organizations & roles](/docs/cloud/organizations) - audit access is owner-only. * [Personal access tokens](/docs/cloud/personal-access-tokens) - credential lifecycle. * [Notifications](/docs/cloud/notifications) - subscribe to activity events. # Billing & plans Source: https://radarhq.io/docs/cloud/billing Manage your subscription, payment method and invoices. Billing is managed by organization owners under **Settings → Organization → Billing**. ## Plans What each plan costs and includes is on the [Pricing page](https://radarhq.io/pricing) and in the plan grid on your Billing page. For a negotiated agreement, contact `sales@skyhook.io` or use **Talk to sales** on the Billing page. ## Change your plan **Upgrade plan** on the Billing page takes you through checkout. To move between plans afterwards, pick one from the grid. The page tells you when the change takes effect and lets you cancel a change that has not landed yet. ## Payment method, invoices, cancellation **Manage billing** on the Billing page opens the Stripe customer portal: * Update the payment method. * Download invoices. * Change the billing email. Stripe sends invoices there. * Cancel the subscription. ## If a payment fails Open **Manage billing** and update the payment method. Nothing is cut off while a payment is being retried. To be told when a payment fails, subscribe to billing events under [Notifications](/docs/cloud/notifications). ## Clusters on a self-managed control plane Clusters on a [self-managed control plane](/docs/cloud/self-hosted) are billed through the same organization, and appear in the same count on the Billing page. ## See also * [Organizations & roles](/docs/cloud/organizations) - billing is owner-only. * [Audit log](/docs/cloud/audit-log) - billing changes are logged. # Cluster token rotation Source: https://radarhq.io/docs/cloud/cluster-token Rotate a cluster's credential without taking it offline, revoke a leaked token immediately, and recover an installation that can no longer connect. Each connected cluster authenticates to the Radar control plane (hosted Radar Cloud, or Radar Cloud Self-Managed on your infrastructure) with a bearer token (`rhc_`). The control plane stores only a SHA-256 hash. Hub-generated installations read the token from a Kubernetes Secret and present it when opening the WebSocket tunnel. The token is a long-lived cluster credential, even though its raw value is revealed only once. ## When to rotate * **Routine** - a rotation policy (every 90 days, say), an operator leaving on good terms, or a token you simply lost. Rotate with the default settings: the cluster stays online while you apply the replacement. * **Leak** - a `values.yaml`, log, or screenshot with the token in it went somewhere it shouldn't, or an operator left on bad terms. Rotate with **Revoke the current token immediately** on. ## What rotation does Only an organization owner can rotate a cluster token, from **Clusters > cluster row > Rotate token** (the row says **Reinstall** while the cluster is disconnected and **Install** before it has ever connected; all three open the same dialog). Rotation issues a replacement token, reveals it once, and starts a grace window for the current one: 1. The current token keeps working until Radar connects with the replacement, or for **24 hours**, whichever comes first. 2. Radar's tunnel stays up. Nothing goes offline when you click **Generate**. 3. The moment Radar connects with the replacement, the previous token stops authenticating. 4. If the replacement is never applied, the previous token expires at the end of the window. Radar can no longer connect with it; apply the replacement to bring the cluster back. While the window is open, the cluster's row shows **New token not applied yet**; hovering it shows when the previous token expires. Rotating again before the replacement has connected supersedes that replacement: the newest token is the one to apply, and the token Radar is currently running on keeps its window unchanged. The one thing a second rotation does retire is a replacement you had already applied but that had not connected yet (a restart still in progress); if that happens, apply the newest artifact. Rotation emits a `cluster.token_rotated` event in the [audit log](/docs/cloud/audit-log). Its metadata records whether the previous token was revoked immediately. ### Revoke a leaked token immediately If the current token may have leaked, turn on **Revoke the current token immediately** before you click **Generate**. There is then no grace window: * the previous token stops authenticating with the click, so no new connection can use it; * Radar's existing tunnel is closed as soon as the control plane sees it is still on the revoked token, and its reconnect attempts are rejected until the replacement is applied; * the cluster is offline until then. That interruption is the point of an emergency rotation. Leave the switch off for routine rotation (a compliance schedule, an operator leaving on good terms): the cluster stays connected and you apply the replacement at your own pace within the window. ## Why the pod must restart The Hub's generated install artifacts configure the Radar Helm chart to expose the Secret value to the Radar process as an environment variable. Updating the Secret does not change the environment of a running container, and the Radar process reads the token once at startup. Every generated recovery artifact therefore ends by restarting the Radar workload after the live Secret holds the replacement. That restart is what makes Radar connect with the new token and retire the old one. A GitOps reconciliation that updates only the Secret is not enough. ## Apply the replacement The dialog generates the steps for Helm, Argo CD, or Flux, plus the bare token for teams that manage the Secret themselves. After you run them, the dialog reports **New token accepted** as soon as Radar connects with the replacement. On a routine rotation the row stays **Connected** throughout; the row hint clears at the same moment. Generated commands target the namespace the agent last reported running in, and assume Helm release `radar` unless you opened the dialog from a link that named the release. If your release has another name, change it in every generated command (the Secret write, the upgrade, and the restart). `helm list -A` shows the real names. ### Helm The generated Helm command chains three stages with `&&`, so a failure in one stops the rest and the pod is never restarted into a state without a token: 1. writes the replacement into the `radar-cloud-config` Secret; 2. runs `helm upgrade` on the release with `--reset-then-reuse-values`, pinned with `--version` to the chart version the agent currently reports, and with `cloud.existingSecretKey=token` so a custom key from an earlier release is not carried forward; and 3. restarts the Radar deployment. The pin means "Rotate token" never upgrades Radar as a side effect. When the agent reports a version that is not a published chart (a development build), or the cluster has never connected, the command installs the newest stable chart instead and says so. The upgrade stage is what migrates an installation that still sets `cloud.token` inline to the Secret-backed layout. If your release reads a Secret with another name or key, use the [bare token](#install-a-raw-replacement-token) instead of the generated command. ### Argo CD or Flux GitOps recovery has two steps, and the dialog shows both: 1. Update the token in the source that manages `radar-cloud-config` (the generated `kubectl create secret` line, your ExternalSecret or SealedSecret source, or the values patch for an inline `cloud.token`). 2. After the live Secret holds the new value, restart the Radar deployment: ```bash theme={null} kubectl rollout restart deployment \ -n NAMESPACE \ -l app.kubernetes.io/instance=RELEASE ``` When the release name is assumed rather than known, the generated command selects by chart instead, `-l app.kubernetes.io/name=radar`, which restarts every Radar deployment in that namespace. If you installed the chart with `nameOverride`, that label differs too: use your release's instance label as above, and check that the deployment actually restarted (`kubectl rollout status`). Wait for the live Secret to change before restarting. A pod restarted while the Secret still holds the old value simply reconnects with the old token, which keeps working inside the grace window, and the dialog stays amber. ## Install a raw replacement token Under the generated commands, **Managing the Secret yourself? Copy just the token** reveals the raw value without prescribing how your cluster is managed. Use this with ExternalSecrets, SealedSecrets, a values file you own, or any other automation. Store it in the `radar-cloud-config` Secret under the key `token`, or in whichever Secret and key your values point `cloud.existingSecret` and `cloud.existingSecretKey` at; the manifest references the Secret by name and never carries the value. If your release sets `cloud.token` directly, update that Helm value instead; changing the pod template makes Helm roll the pod. For a directly managed Kubernetes Secret, write it and then restart the pod: ```bash theme={null} kubectl create secret generic radar-cloud-config \ --namespace NAMESPACE \ --from-literal=token=rhc_NEW_VALUE \ --dry-run=client \ -o yaml | kubectl apply -f - && \ kubectl rollout restart deployment \ --namespace NAMESPACE \ --selector app.kubernetes.io/instance=RELEASE ``` Replace `NAMESPACE` and `RELEASE` with the installation's actual namespace and Helm release. Do not commit the raw token to Git. An inline `cloud.token` also persists in Helm release history, which is stored in a Secret in the release namespace; prefer `cloud.existingSecret`. Commands that include the token can also enter shell history and briefly expose it in the process list, so run them from an appropriately protected administrator environment. Anyone who can read this Secret can authenticate as the cluster agent. Treat the value like any other production credential. ## If the token reveal is interrupted The replacement is shown once. Closing the dialog before copying anything asks you to confirm; if you close anyway, rotate again to mint another replacement. On a routine rotation the cluster stays connected on its current token meanwhile, and that token keeps its original window, so nothing is lost but the unused value. This also applies to a partially completed first install: an earlier generated command stops working once a newer one has connected (or its window lapses), so use only the latest artifact. ## If Radar doesn't reconnect The dialog stays amber until Radar connects with the replacement. If it has been more than a few minutes, the agent's own log says why in one line: ```bash theme={null} kubectl -n NAMESPACE logs -l app.kubernetes.io/name=radar --tail=50 ``` * No new `[cloud] connected` line: the restart didn't happen, or the Secret still held the old value when it did. Re-run the restart after the live Secret has changed. * `401`: the pod is presenting a token that no longer works (revoked, or its window lapsed). Apply the replacement and restart. * A TLS or DNS error: the pod can't reach the control plane at all; see [Connecting a cluster](/docs/cloud/connect-cluster). The replacement token does not expire while you troubleshoot. ## Deleting the Hub record is different from uninstalling Radar Deleting a cluster from Radar Hub removes its control-plane record and credential. It does not uninstall the in-cluster Helm release. Likewise, uninstalling Radar does not delete the Hub record. Deleting the record is not a way to revoke a token: it also discards the cluster's history and settings. Use **Revoke the current token immediately** for that. ## Hosted and self-managed control planes The rotation mechanics are the same for hosted Radar Cloud and your self-managed control plane. Only the Hub URL, and `cloud.insecureSkipVerify` when your Hub serves a self-signed certificate, differ in the installation configuration. ## See also * [Connecting a cluster](/docs/cloud/connect-cluster) - verify the tunnel and diagnose connection failures * [Audit log](/docs/cloud/audit-log) * [Cloud RBAC](/docs/cloud/rbac) # Connecting a cluster Source: https://radarhq.io/docs/cloud/connect-cluster Install Radar in your Kubernetes cluster, what each Helm value does, RBAC scope, and how to verify the connection. Connecting a cluster to Radar Cloud installs the **same** chart Radar OSS uses - just with `cloud.enabled=true` and a cluster credential. The Radar pod dials `api.radarhq.io/agent` over an outbound WebSocket; you don't need any inbound rules on your cluster. ## Three ways to start * **From inside Radar.** The **Radar Cloud** button in Radar's top bar opens a dialog. On a local Radar with no authentication configured, the dialog runs the whole connection itself: it shows a plan of what will be installed, sends you to the browser to approve, then installs or adopts Radar in the cluster. Nothing talks to Radar Cloud until you have seen the plan. On any other setup - an in-cluster Radar, a local Radar with authentication on, a GitOps-managed install - the dialog explains what it found and links you to the wizard below, deep-linked at your namespace, release, or Argo CD / Flux owner where it could identify one. * **From the terminal.** `radar cloud install` does the same device flow from the command line: browser approval, credential minted for you, Radar installed or adopted. Use `--hub-url` for a [Radar Cloud Self-Managed](/docs/cloud/self-hosted/install) control plane. * **From the dashboard.** Generate a Helm command in `app.radarhq.io` and run it yourself - the rest of this page. ## Get the install command In `app.radarhq.io`, click **Connect a cluster**, name it (the underlying ID is opaque `cl_...`), then **Generate install command**. The page polls for connection while you run it. The command looks like: ```bash theme={null} helm repo add skyhook https://skyhook-io.github.io/helm-charts helm repo update skyhook kubectl create namespace radar --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic radar-cloud-config -n radar \ --from-literal=token=rhc_xxx... \ --dry-run=client -o yaml | kubectl apply -f - helm upgrade --install radar skyhook/radar -n radar \ --set cloud.enabled=true \ --set cloud.url=wss://api.radarhq.io/agent \ --set cloud.insecureSkipVerify=false \ --set cloud.clusterName=cl_xxx... \ --set cloud.existingSecret=radar-cloud-config \ --set auth.mode=proxy \ --set rbac.helm=true \ --set rbac.secrets=true \ --set rbac.podExec=true \ --set rbac.portForward=true \ --set rbac.metrics=true \ --set rbac.selfUpgrade=true ``` If your organization uses **IdP groups** for [cluster access](/docs/cloud/sso#cluster-access-for-new-clusters), the generated install disables the default role bindings. Apply the starter Kubernetes binding shown on the install page, using the groups and ClusterRole you want to grant access. Until you do, users cannot read the cluster through Radar. > [!WARNING] > > The token is shown **once**. If you lose it before completing the install, generate a new command from the cluster's row. The unused token stops working once the new one connects, or after 24 hours. ## What each value does | Value | Effect | | - | - | | `cloud.enabled=true` | Switches the chart to cloud-mode (auth proxy, ClusterRoleBindings for `radar:*` groups, hardened listener). | | `cloud.url` | The WebSocket endpoint to dial. `wss://api.radarhq.io/agent` for production. | | `cloud.insecureSkipVerify` | Skips TLS verification of the control plane. Only for trials against a Self-Managed control plane with a self-signed certificate; always `false` for Radar Cloud. | | `cloud.existingSecret` | The Secret holding the cluster token under the key `token`, so the token never appears in Helm values or release history. The token is a long-lived bearer credential identifying this cluster, revealed only once and SHA-256 hashed at rest by the control plane. See [Cluster tokens](/docs/cloud/cluster-token). | | `cloud.clusterName` | The opaque cluster ID assigned by the control plane. The Radar pod advertises this on handshake. | | `rbac.helm` | Enables Helm install / upgrade / rollback / uninstall. Auto-enables `rbac.secrets`. In Cloud mode, Helm runs as the signed-in user: Radar allows Helm writes in a namespace only if that user can create Secrets there ([Helm runs as you](/docs/cloud/rbac#things-to-watch-out-for)). With `cloud.defaultRbac.create=true`, it also binds `radar-helm` to the owner and member tiers and `radar-helm-admin` to owner only. | | `rbac.secrets` | Allows the resource browser to show Secrets. | | `rbac.podExec` | Enables the pod terminal and debug containers. | | `rbac.portForward` | Enables port forwarding through the UI. | | `rbac.metrics` | Reads CPU and memory metrics from metrics-server for the dashboard and resource panels. | | `rbac.selfUpgrade` | Lets Radar Cloud upgrade this Radar from the UI. Scoped to patching the one Radar Deployment. | | `auth.mode=proxy` | Matches the auth mode cloud-mode runs in, so every Helm RBAC binding for the `radar:*` groups renders. | Set any `rbac.*` flag to `false` to restrict what Radar can do in your cluster - the UI gracefully degrades. In cloud mode Radar can always read Secrets, and each user sees only the Secrets their own Kubernetes RBAC allows. See [In-cluster deployment](/docs/configuration/in-cluster) for the full list. ## What the chart deploys ``` Namespace: radar ├─ Deployment: radar # the OSS binary in cloud-mode ├─ ServiceAccount: radar ├─ ClusterRole / ClusterRoleBinding (read access + opt-ins) ├─ ClusterRoleBindings: │ radar:owner → admin ┐ │ radar:member → edit ├ not with defaultRbac.create=false │ radar:viewer → view ┘ │ radar:system → view + cluster-read + integration-read + Secret read │ (Radar Cloud alerts and timeline) │ radar:ai:reader → view-based read + cluster-read + integration-read │ (alert-triggered AI investigations) └─ Service / NetworkPolicy (optional) ``` Alerts and the timeline use a separate [background identity](/docs/cloud/rbac#radar-cloud-background-identity). [AI investigations](/docs/cloud/rbac#ai-investigation-access) use your access for requests you make and a dedicated group for alert-triggered turns. There's no Service exposed to the outside - the Radar pod only needs **outbound** TLS to `api.radarhq.io:443`. No ingress, no LB, no DNS. ## Verify the connection Inspect the installation from any machine with access to the cluster: ```bash theme={null} radar cloud status --context my-cluster ``` The command reports the detected Helm or GitOps installation, chart and image, agent readiness, Cloud configuration, Hub URL, configured cluster reference, and the cluster ID and tunnel state reported by the Hub. It is read-only and never prints the connection token. If Radar finds multiple installations, select one with `--namespace` and `--release`. To check the tunnel, Radar reads the token from the Kubernetes Secret referenced by the Deployment and sends it directly to the configured Hub. A confirmed disconnected tunnel or rejected token makes the command fail. If your Kubernetes identity cannot read that Secret, or the Hub cannot be reached, the tunnel check is shown as unavailable while the local installation diagnostics still run. The install page polls for up to 5 minutes. Once connected, you'll be redirected into the cluster view. If polling times out: ```bash theme={null} # Radar pod logs kubectl logs -n radar deploy/radar # Look for: [cloud] connected to wss://api.radarhq.io/agent as cl_xxx... [cloud] handshake ok, cluster status -> connected ``` Common failures: | Symptom | Likely cause | | - | - | | `401 unauthorized` on handshake | Token invalid or rotated. Generate a new install command. | | `connection refused` | Egress firewall blocking outbound `api.radarhq.io:443`. | | `TLS handshake failure` | Outbound proxy doing TLS inspection without the corporate CA injected into the Radar pod's trust store. | | Connects then drops | The Radar pod must run on a single replica; check for crash loops via `kubectl describe pod`. | ## Reinstall vs. rotate * **Reinstall** - re-run the same Helm command with a fresh token. Used when you've lost the original install yaml. * **Rotate token** - issues a new token without changing the cluster ID. By default the previous token keeps working until Radar connects with the new one (or for 24 hours), so the cluster stays online. Turn on **Revoke the current token immediately** when you suspect a leak. See [Cluster token rotation](/docs/cloud/cluster-token) for both flows. ## Keep Radar up to date Radar Cloud's dashboard is always the latest release, while each cluster runs the Radar version it was installed or last upgraded with. The **Clusters** page shows each cluster's version and marks it when a newer release exists. * **One-click upgrade** - an organization owner can upgrade a connected cluster from the **Clusters** page when its install allows self-upgrade (the default). Radar restarts and reconnects on the new version. * **Helm or GitOps** - when self-upgrade is off, upgrade through the Helm release or the GitOps source that manages Radar. The **Clusters** page says so for that cluster. * **When a page needs a newer Radar** - a page or section that relies on a newer Radar than the cluster runs says so, instead of failing. **Upgrade Radar** opens the **Clusters** page at that cluster's row. * **Out-of-date clusters** - when a cluster's Radar is four or more minor releases behind, its top bar shows **Radar out of date**, linking to the same place. ## Egress requirements | Destination | Port | Why | | - | - | - | | `api.radarhq.io` | 443 | The control-plane WebSocket. | | Container registry that hosts the chart / image | 443 | Pulling the Radar image. Default `ghcr.io/skyhook-io/radar`. | ## Air-gapped clusters Cloud assumes outbound TLS to `api.radarhq.io`. Air-gapped environments can run [Radar OSS](/docs/install), the same UI without a control plane, or [Radar Cloud Self-Managed](/docs/cloud/self-hosted/licensing#air-gapped) with the control plane inside your network. ## See also * [Cloud RBAC](/docs/cloud/rbac) - how `radar:owner` / `radar:member` / `radar:viewer` map to K8s, and access for background jobs and AI investigations. * [Cluster token rotation](/docs/cloud/cluster-token) - rotation flow. * [In-cluster deployment](/docs/configuration/in-cluster) - the chart's full value reference. # Connected apps & MCP access Source: https://radarhq.io/docs/cloud/connected-apps Connect external AI agents to your org over OAuth, manage their grants, and clamp the whole MCP surface to read-only with an org-wide access mode. There are two ways an AI agent reaches your clusters through Radar Cloud: a [personal access token](/docs/cloud/personal-access-tokens) it carries as a bearer credential, or an **OAuth** grant it obtains by signing in. Connected apps covers the OAuth path and the org-wide guardrail that governs both. ## Connect an agent over OAuth Radar Cloud's MCP endpoint is an OAuth 2.1 resource server. An MCP-aware agent that supports OAuth points at the endpoint, gets challenged, and runs a standard sign-in instead of needing a token pasted in: * The endpoint advertises its protected-resource metadata (RFC 9728) and returns a `WWW-Authenticate` challenge on an unauthenticated `/mcp` request, so compliant clients discover where to authenticate on their own. * Tokens are minted by hosted Radar Cloud's identity provider or, with [Radar Cloud Self-Managed](/docs/cloud/self-hosted/index), your own OIDC IdP. With Self-Managed, the IdP groups in that access token are forwarded to your clusters like a browser session's - see [Groups for AI agents over OAuth](/docs/cloud/self-hosted/auth#groups-for-ai-agents-over-oauth). * On first sign-in the agent's user and org membership are provisioned just in time - no pre-invite step. ### Manage grants Manage connected apps on **Agents**. Revoking a grant blocks its next authenticated request, but does not cancel an already-running request or stream, or revoke the token at your identity provider. ## Org-wide MCP access mode Independent of how an agent authenticates, every org has one switch that decides whether AI agents may *change* anything: **Settings → Organization → AI & MCP → MCP access**. Switching to Read-only saves at once; switching to Read-write asks you to confirm first. | Mode | Effect | | - | - | | **Read-write** (default) | Agents can use the full tool surface, including mutating actions. | | **Read-only** | Agents can inspect, never mutate. | Read-only is enforced on both surfaces an agent could use, so it can't be routed around: * On the **MCP endpoint**, write tools are dropped from `tools/list` and refused at `tools/call` with a tool-error the agent can surface. * On the **cluster proxy**, the same credential can otherwise reach Radar's REST and WebSocket API directly, so mutating HTTP methods and exec / attach / port-forward sessions are refused with a `403`. `diagnose` remains available in read-only mode. Radar Cloud publishes a constrained version built from an explicit allowlist of its non-mutating arguments, so workload diagnosis, logs, events, and non-mutating network checks still work. `in_cluster` is withheld from the schema; calls that request a live probe with `in_cluster: true`, supply a malformed value, or include an unclassified argument are refused at call time. New `diagnose` arguments require a Radar Cloud update before read-only agents can use them. Read-write mode exposes the full upstream schema to normal external agents, where `in_cluster` may create up to five self-deleting probe pods. > [!NOTE] > > The mode keys on the **credential AI agents use** - PATs and OAuth grants. Browser (cookie) sessions are never gated by it, so an owner who sets read-only can still operate clusters through the UI. It's **fail-safe by design**: a tool with no read-only annotation, or a name no connected cluster advertises, is treated as a write and hidden. The single exception is `diagnose`, which has a hardcoded argument-level policy in Radar Cloud; a connected cluster cannot introduce another exception. The call-time gate also keys off the specific cluster a call dispatches to, so a mixed-version fleet can't be used to slip a write past a read-only annotation on a different cluster. ## Auditing MCP activity lands in the [audit log](/docs/cloud/audit-log): tool calls record actor, tool, cluster, and outcome alongside grant create / revoke events. Successful high-volume calls may be grouped; arguments and response bodies are not stored for ordinary successful calls. ## See also * [Personal access tokens](/docs/cloud/personal-access-tokens) - the bearer-credential path for agents and CI. * [AI via MCP](/docs/features/mcp) - what the MCP server exposes and why. * [Audit log](/docs/cloud/audit-log) - the record of agent and member activity. # Fleet views Source: https://radarhq.io/docs/cloud/fleet-views Cross-cluster aggregates - issues, applications, search, audit checks, and package drift across every cluster connected to your org. The Fleet section in the left rail rolls up data across every cluster in your org. It's the answer to questions like "which of my clusters has audit failures right now?", "which one is running an outdated version of cert-manager?", or "where is this resource called `payments-api`?". ![Radar Cloud fleet topology - aggregated multi-cluster view with per-cluster health and drift](https://radarhq.io/images/radar/screenshot-fleet-topology.png) ## Scope picker A global **scope picker** narrows every fleet page to the clusters you select at once, instead of filtering each page on its own - so a large org can focus on the handful of clusters that matter right now. The [command palette](/docs/reference/keyboard-shortcuts) also indexes fleet resources inline, so `⌘K` reaches across clusters without opening a page first. ## Pages ### Issues `/fleet/issues` - cross-cluster grid of critical and warning conditions. Shows pods that aren't ready, deployments that aren't progressing, expired or expiring certs, and audit findings tagged `severity=critical`. Grouped by subject, the same triage model as single-cluster [Issues](/docs/features/issues). It's an org-shared triage queue: **acknowledge**, **snooze**, or **dismiss** an issue once and the change applies for everyone on the team, so two people don't re-investigate the same failure. Filterable by: * **Namespace** - across all clusters. * **Severity** - critical / warning. * **Category** - workload / network / cert / audit / etc. Click any row to drop into the resource in its cluster. ### Applications `/fleet/applications` - software grouped by app and folded across the environments it runs in, with promotion lag and version skew at a glance. See [Applications](/docs/cloud/applications) for the full breakdown. ### Search `/fleet/search` - hero-style search bar, queries every connected cluster in parallel and merges results. Useful when: * You know a name (`payments-api`) but don't remember which cluster it's in. * You want to find every Ingress with a specific host. * You're chasing a label across the fleet. Result rows show the cluster as a chip; clicking opens that cluster's resource browser. ### Checks `/fleet/checks` - the [Cluster audit](/docs/features/cluster-audit) findings rolled up across the fleet, as a shared remediation queue. Useful for: * "Which cluster needs the most attention?" (sorted by failing count). * "Which check fires across the most clusters?" (sorted by check). * "Did the new policy land everywhere?" (filter by check ID, see which clusters still fail). Org-level **Policy** lets you mute a category or an individual check across the fleet, so checks that don't apply to your org stop adding noise to every cluster. ### Packages `/fleet/packages` - drift matrix for installed Helm releases (and any tools Radar has version metadata for, like Cilium / Argo / Karpenter / Linkerd). One row per package, one column per cluster, cells colored by version skew. Useful for: * "Are all clusters on the same `cert-manager` version?" * "Which clusters haven't been upgraded to `argocd:2.13`?" ## Performance Fleet views fan out to every connected cluster in parallel. For \~10 clusters this is sub-second; for 100+ clusters expect a few-second initial load with progressive rendering as each cluster reports back. Disconnected clusters are shown with a `not reporting` chip - their data slot is empty until the in-cluster Radar reconnects. ## RBAC Fleet views are gated on the user's Cloud role: * **Viewer / member / owner** all see fleet views for clusters they have access to. * "Access" means the cluster is in the org. There's no per-cluster RBAC at the Cloud layer today - role gating happens inside the cluster via [Cloud RBAC](/docs/cloud/rbac). If you need per-cluster access control across the fleet (some users see prod, others don't), use K8s impersonation: assign the `radar:viewer` group to non-prod clusters' bindings only and bind `radar:member` only on the clusters where they should mutate. ## What's NOT here today * **Cross-cluster topology.** Each cluster's topology is per-cluster. Fleet-wide topology (services that span clusters via mesh / Submariner / etc.) is on the roadmap. * **Cross-cluster timeline.** Same - per-cluster only today. The dashboard's "recent problems" lane on the home page is the closest thing. ## See also * [Cluster audit](/docs/features/cluster-audit) - per-cluster checks. * [Cloud RBAC](/docs/cloud/rbac) - per-cluster role mapping. * [Connecting a cluster](/docs/cloud/connect-cluster) - what makes a cluster show up here. # IdP group-based access Source: https://radarhq.io/docs/cloud/idp-groups Reuse the groups you already manage in your identity provider to grant Kubernetes access through Radar. Your IdP group memberships arrive as radar:idp: subjects you bind in your own RBAC. Most teams already express "who can touch what" as groups in their identity provider (Entra, Okta, a generic OIDC / SAML IdP). Radar lets you reuse those exact groups to grant Kubernetes access - without re-authoring per-user bindings - on hosted Cloud and on a self-managed hub alike. Your IdP group memberships flow through sign-in and land in your cluster as `radar:idp:` subjects, which you bind in your own Kubernetes RBAC exactly as you already bind your raw IdP groups. This page covers the end-to-end process. It builds on two pages you should read first: * [SSO](/docs/cloud/sso) - group passthrough rides on top of a working SSO connection. * [RBAC under Cloud mode](/docs/cloud/rbac) - the binding recipes and governance models you'll use in the final step. > [!NOTE] > > **Hosted Cloud and self-managed enable this differently.** The subject vocabulary, the binding recipes and the troubleshooting below apply to both, but the setup steps on this page - the SSO setup portal and its attribute mappings - are hosted-Cloud machinery. > > On a self-managed hub passthrough is on by default and configured through Helm values instead - there is no portal step and nothing to switch on. See [Self-Managed authentication → IdP group passthrough](/docs/cloud/self-hosted/auth#idp-group-passthrough), then come back here for the binding recipes. ## The identity Radar presents to your cluster When a signed-in user drives a cluster request, the control plane does **not** forward the user's raw IdP identity (their IdP email, their real IdP group names) to your cluster. It asserts a **Radar-scoped, prefixed identity** that you opt into through your own RBAC. On each request the control plane injects a groups header containing: | Subject | Meaning | | - | - | | `radar:` | The user's Cloud role tier - `owner` / `member` / `viewer`. | | `radar:org:` | The organization the identity belongs to. | | `radar:user:` | An opaque per-user ID (stable across email changes). | | `radar:email:` | The user's lowercased email, for RBAC that binds individual humans by email. | | `radar:idp:` | One entry per IdP group the user belongs to (this page). | > [!NOTE] > > `radar:*` is the vocabulary to bind against. Use it for role, organization, user, email, and IdP group subjects. **Why a prefixed identity instead of your raw one?** You likely have many existing Kubernetes RoleBindings written over years and keyed to your real IdP identity - some for `kubectl`, some for CI, some binding a platform admin to `cluster-admin`. If Radar asserted the raw identity, every one of those bindings would become reachable the moment someone signs into Radar. With the prefixed identity, only the bindings you **explicitly** grant to a `radar:*` subject are reachable. You can `grep radar:` across your RBAC and see the complete, auditable list of what Radar can touch. This is **channel isolation** - a smaller, deliberately scoped, auditable door - not a blast-radius cap on a control-plane compromise. State it that way to a security team: the value is that Radar can reach only what you bound to `radar:*`, and that set is greppable and reviewable. ## The process end to end 1. **Connect SSO.** Group passthrough requires a working SSO connection ([SSO](/docs/cloud/sso)). 2. **Emit a stable group claim from your IdP** so memberships arrive as `radar:idp:`. 3. **Verify** the groups arrive as stable IDs before you write any RBAC. 4. **Bind `radar:idp:` in your Kubernetes RBAC**, optionally disabling the default role tiers for scoped users. ### 1. Connect SSO Follow [SSO](/docs/cloud/sso) end to end. Group passthrough is enabled per connection as part of SSO onboarding; if groups don't arrive after you complete the steps below, contact support to confirm passthrough is active on your connection. ### 2. Emit a stable group claim from your IdP This is two actions: your IdP has to **emit** a groups claim, and the SSO setup has to **map** that claim into Radar. Both are required - a claim your IdP emits but no one maps never reaches your cluster. 1. **In your IdP**, add a **groups claim** to the assertion / token it sends (see the provider examples below). 2. **In the SSO setup portal** (the "Open SSO setup" link from your [SSO](/docs/cloud/sso) connection), open **Attribute mappings → Edit attribute mappings**, find the **`groups`** attribute, and set its IdP field name to the exact attribute name your IdP emits the claim under. Save. > [!WARNING] > > The `groups` mapping is **optional** in the setup portal - unlike email / name it has no "Required" badge, so it is easy to skip, and if you skip it the field stays unset and **no groups ever flow** (with no error anywhere). If group access "just doesn't work," this unset mapping is the first thing to check. > > Enter the IdP field name **exactly** as your IdP emits it. It is often a bare attribute name, not a namespaced URI - e.g. Entra emits its customized group claim as plain `groups`, not `http://schemas.microsoft.com/ws/2008/06/identity/claims/groups`. A near-miss maps nothing. > [!WARNING] > > **The group value must be a stable, immutable identifier - a group object ID / GUID, not a display name.** Two reasons: > > 1. A RoleBinding names the value verbatim. If your IdP emits display names, renaming a group in the IdP silently breaks - or worse, repoints - every binding that named the old string. > 2. As a safety measure, Radar **drops** any group value containing whitespace, a comma, or a non-ASCII byte (this prevents a crafted group name from injecting an extra subject into the identity). A human-readable name like `Platform Admins` has a space and is dropped entirely; only a clean identifier such as a 36-character GUID survives. > > Configure your IdP to source the groups claim from the immutable group ID. This is usually the default. Provider examples (illustrative - any OIDC / SAML IdP works the same way): * **Microsoft Entra:** add a groups claim (App registration → Token configuration, or Enterprise App → Single sign-on → Attributes & Claims). Leave the **source attribute at its default** so the value is the group **object ID (GUID)**, not `sAMAccountName` or the cloud display name. * **Okta:** emit a groups claim on the SAML / OIDC app and use the **immutable group ID** as the value, not the group label. * **Generic OIDC / SAML:** expose your IdP's groups claim and source it from a **stable ID**. ### 3. Verify before writing RBAC Before you author a single RoleBinding, confirm your IdP actually sends the groups - as stable IDs. You can check this yourself in the SSO setup portal, no support ticket needed: 1. In the setup portal, click **Test sign-in** (or just have a user who belongs to the group sign in normally). 2. Open the **Sessions** list, click the sign-in you just made, and expand the **SAML response** (SAML) or token payload. 3. Confirm a groups attribute is present and its values are stable IDs (e.g. `a1b2c3d4-1234-5678-9abc-def012345678`), **not** display names with spaces. If the groups attribute is **absent**, your IdP isn't emitting it or the attribute mapping is unset (step 2). On a **self-managed** hub the equivalent causes are `auth.oidc.groupsClaim` naming a claim your IdP doesn't emit (it defaults to `groups`, which suits Okta, Keycloak and Entra but not ADFS), it having been set empty to turn passthrough off, or the `groups` scope in `auth.oidc.scopes` not matching what your provider expects. That cuts both ways: Okta and Keycloak may need the scope, and adding one your provider does not define fails the entire sign-in instead of just omitting the groups - see [the provider table](/docs/cloud/self-hosted/auth#idp-group-passthrough). If it's present but the values are **display names**, they'll be dropped and the user will see nothing - fix the IdP source attribute so it emits the group object ID. Only once the assertion carries the expected IDs should you write bindings whose subject is `kind: Group, name: radar:idp:`. ### 4. Bind the groups in Kubernetes RBAC You now bind `radar:idp:` subjects in **your own** Roles and RoleBindings, exactly as you bind your raw IdP groups today. A minimal example - a vendor group that can edit one namespace: ```yaml theme={null} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: vendor-team-edit namespace: vendor-prod subjects: - kind: Group name: radar:idp:a1b2c3d4-1234-5678-9abc-def012345678 # the group's object ID apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: edit apiGroup: rbac.authorization.k8s.io ``` Two related subjects are available for cases where a group isn't the right key: * `radar:email:` - bind an individual human by email, for RBAC that grants specific people rather than groups. * `radar:user:` - bind by opaque, email-independent user ID (see [Per-user bindings](/docs/cloud/rbac#per-user-bindings)). **Scoped users should usually get only what their group grants.** By default, on top of their group bindings, a Cloud user also gets the Kubernetes tier their **Cloud role** maps to: `radar:viewer` → `view`, `radar:member` → `edit`, `radar:owner` → `admin` - each with a cluster-scoped infra-read add-on (Nodes, PersistentVolumes, StorageClasses, IngressClasses, PriorityClasses, RuntimeClasses). A user carries **one** tier, not all three. So to give external or namespace-scoped users *no* baseline beyond their group, turn off the default bindings **for the tier they actually land in**. If they land as `viewer`, turn the viewer tier off so they start from nothing: ```yaml theme={null} cloud: defaultRbac: viewer: false # no namespaced "view" tier and no cluster-scoped infra read for viewers ``` > [!NOTE] > > `defaultRbac.viewer: false` on its own already removes both bindings for the tier - the namespaced view and the cluster-scoped infra read; the chart grants a disabled tier nothing. `clusterScopedRead.viewer: false` is the switch for keeping the tier on but dropping only the infra read. Setting both is harmless. **Match the flags to the tier they land in.** The viewer flags only zero out a `viewer`. If your scoped users land as `member` - the common default for auto-join and invites - disabling the viewer flags does nothing for them: a member keeps `radar:member` → `edit`. Turn off the member tier instead (`cloud.defaultRbac.member: false`), or land them as `viewer` first. See [SSO for external users](/docs/cloud/sso-external-users) for the contractor-specific walkthrough. > [!NOTE] > > Disabling human tier bindings leaves separate grants for [background jobs](/docs/cloud/rbac#radar-cloud-background-identity), including cluster-wide Secret read, and [alert-triggered investigations](/docs/cloud/rbac#ai-investigation-access). Review those grants separately. **To make IdP groups the default for every new cluster**, set [cluster access](/docs/cloud/sso#cluster-access-for-new-clusters) to **IdP groups**. Existing clusters are never changed; to switch one, follow [Switching an existing cluster to IdP groups](/docs/cloud/rbac#switching-an-existing-cluster-to-idp-groups). The two paths compose: keep the default tiers for your internal operators, and grant scoped external users access exclusively through their `radar:idp:` bindings. The [RBAC governance models](/docs/cloud/rbac#choosing-a-governance-model) walk through the mixed and full-IdP setups in depth - this page is the "how the groups get there" half of that. ## How fresh the groups are On hosted Cloud, groups come from your organization membership in WorkOS. Without Directory Sync, WorkOS updates that membership from your IdP on an **SSO sign-in**; emailed codes do not refresh it. Radar picks up the update within about five minutes of that sign-in. Contact us to enable Directory Sync for your organization. Removing someone from an IdP group does not revoke their existing Radar access immediately. To reduce access without waiting for another SSO sign-in: * Change the group's Kubernetes binding. Live operations use the new permissions; cached reads can keep the old permissions for up to two minutes. Other bindings may still grant access. * Remove the person under **Settings → Organization → Members** and revoke their credentials. Removal blocks new browser and PAT requests, but open terminals and streams continue. SSO, auto-join, or an MCP OAuth token that still asserts organization access can add them back; revoke their access in your IdP too. See [removing a member](/docs/cloud/members#removing-a-member). On a self-managed Hub, groups come from the ID token. Access changes follow that token's lifetime and refresh settings; see [how long group access lasts](/docs/cloud/self-hosted/auth#how-long-group-access-lasts). ## How enforcement works Kubernetes RBAC controls cluster access. Live operations run as the user; cached reads are filtered by Radar. Most cached namespaced reads use namespace-wide access, while Secrets and cluster-scoped resources have separate checks. See [Cloud RBAC](/docs/cloud/rbac) for the enforcement model and its limits. ## Troubleshooting **A user signs in fine but sees nothing.** When they can't list Pods or Deployments in any namespace, Radar shows a banner - "You can't read any namespace in this cluster" - listing the groups the cluster sees for them; check that a `radar:idp:*` subject is there. Almost always the IdP is emitting group **display names instead of stable IDs**. Names that contain spaces (or commas / non-ASCII characters) are dropped, so no `radar:idp:*` subject reaches the cluster and nothing is bound. Point the group claim at the immutable group object ID and have the user sign in again. Secondary causes: no RoleBinding yet names that group's ID, or group passthrough isn't enabled on the connection (contact support). **Renaming a group in the IdP broke access.** You bound a display name rather than the object ID, and the rename changed the string your RoleBinding referenced. Rebind to the group's immutable ID; renames won't affect it. **An AI investigation cannot read the cluster.** Requests you make use your access; alert-triggered turns use a separate group. See [AI investigation access](/docs/cloud/rbac#ai-investigation-access) for permissions and upgrade requirements. **A group with a clean ID still grants nothing.** Confirm the RoleBinding subject is exactly `radar:idp:` (the `radar:idp:` prefix is required - the raw group ID alone won't match) and that the ID in the binding matches the ID your IdP emits, character for character. ## See also * [Sign-in and access: which path applies](/docs/cloud/access-overview) - where group-based access sits among the other pieces. * [SSO](/docs/cloud/sso) - the connection this builds on. * [RBAC under Cloud mode](/docs/cloud/rbac) - binding recipes, the default role tiers, and the mixed vs full-IdP governance models. * [Organizations & roles](/docs/cloud/organizations) - the Cloud roles that seed the default tiers. * [Connecting a cluster](/docs/cloud/connect-cluster) - the chart values that enable cloud-mode enforcement. # Integrations: cloud accounts as agent tools Source: https://radarhq.io/docs/cloud/integrations Connect an AWS or Google Cloud account from Settings and the account's APIs become tools on the MCP endpoint your agents already use. Preview. An agent connected to Radar Cloud gets the cluster: resources, events, logs, metrics, the diagnose tool. **Integrations** extend that to the cloud account around the cluster. An organization owner connects an account from a catalog in Settings, Radar Cloud holds the credential, and the account's APIs show up as extra tools on the same MCP endpoint the agent is already connected to, next to the cluster tools. The agent never sees the credential. It calls a tool; Radar Cloud makes the call with the sealed credential and returns the result. > [!NOTE] > > Integrations are in **Preview**. The catalog currently offers **AWS** and **Google Cloud**; both carry a Preview label. More vendors are built and will be enabled as their coverage settles. ## Connect an account **Settings → Organization → Integrations**. Owners connect, repair and remove; the page shows every organization role what is connected. * **Connected** - the accounts already connected, each with a status pill, its tool count and when it was last used. **Recheck all** tests every installation that can be tested in parallel; one that is still waiting for you to finish its setup is skipped, not counted as broken. Click a card to open its details or repair a credential. * **Available** - the catalog, with search. Connecting walks you through the provider's own requirements: a form for the settings it needs and an optional test before saving. | Provider | Credential | How it works | | - | - | - | | **AWS** | IAM role (recommended on Radar Cloud) | Two phases: Radar mints an external ID you cannot change and shows the exact trust policy for a new role; you create the role with the policy you choose and paste its ARN. Radar verifies the confused-deputy guard before accepting. Temporary credentials are issued per request. | | **AWS** | Access keys | A long-lived access key and secret, optionally a session token. Best for a self-managed Hub, which already has an identity in your account. | | **Google Cloud** | Service account key | The project ID and a service-account JSON key. | For AWS, whatever the role or key is allowed to do is exactly what the agent can do; Radar does not narrow it and cannot. For Google Cloud, Radar mints tokens restricted to read-only Monitoring, Logging and Trace scopes, so the agent reads observability data regardless of what else the service account could do. See the note on read-only mode below. ## What an agent sees Nothing changes on the agent side. The MCP endpoint it already uses - through a [personal access token](/docs/cloud/personal-access-tokens) or an [OAuth grant](/docs/cloud/connected-apps) - lists the integration tools next to the cluster tools. Tools are named `skyhook__`, or `skyhook___` for a provider with several services (Google Cloud), and take an `integration` argument that names the connected account when the organization has more than one. > [!WARNING] > > The organization's [read-only MCP mode](/docs/cloud/connected-apps#org-wide-mcp-access-mode) governs the tools Radar itself authorizes - the cluster tools. It does **not** restrict integration tools: an AWS call is authorized by the credential you connected, and Radar cannot classify read from write across that API, so grant the role or key only what agents should be able to do. Google Cloud calls are limited to read-only Monitoring, Logging and Trace scopes by the token Radar mints. A tool call against an account whose credential has stopped working (rotated key, a sealed credential that can no longer be decrypted) returns an error naming the integration, and the card shows it as needing attention until you repair it. ## Permissions | Action | Owner | Member | Viewer | | - | - | - | - | | See what is connected | yes | yes | yes | | Recheck a connection | yes | through the API only | no | | Open details, connect, repair, remove | yes | no | no | | Use integration tools through an agent | governed by the credential you connected, not by Radar role | | | ## Related * [Connected apps & MCP access](/docs/cloud/connected-apps) - how agents authenticate to Radar Cloud, and the read-only clamp that applies to cluster tools * [Personal access tokens](/docs/cloud/personal-access-tokens) * [MCP server](/docs/features/mcp) - the cluster tools every agent gets # Members & invitations Source: https://radarhq.io/docs/cloud/members Invite members by email, set their role, and use domain auto-join so the right people land in your org without explicit invites. There are three ways someone joins an org: an explicit **invitation** to their email, **domain auto-join** when their email domain is one your org has verified, or **signing in through your organization** - an SSO sign-in that WorkOS routes to your org joins it on first sign-in. [Sign-in and access](/docs/cloud/access-overview) lays out how they combine. ## Inviting a member Open **Settings → Organization → Members**. Owners can use **Invite your team** to: * Enter one or more email addresses. * Pick a role - [owner / member / viewer](/docs/cloud/organizations#roles). * Click **Send invite**. Radar emails each person a one-time-use invitation that expires after 14 days. They must sign in with the invited email address. ## Pending invites Owners can cancel an invitation with **Revoke** under **Pending invitations**. To send a new invitation, enter the address again in **Invite your team**; a successful resend replaces the old link. ## Removing a member Under **Settings → Organization → Members**, owners can click **Remove** in another member's row and confirm. Their account and other organization memberships remain. Removal blocks new browser and PAT requests to this organization. On a self-managed Hub, it also ends their browser sessions. It does not terminate an already-open terminal or stream. Revoke their org-scoped PATs separately under **Settings → Organization → AI & MCP**. The tokens are not revoked by membership removal and can work again if membership is restored. Removal alone does not keep someone out: SSO, domain auto-join, or an MCP OAuth token that still asserts organization access can add them back. Revoke their access in your identity provider as well. ## Changing a role Owners can change a member's **Role** dropdown on the Members page. Granting or removing the owner role requires confirmation. Changes apply on the user's next request. ## Domain auto-join If your team all uses the same email domain, add it under **Settings → Organization → Domains** and verify it to skip the invite flow entirely. Verification is a DNS-TXT record the page shows you (`verification_token=...`) at the domain you are verifying - for a subdomain such as `eng.acme.com`, the guided setup gives the exact DNS host, not the parent's apex; Radar checks it in the background. Once the domain is verified, its auto-join switch is on, and the org-wide switch is on, new users who sign in with a verified email at that domain (asserted by your IdP through SSO, or confirmed by the emailed code) join automatically with the role set under **Settings → Organization → General → How people join**. An org-wide switch there turns auto-join off for every domain at once. ``` Domains: acme.com (verified, auto-join on), eng.acme.com (pending) Default role: member ``` A verified domain also routes its sign-ins through [SSO](/docs/cloud/sso) once a connection is set up; removing it stops both. Free-mail providers (`gmail.com`, `outlook.com`, `proton.me`, etc.) are blocklisted - you can't auto-join shared domains. Auto-join records a distinct `org_member.auto_joined` event in the [audit log](/docs/cloud/audit-log) - separate from `org_member.joined` (accepted invitations) - so you can filter auto-joins from explicit invites by action. ### Owners still need explicit promotion Auto-joined users always get the **default invite role** (typically `member`). The system never promotes anyone to owner via auto-join - that always requires explicit role assignment. ## Contact email for external users A contractor who signs in as a guest through SSO normally uses their **real employer address** (for example `dana@partner.com`), and the **Members** list shows it like any other. If instead your IdP rewrites guests onto a subdomain you own (`dana-partner.com@ext.example.com`) and sends a `contactEmail` claim, Radar keeps the real address and the **Members** list shows it on top, with the sign-in identifier beneath it. Either way, the real address is what an invitation sent to their employer email matches against, so an invite and an SSO guest resolve to the same person. See [SSO for external users](/docs/cloud/sso-external-users) for both setups. ## Invitation flow under SSO When the org has an [SSO connection](/docs/cloud/sso) configured, invitations and domain auto-join still work - they're additive. A user invited by email but who arrives via SSO will be matched on their verified email, and the invitation will resolve. Radar cannot yet require SSO for an organization: a member who already has an account can still use the Cloud sign-in page. What you can do today is skip invitations entirely - verify your domain and let auto-join or the organization sign-in link bring people in through your IdP - and keep Cloud roles and cluster bindings tight so a stray sign-in reaches little. ## See also * [Sign-in and access: which path applies](/docs/cloud/access-overview) - how invitations, auto-join and the organization sign-in link combine with SSO. * [Organizations & roles](/docs/cloud/organizations) - the role definitions. * [SSO](/docs/cloud/sso) - SAML / OIDC self-serve. * [SSO for external users](/docs/cloud/sso-external-users) - contractors and guests who keep their employer email. * [Audit log](/docs/cloud/audit-log) - every invite / join / remove is recorded. # Notifications Source: https://radarhq.io/docs/cloud/notifications Personal email preferences, the in-app inbox, and org-level Slack / generic webhook destinations for cluster, member, and billing events. Radar Cloud has three notification surfaces: * **In-app inbox** - the bell icon in the top nav. Free, per-user, automatic. * **Personal email** - opt-in toggles per event type, scoped to the events that concern you (e.g. `cluster.disconnected` for a cluster you own). * **Org destinations** - Slack and HMAC-signed webhooks for org-wide subscriptions. Owner-only. ## Event kinds | Kind | Source | Default surfaces | | - | - | - | | `cluster.disconnected` | Tunnel drops past the flap-short threshold | Inbox + email (opt-in) + org destinations | | `cluster.reconnected` | Tunnel comes back | Inbox + org destinations | | `org.member.invited` | Invite sent | Inbox + org destinations | | `org.member.joined` | User joined (invite or auto-join) | Inbox + org destinations | | `billing.payment_failed` | Stripe webhook | Inbox (owners) + email (owners) + org destinations | | `billing.free_tier_cap_hit` | 4th cluster create attempted on Free | Inbox (owners) + org destinations | | `issue.opened` | A live issue matches an [alert rule](/docs/cloud/alerts) and persists past its minimum duration | Inbox + rule's destinations | | `issue.resolved` | A matched issue clears past its resolve grace | Inbox + rule's destinations | Members don't see billing events in their inbox - that lane is owner-scoped. The cluster, member, and billing events above are built-in and routed by per-destination kind filters. **Issue alerts work differently** - they're driven by [notification rules](/docs/cloud/alerts) you define over the Issues stream, with their own filtering, timing, and a durable [Alerts](/docs/cloud/alerts) history. The `issue.opened` / `issue.resolved` rows here are just where those alerts land as inbox and destination deliveries. ## In-app inbox The bell icon shows unread count. Click it to open the inbox tray. Read state is per-user-per-event: marking read on your laptop syncs across your sessions but doesn't affect anyone else. The inbox doesn't expire on its own today; deletion is manual. (Archive / snooze is on the roadmap.) ## Personal email preferences **Settings → Preferences → Notifications**. | Toggle | Default | What it does | | - | - | - | | Notify on cluster disconnect | off | Email on `cluster.disconnected` for clusters you connected | | Notify on payment failure | on (owners) | Email on `billing.payment_failed` | ## Org destinations (Slack / webhook) **Settings → Organization → Notifications → Add destination** (owner-only). ### Slack We use Slack's incoming-webhook model. Click **Add destination → Slack**, paste a Slack webhook URL, and pick which event kinds the destination subscribes to. Test with the **Send test message** button. The webhook URL is sealed at rest (NaCl `secretbox`, key from the control plane's `HUB_NOTIFY_KEY` env var). Owners only see a redacted view (`https://hooks.slack.com/services/T***/B***/...`) after creation. ### Generic webhook For anything that isn't Slack - PagerDuty event API, Statuspage, your own incident tool. Click **Add destination → Webhook**: * **URL** - the endpoint we POST to. * **HMAC secret** (optional) - if set, every request includes `X-Radar-Signature: sha256=` over the raw body. * **Event kinds** - the lane filter (empty = subscribe to everything). Body shape: ```json theme={null} { "event_kind": "cluster.disconnected", "org_id": "org_01HF...", "occurred_at": "2026-04-22T12:34:56Z", "data": { "cluster_id": "cl_01HF...", "cluster_name": "prod-us-east-1", "last_connected_at": "2026-04-22T12:30:00Z", "downtime_seconds": 296 } } ``` `data` is per-event-kind. Schemas live alongside the event-kind table on the [Audit log](/docs/cloud/audit-log) page. ### Verifying signatures ```bash theme={null} echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" ``` Match against the `X-Radar-Signature: sha256=` header. If they don't match, drop the request. ### Delivery * Best-effort. Failed deliveries retry on a fixed backoff (3 attempts) and then drop. * Each delivery records a row in `notification_deliveries` so you can see latency and outcome in the **Deliveries** tab of the destination. * Strict per-destination kind filtering. If a destination subscribes to `["cluster.disconnected"]`, it never receives `cluster.reconnected`. ## Subscribing to specific clusters Today the personal toggles are global ("notify me on any cluster disconnect"). Per-cluster filtering is on the roadmap - until then, use a webhook destination with kind filter + your own routing logic. ## See also * [Audit log](/docs/cloud/audit-log) - the underlying event taxonomy. * [Cluster token rotation](/docs/cloud/cluster-token) - rotating a token also fires a `cluster.token_rotated` event. # Organizations & roles Source: https://radarhq.io/docs/cloud/organizations Organizations are the tenant boundary in Cloud - they own clusters, members, billing, and audit. Three roles: owner, member, viewer. In Radar Cloud, the **organization** is the tenant boundary. Clusters belong to one org. Members belong to one or more orgs. Billing, audit log, SSO config, and notification destinations are all org-scoped. ## Roles | Role | What it can do | | - | - | | **Owner** | Manage members, billing, SSO, organization settings, and cluster lifecycle. | | **Member** | Use clusters and edit their details. Can connect new clusters when the organization allows it. Cannot manage the organization. | | **Viewer** | Use read-only product surfaces. Cannot change organization or cluster settings. | These roles control what someone can do in Radar. Access inside Kubernetes is configured separately for each connected cluster and can also use identity-provider groups. See [RBAC under Cloud mode](/docs/cloud/rbac) for details. ## Multi-org membership A user can belong to multiple orgs (e.g. one org per company they consult for, or a personal org plus a work org). The org switcher is the dropdown at the top of the left rail. Switching orgs reloads the page - your "current org" is server-side state, so links to clusters / settings always resolve in the correct tenant. ## Org settings **Settings → Organization** (owner-only edit; members see read-only): * **Name** - human label. * **Allowed email domains** - list of domains that auto-join. Free-mail providers (`gmail.com`, etc.) are blocklisted. * **Default invite role** - the role applied when users join via auto-join or an invitation link without a pre-selected role: `member` or `viewer`. It can't be `owner`, because it also applies to anyone who joins through SSO or domain auto-join. Organizational metadata you might also need lives elsewhere: * **Members** - [Members & invitations](/docs/cloud/members) * **Billing** - [Billing & plans](/docs/cloud/billing) * **SSO** - [SSO](/docs/cloud/sso) * **Audit log** - [Audit log](/docs/cloud/audit-log) * **Notifications** - [Notifications](/docs/cloud/notifications) ## Creating a new org From the org switcher, click **Create organization**. Name it, and you'll land on its empty home page. Connecting your first cluster mints the org's first audit event. ## Deleting an org Owner-only. **Settings → Organization → Delete organization**. If the org has an active, trialing or past-due subscription, the delete is refused. Cancel the subscription first from **Settings → Billing → Manage billing**, then come back. Deleting the org: 1. Removes every cluster and closes its tunnel. The in-cluster Radar pods keep running but can no longer reach the control plane, so uninstall the chart to clean up fully. 2. Drops all members from this org. Their user accounts aren't deleted, they just lose membership. 3. Deletes the org's audit log immediately. Nothing is kept for the retention window, so [export it as CSV](/docs/cloud/audit-log#export) first if you need a record. There's no recovery once delete is confirmed. ## See also * [Members & invitations](/docs/cloud/members) - how to add people. * [SSO](/docs/cloud/sso) - SAML / OIDC. * [Billing & plans](/docs/cloud/billing) - free tier and Team / Enterprise / Custom. * [Cloud RBAC](/docs/cloud/rbac) - how the role flows into K8s. # Radar Cloud overview Source: https://radarhq.io/docs/cloud/overview How Radar Cloud is built - the hosted control plane, the in-cluster Radar pod, the tunnel, and what stays in your cluster vs. what goes to the cloud. Radar Cloud is a hosted control plane that sits in front of your in-cluster Radars. **The Radar binary still runs in each of your clusters** - the same OSS engine, configured to dial out over a long-lived WebSocket. Skyhook hosts the team layer (`app.radarhq.io` UI, `api.radarhq.io` control plane), and your data plane stays where the cluster is. What we host: auth, orgs, billing, audit, the cluster registry, and the WebSocket multiplexer that fans your browser's requests out to the right in-cluster Radar. What we don't host: the cluster-side binary itself and its Kubernetes credentials. Radar Cloud doesn't copy your manifests, logs or metrics: when you open one in the UI, it streams through the control plane to your browser and isn't stored. What Radar Cloud does keep, including your event timeline and the logs and manifests a hosted AI investigation read, is listed under [What lives in the Cloud](#what-lives-in-the-cloud). ## What you get on top of OSS * **Multi-cluster from one URL.** Each cluster runs its own in-cluster Radar. Cloud federates them - they appear in a switcher, and fleet views aggregate across them. * **Team workflow.** Organizations, role-based access (owner / member / viewer), email + SSO invitations, audit log. * **Hosted auth.** Email magic-link, Google, passkeys, plus customer-side SAML / OIDC self-serve setup. * **Personal access tokens** for AI / CI clients, scoped per user, with rotation. * **In-app inbox + webhook deliveries** for cluster / org / billing events. * **Stripe billing** with a real Free tier, per-cluster Team and Enterprise plans, and Custom contracts. The cluster engine itself is identical - same topology, timeline, audit, MCP, integrations. ## Architecture ``` Browser (app.radarhq.io) │ session cookie (WorkOS sealed JWT) ▼ Radar Cloud control plane (api.radarhq.io) - Go service ├─ owns: users, orgs, clusters, members, billing, audit, PATs, prefs ├─ stores: Postgres (durable state) │ ├─ per cluster: long-lived WebSocket (yamux) │ wss:443 (TLS, outbound from your cluster) └─→ Customer Kubernetes cluster └─ Radar pod (the OSS binary, in cloud-mode) ├─ does NOT store browser session secrets ├─ does NOT phone home except on the WS ├─ Control plane injects X-Forwarded-User / X-Forwarded-Groups on every request └─ Radar (--auth-mode=proxy) impersonates live calls and filters cached reads ``` When you open a cluster in the UI, the browser hits `api.radarhq.io/c/{cluster_id}/...` over HTTPS. The control plane validates your session, opens a fresh stream over the cluster's existing WebSocket, and reverse-proxies the request to the Radar pod inside the cluster. SSE, pod-exec WebSockets, and MCP all flow through the same yamux multiplex. ## What stays in your cluster | Data | Lives in | Why | | - | - | - | | Kubernetes resources, logs, metrics | Cluster | Read live when you open them and streamed through the control plane to your browser, not stored. Live K8s state is authoritative; copying it would just stale-cache it. | | Topology, audit findings, traffic | Cluster | Computed at request time from live K8s state. | | Helm releases, GitOps state | Cluster | Read straight from the cluster's own secrets / CRDs. | ## What lives in the Cloud | Data | Why | | - | - | | User identity, org membership, roles | Tenancy boundary - the control plane enforces access. | | Cluster registry (id, name, env, labels, install-token hash, last-connected) | So the org has a shared list of clusters. | | Billing + subscription state | Stripe lives here. | | Event timeline: Kubernetes events and resource changes (kind, namespace, name, and what changed) | So history survives pod restarts and spans clusters. Kept for your plan's [retention period](/docs/oss-vs-cloud). | | Alert rules, alert history and notifications | So alerts fire and route when nobody has Radar open. | | AI investigation transcripts, including the logs and manifests an investigation read | So investigations can be resumed and shared. They run on a model managed by Radar Cloud; see [investigations in Radar Cloud](/docs/features/diagnose#investigations-in-radar-cloud). | | Audit log + PATs | Cross-cluster audit trail and AI tokens. | | User preferences (theme, pinned kinds, notify-on-\* toggles) | Follows you across clusters. | With [Radar Cloud Self-Managed](/docs/cloud/self-hosted), all of the above lives in your own infrastructure instead, apart from any AI model or identity provider you connect. The cluster bearer token is one-time-reveal: the control plane stores only a SHA-256 hash. If you lose the token, [rotate it](/docs/cloud/cluster-token). ## Auth boundary The cluster's bearer token (Bearer `rhc_...`) is validated by the control plane on the WebSocket handshake. Once the tunnel is open, in-cluster requests are implicitly trusted - the tunnel is the boundary. There's no per-request token exchange. For requests through the tunnel, the control plane strips inbound `X-Forwarded-*` (anti-spoof) and injects `X-Forwarded-User: ` + `X-Forwarded-Groups: radar:,radar:org:,radar:user:`. The in-cluster Radar runs `--auth-mode=proxy` under cloud-mode and uses that identity for impersonated live calls and authorization checks on cached reads. See [Cloud RBAC](/docs/cloud/rbac) for how the Radar groups map to K8s ClusterRoles. ## Where to next * [Connecting a cluster](/docs/cloud/connect-cluster) - the install command and what each Helm value does. * [Organizations & roles](/docs/cloud/organizations) - the permission model. * [SSO](/docs/cloud/sso) - SAML / OIDC self-serve. * [Billing & plans](/docs/cloud/billing) - free tier, Team, Enterprise, Custom. * [Audit log](/docs/cloud/audit-log) - what's recorded and how to export. # Personal access tokens Source: https://radarhq.io/docs/cloud/personal-access-tokens Mint per-user PATs for MCP clients (Claude Code, Codex, Cursor, and others) and CI scripts. Org-pinned at mint time, SHA-256 hashed at rest, revocable any time. Personal access tokens (PATs) are per-user bearer credentials. They're how Cloud authenticates non-browser clients - MCP clients (such as Claude Code, Codex, Cursor, GitHub Copilot, OpenCode, and Google Antigravity) and CI scripts hitting the API. PATs: * Are **per user**, **per org** (pinned at mint time). * Carry the user's role and identity into the request - just like a browser session would. * Are revealed **once** at mint and stored only as a SHA-256 hash on the Radar Cloud control plane. * Can be revoked instantly. * Show up in the [audit log](/docs/cloud/audit-log) on create / use / revoke. > [!NOTE] > > A PAT is one of two ways an agent authenticates - the other is OAuth. For the OAuth flow, managing connected apps, and the org-wide read-only switch that governs what *any* agent may mutate, see [Connected apps & MCP access](/docs/cloud/connected-apps). ## Mint a PAT Open **Agents**, pick your client, and select **Personal access token**. * Choose an expiry: one year by default, or **No expiry** to keep it valid until revoked. * Click **Generate**. The token starts with `rhp_` and is shown once. Copy it now; you cannot retrieve it later. It is named for the selected client, and you can rename it after generation. ## Use it - AI clients The **Agents** page provides the setup snippet for your selected client. For an HTTP client: ```json theme={null} { "mcpServers": { "radar-prod": { "type": "http", "url": "https://api.radarhq.io/c//mcp", "headers": { "Authorization": "Bearer rhp_..." } } } } ``` For client-specific instructions, see [AI via MCP](/docs/features/mcp). ## Use it - HTTP API ```bash theme={null} curl -H "Authorization: Bearer rhp_..." \ https://api.radarhq.io/c//api/topology ``` Anything you can do in the UI you can do with a PAT - constrained by your role and your org's RBAC. ## Org pinning A PAT is bound to **one** org at mint time, even if the user belongs to multiple. This makes audit trails clean (you know which org a token can act in) and keeps "switch org" semantics out of the MCP / CI surface area. If you need access to multiple orgs, mint one PAT per org. ## Rotate There's no "rotate in place" - mint a new token, update the consumer, then revoke the old one. The Agents page shows tokens and their last-used timestamps. ## Revoke On **Agents**, revoke the token from its row. The next API call using it returns `401`; an already-running request or stream is not canceled. Revoking emits a `pat.revoked` audit event. ## When a member is removed Membership removal and token revocation are separate. Org-scoped PATs fail new authorization checks while their owner is absent, but can work again if membership is restored. See [removing a member](/docs/cloud/members#removing-a-member). ## Visibility Users manage their own PATs on **Agents**. Organization owners can list and revoke org-scoped PATs under **Settings → Organization → AI & MCP**. Neither page reveals a token again after generation. ## See also * [AI via MCP](/docs/features/mcp) - the per-tool setup snippets. * [Audit log](/docs/cloud/audit-log) - PAT events. * [Members & invitations](/docs/cloud/members) - membership removal and credential revocation. # RBAC under Cloud mode Source: https://radarhq.io/docs/cloud/rbac How a Cloud user's role becomes a Kubernetes identity. The radar:owner / radar:member / radar:viewer groups, the X-Forwarded-* headers, and how to override the default ClusterRoleBindings. Radar Cloud passes the signed-in user's identity to the in-cluster Radar. Kubernetes RBAC controls their cluster access: * **Live operations** - writes, exec, logs, attach, and uncached reads - run as the user through Kubernetes impersonation. * **Cached reads** - including ordinary resource lists, details, timeline, and topology - are filtered by Radar using Kubernetes authorization checks. For most namespaced resources, access is namespace-wide: permission to read one kind can expose other cached kinds in that namespace. Secrets and cluster-scoped resources have separate checks. See [read granularity](/docs/configuration/authentication#read-granularity-namespace-level). Shared Radar configuration, such as integration settings and Helm chart sources, also requires the appropriate Cloud role. ## The header contract For every request through the tunnel, the control plane: 1. **Strips inbound `X-Forwarded-*`** to prevent spoofing. 2. **Injects:** * `X-Forwarded-User: ` (e.g. `user_01HF...`) * `X-Forwarded-Groups:` a comma-separated list of the groups below. The groups header can carry any of these, and you can bind K8s RBAC to any of them: | Group | Source | | - | - | | `radar:` | The user's Cloud role (`owner` / `member` / `viewer`) in the org that owns the cluster, including investigations they start or follow up on. | | `radar:org:` | The org that owns the cluster. | | `radar:user:` | The individual Cloud user. | | `radar:idp:` | One entry per IdP group. Hosted Cloud uses the organization memberships stored in WorkOS; self-managed hubs use the ID token. See [IdP groups](/docs/cloud/idp-groups#how-fresh-the-groups-are). | | `radar:email:` | The user's verified email, passed through from the IdP identity. | | `radar:ai:reader` | The initial alert-triggered investigation turn. See [AI investigation access](#ai-investigation-access). | The `radar:idp:*` and `radar:email:*` groups are the hooks the [governance examples](#choosing-a-governance-model) bind - they exist independently of this chart's tier bindings. For user requests, role, org, and user groups are also emitted under the deprecated `cloud:` prefix. IdP and email groups use only `radar:`; write new bindings against `radar:*`. Alert-triggered investigation turns send only `radar:ai:reader`; hub background jobs send only `radar:system`. The in-cluster Radar (under `RADAR_CLOUD_MODE=true`) is **forced** to `--auth-mode=proxy` with these header names pinned. The chart enforces this; you can't override it without un-cloud-mode-ing the in-cluster Radar. ## Pod trust boundary The Radar ServiceAccount can impersonate any Kubernetes user or group. Treat the pod as a privileged authentication proxy: restrict who can modify or exec into it, or read its ServiceAccount token. Disabling the default Cloud-user bindings does not reduce this impersonation grant. ## The default RBAC The chart ships three ClusterRoleBindings out of the box: | Cloud role | Impersonated as group | Bound to ClusterRole | | - | - | - | | Owner | `radar:owner` | `admin` (K8s built-in) | | Member | `radar:member` | `edit` (K8s built-in) | | Viewer | `radar:viewer` | `view` (K8s built-in) | K8s itself decides what each ClusterRole permits. `view` is read-only for namespaced resources. `edit` adds writes. `admin` adds RBAC operations. ### Cluster-scoped resources need an extra grant The built-in `view` / `edit` / `admin` roles are **namespace** roles - by Kubernetes design they grant nothing on most cluster-scoped resources: | Cluster-scoped resource | `view` | `edit` | `admin` | | - | :-: | :-: | :-: | | Node | ✗ | ✗ | ✗ | | PersistentVolume | ✗ | ✗ | ✗ | | StorageClass | ✗ | ✗ | ✗ | | IngressClass | ✗ | ✗ | ✗ | | PriorityClass | ✗ | ✗ | ✗ | | RuntimeClass | ✗ | ✗ | ✗ | | ClusterTrustBundle | ✗ | ✗ | ✗ | So a tier bound only to a built-in role can't see Nodes, PVs, storage, or ClusterTrustBundles - core infrastructure and trust metadata used by an observability tool. Radar's chart closes this with a **cluster-read add-on**, **on by default per tier** (`cloud.defaultRbac.clusterScopedRead.{viewer,member,owner}`, each `true`). It grants `get`/`list`/`watch` on the kinds above - **no Secrets, no RBAC objects, and no PodCertificateRequests**. ### Pod certificate requests are opt-in Kubernetes 1.37 serves namespaced `PodCertificateRequest` resources, and Radar can display them in the generic resource browser. The default Cloud tier bindings deliberately do not grant access: certificate-request metadata is more sensitive than ordinary workload inventory. A user sees these resources only where their own Kubernetes RBAC grants `get/list` access. To opt in, bind a Role or ClusterRole granting `get/list/watch` on `certificates.k8s.io/podcertificaterequests` to the intended `radar:viewer`, `radar:member`, or `radar:owner` group. Prefer a namespaced Role when users need access only within a specific workload boundary. Cluster-read is its **own axis**, separate from which namespaced ClusterRole a tier binds to: * **Turn it off for a tier to remove this add-on's binding.** With the default built-in mappings that leaves the tier namespaced-only: `clusterScopedRead.viewer: false` leaves viewers on the stock `view` (namespaced read) with no cluster-scoped visibility - the usual choice for a low-trust viewer tier (see [security-conscious setups](#locking-down-for-security-conscious-orgs)). With a custom `*ClusterRole`, the tier's cluster-scoped reach is whatever that role grants - the flag only controls our binding. * **Swapping a tier's namespaced role does not disable cluster-read.** `viewerClusterRole`/`memberClusterRole`/`ownerClusterRole` and `clusterScopedRead.*` are independent bindings. If you want a custom namespaced role *and* no infra-read, set that tier's `clusterScopedRead` to `false` too. * **A tier you've disabled entirely** (`viewer: false`) gets nothing here either - you own that tier's RBAC. The add-on covers the **core** cluster-scoped kinds above, plus a few integration kinds that a Radar screen gates on per caller, each granted only when the chart's own collection of that group is on: Karpenter's groups (`rbac.crdGroups.karpenter`, for the Capacity view), Prometheus Operator `PrometheusRules` (`rbac.crdGroups.prometheus`), and the two Cilium policy kinds `CiliumNetworkPolicy` and `CiliumClusterwideNetworkPolicy` (`rbac.crdGroups.cilium`, for the Traffic view's dropped-flow panel). Other cluster-scoped *CRDs* that appear in topology (`GatewayClass`, Calico global policies, …) are **not** included - grant those per the CRD's group if your tiers need them. Owner node **cordon/drain** is a separate cluster-scoped *write* and is **off by default** - enable with `cloud.defaultRbac.nodeOps: true`. ## Radar Cloud background identity Some Radar Cloud features run without a person behind them: the **alerts worker** (which polls each cluster for issues) and the **timeline puller** (which records the cluster's change history in Radar Cloud). They send only the `radar:system` group, and the chart binds that group for them: | Binding | What it grants | | - | - | | `radar:system` → a role that tracks Kubernetes `view` | Namespaced read, plus permissions added by roles labelled `aggregate-to-view`. | | `radar:system` → the cluster-read and integration-read add-ons | The same cluster-scoped infra read and integration kinds the tiers get. | | `radar:system` → Secret `get`/`list`/`watch`, cluster-wide | Helm stores each release as a Secret, so Helm release alerts and Secret changes in the Radar Cloud timeline need it. Read-only. | The chart supplies read-only permissions. These grants are independent of `cloud.defaultRbac`: they stay when you turn the tier bindings off, so alerts and the timeline keep working when [IdP groups decide cluster access](#choosing-a-governance-model). To remove all of the chart's default background grants, set `cloud.systemRbac` (default `true`) to `false`: ```yaml theme={null} cloud: systemRbac: false # remove the chart's default background grants ``` With it off, alerts and the Radar Cloud timeline read nothing unless you bind `radar:system` yourself. If a cluster lacks background access, upgrade its Helm chart or bind `radar:system` yourself. * **Change it through the chart value.** A binding you edit or delete with `kubectl` comes back on the next `helm upgrade` or GitOps sync. * **Upgrades with `--reuse-values`** from a chart that predates `cloud.systemRbac` leave it **off**, so an existing install never gains cluster-wide Secret read without someone choosing it. Set `cloud.systemRbac=true` on those installs. Fresh installs, plain `helm upgrade`, `--reset-then-reuse-values` and GitOps get the default. * **Investigations use separate access.** See [AI investigation access](#ai-investigation-access). > [!NOTE] > > Include `radar:system` in access reviews. Its Secret read covers every namespace: Kubernetes RBAC cannot narrow it to Helm's Secrets. ## AI investigation access [AI investigations](/docs/features/diagnose) are read-only. Hosted investigations, whether started by a person or an alert, require **Radar 1.16.0 or later** on the cluster. If Radar is older or offline, the investigation cannot proceed; upgrade or restore the connection first. ### Requests from a person use their access Starting an investigation or asking a follow-up uses that person's Kubernetes identity and groups, including their Radar role and IdP groups. A user restricted to certain namespaces gets an investigation restricted to those namespaces too. This also applies when someone follows up on an investigation started by an alert. If Radar Cloud cannot confirm the person's access, the request stops and asks them to sign in again. It does not use a broader identity instead. ### Alert-triggered investigations use `radar:ai:reader` The initial alert-triggered turn sends only `radar:ai:reader`, without a person's groups or a Radar role. Kubernetes audit logs record these reads as `radar:ai:`. By default, the chart grants: | Access | What it covers | | - | - | | Read permissions based on Kubernetes `view` | Namespaced resources, including pod logs and ConfigMaps. Stock `view` excludes Secrets. | | Cluster-read and integration-read add-ons | The infrastructure and integration resources described [above](#cluster-scoped-resources-need-an-extra-grant). | These grants are independent of `cloud.defaultRbac`, so disabling the human tier bindings does not remove them. Changing `viewerClusterRole` does not affect them, but roles labelled `aggregate-to-view` widen the AI role too. Review those roles before assuming it cannot read Secrets or other sensitive data. To replace the chart's default grant with your own bindings: ```yaml theme={null} cloud: aiRbac: false # remove the chart's default AI access grants ``` Any bindings you create for `radar:ai:reader` still apply. For example, use RoleBindings to limit it to selected namespaces. With no grants, alert-triggered investigations cannot read the cluster. To stop automatic analysis, clear **Include AI analysis** on your alert rules under **Settings → Organization → Notifications**. When upgrading from a chart that predates `cloud.aiRbac`, use `--reset-then-reuse-values` to apply new defaults while preserving your supplied values. Plain `--reuse-values` can leave the key absent, which creates no default grant. An explicit `cloud.aiRbac=false` is preserved by either upgrade. ### Who can see the results Sharing an investigation makes its whole transcript visible to the organization. Alert-triggered investigations are organization-visible from the start. Transcripts contain results collected under each asker's access, which can exceed another member's own Kubernetes access. See [investigation sharing](/docs/features/diagnose#investigations-in-radar-cloud). [MCP clients](/docs/features/mcp), such as Claude Desktop and Cursor, use the user's own Cloud identity and groups. ## Overriding the defaults The chart's `cloud.defaultRbac.*` block lets you swap each binding: ```yaml theme={null} # values.yaml for the in-cluster Radar cloud: enabled: true url: wss://api.radarhq.io/agent clusterName: cl_xxx existingSecret: radar-cloud-token defaultRbac: create: true # set false to skip all three bindings owner: true # emit radar:owner binding member: true viewer: true ownerClusterRole: cluster-admin # ClusterRole each binding maps to memberClusterRole: edit viewerClusterRole: view clusterScopedRead: # cluster-scoped infra read, per tier (each default true) viewer: true # set false → viewer is namespaced-only member: true owner: true nodeOps: false # owner cordon/drain (cluster-scoped write); off by default ``` To **disable** a binding entirely (e.g. you want only owners to have access, no members or viewers): ```yaml theme={null} cloud: defaultRbac: member: false viewer: false ``` Members and viewers will then hit 403s when they try to do anything in the cluster. The Cloud UI still shows the cluster in the list - it's the K8s API that's saying no. ## Locking down for security-conscious orgs The defaults optimize for "an observability tool should show infrastructure": every tier gets cluster-scoped **read** out of the box, and **writes** still flow through each tier's K8s role. If your security posture is tighter, these are the levers - each is a small `values.yaml` change, no custom roles required: **Make the viewer tier namespaced-only** (most common). Viewers keep read access to the namespaces their K8s identity allows, but cannot enumerate Nodes, PVs, or storage cluster-wide: ```yaml theme={null} cloud: defaultRbac: clusterScopedRead: viewer: false # member + owner still get infra read ``` **No cluster-scoped read for anyone** - every tier is limited to its namespaced built-in role: ```yaml theme={null} cloud: defaultRbac: clusterScopedRead: viewer: false member: false owner: false ``` **Non-admin owners.** The default `owner → admin` reads Secrets and mutates RBAC in every namespace (namespace-scoped, not cluster-wide). Point owners at a tighter custom ClusterRole; cluster-read stays independent, so you keep infra visibility unless you also disable it: ```yaml theme={null} cloud: defaultRbac: ownerClusterRole: my-restricted-owner # your ClusterRole # clusterScopedRead.owner stays true → owners still see Nodes/PVs/etc. ``` **Keep cluster-scoped write off.** Node cordon/drain (`nodeOps`) is already `false` by default. Leave it off unless owners genuinely operate nodes from Radar - K8s RBAC cannot scope `patch nodes` to just the cordon field (it also permits relabeling/tainting nodes) and cannot scope `pods/eviction` to a single node's pods (it permits evicting any pod in any namespace; PodDisruptionBudgets bound the impact only where they exist). What you **cannot** do through `values.yaml` is change *which* cluster-scoped kinds the add-on grants (it's a fixed, Secret-free infra set). If you need a different set for a tier - say Nodes but not PVs - set that tier's `clusterScopedRead` to `false` and bind your own ClusterRole to the `radar:` group ([recipe below](#i-cant-see-nodes--pvs--storageclasses)). The `radar:*` groups exist independent of this chart, so your binding composes with the built-in namespaced one. ## Choosing a governance model There are two coherent ways to combine the tier defaults with group-based bindings. Both are fully supported; they differ in **where cluster access is governed**, and you can move from the first to the second at any time. **Mixed (the default): tiers for your operators, IdP groups for scoped access.** Your platform team holds Owner/Member and rides the default tier bindings - cluster-wide access through the upstream-maintained `admin`/`edit` roles, zero binding authoring. External or namespace-scoped users (vendor teams, contractors) get their tier binding disabled (`viewer: false`) and access exclusively through `radar:idp:` / `radar:user:` bindings you author. Each population sits on the mechanism built for it. The tradeoff: **two systems of record** - operator access is governed by Radar Cloud membership roles (visible in the Cloud UI and audit log), scoped access by your IdP groups and RBAC manifests. **Full-IdP: every human's cluster access flows through your IdP groups.** For organizations whose access reviews require a single source of truth, disable all three tier bindings and bind your own groups end to end: ```yaml theme={null} cloud: defaultRbac: owner: false member: false viewer: false ``` ```yaml theme={null} # Your platform-team group gets cluster-wide admin - same pattern as any # other group, just bound to a broad role. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: platform-team-admin roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: admin subjects: - kind: Group name: radar:idp: apiGroup: rbac.authorization.k8s.io ``` Now every grant - operator and vendor alike - is a binding referencing one of your IdP groups: one review process, one place to look, and offboarding is group removal in your IdP. In full-IdP mode: * **Cloud roles don't disappear - they govern the product, not the cluster.** Owner/Member/Viewer still control billing, member management, SSO, cluster lifecycle, and owner-only product surfaces: Radar's settings, its Argo CD and Kubecost connections, audit settings, agent updates, and the OCI chart sources Radar checks for Helm upgrades. The mental model: **Cloud role = control-plane permissions; Kubernetes bindings = cluster access.** The tier bindings are merely the *default* cluster access, and they're optional. * **The add-on grants ride the tier bindings.** Cluster-scoped read, `nodeOps`, and the Helm add-on roles bind to tier groups - with all tiers disabled, none of them render for people, and you own those grants in your own ClusterRoles (see [the recipe above](#i-cant-see-nodes--pvs--storageclasses) for the cluster-read set). * **Background and AI access stay separate.** Disabling human tier bindings leaves the chart's grants for [`radar:system`](#radar-cloud-background-identity) and [`radar:ai:reader`](#ai-investigation-access) in place. Review them separately, especially the background identity's cluster-wide Secret read. ### Full-IdP for new clusters: the organization setting Instead of editing chart values per cluster, an owner can make IdP groups the default for **new** clusters with the [Cluster access](/docs/cloud/sso#cluster-access-for-new-clusters) setting (`auth.oidc.clusterAccess` on a [self-managed hub](/docs/cloud/self-hosted/auth#cluster-access-for-new-clusters)). Fresh installs then add `cloud.defaultRbac.create=false`. Existing clusters are never changed; when you connect a cluster that already runs Radar, turn on **Already running Radar in this cluster?** in the connect wizard to keep its bindings. ### Switching an existing cluster to IdP groups Do it in this order, or everyone loses access until a binding exists: 1. **Bind your groups** in the cluster (`radar:idp:`, see [IdP group-based access](/docs/cloud/idp-groups#4-bind-the-groups-in-kubernetes-rbac)). 2. **Check what each group alone is allowed.** Checking in Radar doesn't prove it: the tier bindings still grant access until you remove them, and Kubernetes RBAC is additive, so a binding that names the wrong group looks fine. Ask Kubernetes about the group on its own: ```bash theme={null} kubectl auth can-i --list -n --as=radar-idp-check --as-group=radar:idp: ``` An empty list means the binding doesn't match. `kubectl` requires `--as` with `--as-group`; the user name is arbitrary. 3. **Upgrade the cluster to a Radar chart with the `radar:system` binding**, then remove the tier bindings: ```bash theme={null} helm repo update skyhook && helm upgrade radar skyhook/radar -n radar \ --reset-then-reuse-values \ --set cloud.defaultRbac.create=false ``` To undo, put the tier bindings back: ```bash theme={null} helm repo update skyhook && helm upgrade radar skyhook/radar -n radar \ --reset-then-reuse-values \ --set cloud.defaultRbac.create=true ``` Use the release name and namespace from your installation in these commands. ## Per-user bindings If you need finer control than role-level (e.g. specific user can edit deployments but only in `apps/*` namespaces), bind directly to the per-user group: ```yaml theme={null} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: alice-apps-edit namespace: apps-prod subjects: - kind: Group name: radar:user:user_01HF... # user_id from the audit log apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: edit apiGroup: rbac.authorization.k8s.io ``` Find a user's `user_id` in: * The audit log row metadata. * The user's PAT page (their own ID is shown at the top). * Owner's view of **Settings → Organization → Members** (hover the row). ## Per-org bindings Every request also carries `radar:org:` in the groups header. This is mostly useful for clusters that move between orgs - typically you wouldn't need to bind to it directly. But if you ever need "any user from this org can do X", that's the hook. ## Auditing what the in-cluster Radar did K8s itself logs impersonated requests in `audit.k8s.io` if you've enabled the audit policy on the apiserver. The user ID and groups are present in the audit log entry, so you can correlate "user X did Y" against the Cloud audit log (which shows control-plane actions) and the K8s audit log (which shows in-cluster actions). ## I can't see Nodes / PVs / StorageClasses If a tier can't see cluster-scoped resources, its bound role doesn't grant them. Most likely: * **That tier's `clusterScopedRead` is off** - set `cloud.defaultRbac.clusterScopedRead.: true` (the default). * **You're on an older chart** that predates the cluster-read add-on - upgrade, or grant the reads directly (below). To grant cluster-scoped read directly (works on any chart version), bind the tier group to a ClusterRole. Give it a distinct name - the chart already renders a ClusterRole named `radar-cluster-read`, so reusing that name would make `kubectl apply` overwrite the Helm-owned object. Use `radar-cluster-read-custom`: ```yaml theme={null} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: radar-cluster-read-custom rules: - 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"] - apiGroups: ["node.k8s.io"] resources: ["runtimeclasses"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: radar-cluster-read-custom roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: radar-cluster-read-custom subjects: - kind: Group name: radar:viewer # and/or radar:member, radar:owner apiGroup: rbac.authorization.k8s.io ``` ## Things to watch out for * **Default `admin` is broad within namespaces, but not cluster-wide.** It reads Secrets and mutates RBAC in every namespace, but as a namespace role it grants nothing cluster-scoped on its own - no Node read, no node drain (that's the opt-in `nodeOps` add-on), no PV/StorageClass. If you have non-admin owners, swap `radar:owner` to a custom ClusterRole. * **Secret read permission includes values.** The built-in `view` role denies Secrets. Granting `get/list` exposes their encoded `data` in Radar's generic resource UI and API; Kubernetes has no names-only permission. [MCP redaction](/docs/features/mcp#security) does not protect that separate surface. * **Built-in roles are aggregation targets.** `view`/`edit`/`admin` are auto-reconciled and accept `aggregate-to-*` labels, so third-party charts (metrics-server, kyverno, some operators) can widen what a tier effectively holds - the tier's scope drifts with cluster contents, not just with this chart. Audit with `kubectl auth can-i --list --as-group=radar:viewer --as=x` if you need certainty. * **Helm runs as you.** Helm install / upgrade / rollback / uninstall, and viewing a release's values and manifests, run with your own Kubernetes identity; your Cloud role doesn't gate them. Radar enables the Helm write actions in a namespace when you can create Secrets there (Helm stores each release as a Secret), and Kubernetes checks every resource the chart touches. With `rbac.helm=true` and the tier bindings on, the chart also binds the Helm add-on roles to the owner and member tiers; in Cloud mode it grants Radar's own ServiceAccount no write access for Helm. ## A user sees an empty cluster If a user can't list Pods or Deployments in any namespace, Radar shows a banner - **"You can't read any namespace in this cluster"** - with the groups the cluster sees for them, so they can tell an admin which group to bind. On a large cluster the banner can take a page reload to appear the first time. ## See also * [Authentication](/docs/configuration/authentication) - how proxy / OIDC mode work generally. * [Connecting a cluster](/docs/cloud/connect-cluster) - the chart values that enable cloud-mode. * [IdP group-based access](/docs/cloud/idp-groups) - where the `radar:idp:` subjects come from and how to make your IdP emit them. * [Organizations & roles](/docs/cloud/organizations) - the role definitions that drive these groups. # AI investigations Source: https://radarhq.io/docs/cloud/self-hosted/ai-agent Run hosted AI investigations on a self-managed control plane with your own Anthropic, Amazon Bedrock, or Google Vertex AI account. Hosted [AI investigations](/docs/features/diagnose) are available on a self-managed control plane. They are off by default. When you enable them, each investigation turn runs as a short-lived Kubernetes Job in a sandbox namespace, and the model is called through **your own** provider account. In the chart it is configured under `hub.agent`. ## How it runs * Each turn is a Job (pods labelled `app=diagnose-turn`) in the sandbox namespace. The pod has no mounted ServiceAccount token. * The agent reads your cluster through the control plane's MCP tunnel. Write tools are refused. See [AI investigation access](/docs/cloud/rbac#ai-investigation-access) for whose permissions apply. * The model credential is injected only into a sidecar broker, never into the agent container. Only the selected provider's broker is mounted, so the pod holds no credential for any other provider. * A NetworkPolicy allows DNS, the hub, Postgres, and outbound HTTPS on port 443, and denies all ingress. The HTTPS rule is port-based: it does not limit which destinations the pod reaches. For destination-level control, route the sandbox through an egress proxy or use a CNI policy that filters by FQDN. ## Requirements * **Radar 1.16.0 or later on connected clusters**, for both user-started and alert-triggered investigations. * **A model provider account** - one of: * **Anthropic** - an API key (`sk-ant-...`). Works from any cloud. * **Amazon Bedrock** - an IAM service-specific credential for `bedrock.amazonaws.com`, with model access enabled in the region you choose. * **Google Vertex AI** - a service-account key (JSON) with `roles/aiplatform.user` on the project, and the Claude model enabled in Model Garden. Only key-based auth is supported; workload identity is not. * **A CNI that enforces NetworkPolicy**, if you rely on the sandbox egress policy. Enforcement is CNI-dependent: kindnet ignores NetworkPolicy, and on EKS network policy support in the Amazon VPC CNI is off until you enable it. * **Egress from the sandbox namespace** to your provider over HTTPS (`api.anthropic.com`, `bedrock-runtime..amazonaws.com`, or for Vertex `oauth2.googleapis.com` plus the endpoint for `hub.agent.vertex.location`: `aiplatform.googleapis.com` for `global`, `aiplatform.us.rep.googleapis.com` for `us`, `aiplatform.eu.rep.googleapis.com` for `eu`, and `-aiplatform.googleapis.com` for a region such as `us-east5`) and a way to pull the sandbox image. ## What the chart creates With `hub.agent.enabled=true`, the chart renders these in the sandbox namespace: | Resource | Purpose | | - | - | | Namespace `-sandbox` | Where the per-turn Jobs run. Override with `hub.agent.sandbox.namespace`; set `hub.agent.sandbox.create=false` to use a namespace you already manage. | | Secret `-agent` | The model credential and `HUB_AGENT_DB_DSN`, the Postgres DSN as the sandbox pod resolves it. Not created when you use `hub.agent.credentials.existingSecret`. | | NetworkPolicy | Egress to DNS, 443, the hub, and Postgres (`hub.agent.sandbox.networkPolicy.postgresPort`, default `5432`). Add rules with `hub.agent.sandbox.networkPolicy.extraEgress`. | | Role + RoleBinding | Lets the hub's ServiceAccount create Jobs and list their pods in that namespace. | `` is the release name when it already contains `radar-hub`, otherwise `-radar-hub`. For the release `radar-hub` used throughout these docs, that is namespace `radar-hub-sandbox` and Secret `radar-hub-agent`. Helm won't install over a namespace it doesn't own, so a name collision fails the install instead of taking the namespace over. ## Provider values **Anthropic** | values.yaml | Default | Notes | | - | - | - | | `hub.agent.provider` | `anthropic` | | | `hub.agent.credentials.apiKey` | | Anthropic API key. | | `hub.agent.anthropic.model` | `claude-sonnet-4-6` | Bare model id. | **Amazon Bedrock** | values.yaml | Default | Notes | | - | - | - | | `hub.agent.provider` | | `bedrock` | | `hub.agent.credentials.apiKey` | | IAM service-specific credential for `bedrock.amazonaws.com`. | | `hub.agent.bedrock.region` | `us-east-1` | Region of the `bedrock-runtime` endpoint. | | `hub.agent.bedrock.model` | `us.anthropic.claude-sonnet-4-6` | Inference-profile id, not the bare model id. | **Google Vertex AI** | values.yaml | Default | Notes | | - | - | - | | `hub.agent.provider` | | `vertex` | | `hub.agent.credentials.serviceAccountJSON` | | The service-account key JSON. Pass it with `--set-file`. `apiKey` is refused under this provider. | | `hub.agent.vertex.project` | | GCP project id. Required, no default. | | `hub.agent.vertex.location` | `global` | `global` (widest model availability), `us`, `eu`, or a region such as `us-east5`. | | `hub.agent.vertex.model` | `claude-sonnet-4-6` | Bare model id. | `hub.agent.engineLabel` sets the engine name shown on the investigation panel (for example `Claude (via Agent SDK)`). Empty hides it. ### AI analysis in alerts Alert rules can attach an AI analysis to the issues they match. `hub.agent.alertAnalysis: true` is the install-wide switch for it, and it needs the engine: setting it with `hub.agent.enabled: false` fails the render rather than sitting in your values doing nothing. | values.yaml | Default | Notes | | - | - | - | | `hub.agent.alertAnalysis` | `false` | Lets alert rules attach an AI analysis. Requires `hub.agent.enabled: true`. | Three more gates sit outside the chart, all off until someone turns them on: * An organization owner consents. The disclosure appears the first time AI analysis is turned on for an alert rule, and **Settings → Organization → AI & MCP** shows the state afterwards. * An alert rule opts in - see [Alerts](/docs/cloud/alerts). * The hub process runs the alerts worker. It does by default; a replica running with it disabled analyses nothing. Analyses call the same model account as the investigations you start by hand, so the provider spend follows how often your rules fire. A self-managed Hub sets no monthly limit on investigations or model spend; the quotas and budget alerts on your provider account are the limit. ### The sandbox DSN The sandbox pod runs in another namespace, so it can't use the hub's own DSN when that DSN is a bare Service name. The chart needs one of: * **Bundled eval Postgres** - set `postgres.bundled.auth.password` explicitly, and the chart derives the cross-namespace DSN. A generated password can't be read back when the chart renders, so without an explicit password the install fails (see [Add the agent to an existing install](#add-the-agent-to-an-existing-install)). * **External Postgres** - set `hub.agent.credentials.podDSN` to a DSN whose host resolves and is reachable from the sandbox namespace. * **Your own Secret** - see below. ### Using your own Secret For sealed-secrets or external-secrets workflows, create a Secret **in the sandbox namespace** and set `hub.agent.credentials.existingSecret` to its name. Don't set `apiKey`, `serviceAccountJSON`, or `podDSN` alongside it; the chart refuses the combination. The Secret must carry: | Key | When | | - | - | | `HUB_AGENT_DB_DSN` | Always | | `HUB_AGENT_ANTHROPIC_API_KEY` | `provider: anthropic` | | `HUB_AGENT_BEDROCK_API_KEY` | `provider: bedrock` | | `HUB_AGENT_VERTEX_CREDENTIALS` | `provider: vertex` (the service-account key JSON) | A Secret can't be referenced across namespaces, so a copy in the hub's namespace doesn't work. ## Enable it on a new install Take your install command - the one from [Install](/docs/cloud/self-hosted/install), or the one generated in the web app under **Settings → Self-Managed** - and add the agent values. With the bundled eval Postgres, also pin its password: ```bash theme={null} helm install radar-hub skyhook/radar-hub \ --namespace radar-hub --create-namespace \ ... # publicURL, cookiePassword, license, break-glass, exposure --set-string postgres.bundled.auth.password="$(openssl rand -hex 24)" \ --set hub.agent.enabled=true \ --set hub.agent.provider=anthropic \ --set-file hub.agent.credentials.apiKey=./anthropic.key ``` Keep the credential in a file and pass it with `--set-file`, so it stays out of your shell history. Write the file without a trailing newline (`printf '%s' "$ANTHROPIC_API_KEY" > anthropic.key`) - `--set-file` copies the file byte for byte. For Bedrock or Vertex, swap the provider lines as shown in the next section. With external Postgres, drop the `postgres.bundled.auth.password` line and set `hub.agent.credentials.podDSN` instead. ## Add the agent to an existing install The command generated under **Settings → Self-Managed** doesn't set a Postgres password, so on first install the chart generated one and stored it in the `-postgres` Secret. Upgrading with only the agent values fails at render time with: ```text theme={null} Error: UPGRADE FAILED: execution error at (radar-hub/templates/secret.yaml:164:4): hub.agent with the bundled Postgres needs postgres.bundled.auth.password set explicitly ``` Pass the **existing** password back in. Don't pick a new one: changing `postgres.bundled.auth.password` doesn't rotate the password inside the database, so the hub and the sandbox would both lose access. **1. Save the current values** The upgrade below starts from this file, so everything you set at install time (license, break-glass admin, public URL, exposure) carries over: ```bash theme={null} (umask 077 && helm get values radar-hub -n radar-hub -o yaml > radar-hub-values.yaml) ``` > [!WARNING] > > This file holds every value you passed inline, including the cookie password, license, break-glass credentials, and model key. Keep it readable only by you, never commit it, and delete it (or move it into your secret store) once the upgrade succeeds. Check `helm history radar-hub -n radar-hub` first. If the latest revision is `failed`, the release's current values are those of the last successful revision, not the failed one. Add `--revision ` to read the values of the revision you actually intended (see [Troubleshooting](#troubleshooting)). **2. Read the generated Postgres password** Skip this step if you use external Postgres; set `hub.agent.credentials.podDSN` in the next step instead. ```bash theme={null} PGPW="$(kubectl -n radar-hub get secret radar-hub-postgres \ -o jsonpath='{.data.password}' | base64 -d)" ``` **3. Upgrade with the agent enabled** `-f radar-hub-values.yaml` carries your install values while the new chart's defaults, which the agent settings depend on, still apply. Plain `--reuse-values` would drop those defaults when you move to a newer chart. **Anthropic** ```bash theme={null} helm repo update skyhook helm upgrade radar-hub skyhook/radar-hub -n radar-hub \ -f radar-hub-values.yaml \ --set-string postgres.bundled.auth.password="$PGPW" \ --set hub.agent.enabled=true \ --set hub.agent.provider=anthropic \ --set-file hub.agent.credentials.apiKey=./anthropic.key ``` **Amazon Bedrock** ```bash theme={null} helm repo update skyhook helm upgrade radar-hub skyhook/radar-hub -n radar-hub \ -f radar-hub-values.yaml \ --set-string postgres.bundled.auth.password="$PGPW" \ --set hub.agent.enabled=true \ --set hub.agent.provider=bedrock \ --set hub.agent.bedrock.region=us-east-1 \ --set hub.agent.bedrock.model=us.anthropic.claude-sonnet-4-6 \ --set-file hub.agent.credentials.apiKey=./bedrock.key ``` **Google Vertex AI** ```bash theme={null} helm repo update skyhook helm upgrade radar-hub skyhook/radar-hub -n radar-hub \ -f radar-hub-values.yaml \ --set-string postgres.bundled.auth.password="$PGPW" \ --set hub.agent.enabled=true \ --set hub.agent.provider=vertex \ --set hub.agent.vertex.project=my-gcp-project \ --set hub.agent.vertex.location=global \ --set-file hub.agent.credentials.serviceAccountJSON=./sa-key.json ``` If the web Service is a `LoadBalancer` that hasn't received an address, leave out `--wait`; see [Troubleshooting](#troubleshooting). **4. Verify** Follow [Verify](#verify) below. ## Verify 1. The hub logs the engine at startup: ```bash theme={null} kubectl -n radar-hub logs deploy/radar-hub-hub -c hub | grep diagnose ``` Expect `radar-hub: diagnose Job engine enabled` with `model_provider`, `model`, `namespace`, and `image`. If it says `no diagnose engine configured`, `hub.agent.enabled` isn't set on the running release. 2. The sandbox resources exist: ```bash theme={null} kubectl get ns radar-hub-sandbox kubectl -n radar-hub-sandbox get secret,networkpolicy,role,rolebinding ``` 3. Start an investigation from a connected cluster in the web app. The first time anyone in your organization starts an investigation, a disclosure card describes what leaves the cluster; one acceptance in the web app covers the whole organization. Until then, investigations started through an API token are refused; a token cannot accept the disclosure itself. Each turn creates a Job in the sandbox namespace: ```bash theme={null} kubectl -n radar-hub-sandbox get jobs,pods -l app=diagnose-turn -w ``` ## Troubleshooting ### Upgrade fails with "needs postgres.bundled.auth.password set explicitly" The release uses the bundled eval Postgres with a generated password. Follow [Add the agent to an existing install](#add-the-agent-to-an-existing-install) and pass the existing password back in. ### Upgrade fails with "podDSN is required with an external database" The chart can't read your Postgres Secret to rewrite its host. Set `hub.agent.credentials.podDSN` to a DSN reachable from the sandbox namespace, or put the whole credential set in `hub.agent.credentials.existingSecret`. ### `helm upgrade --wait` ends with "context deadline exceeded" With `service.web.type=LoadBalancer`, `--wait` also waits for the load balancer to receive an address. If it never does, Helm times out and marks the revision `failed`, even though the manifests were applied and the pods may be healthy. Check the Service events (`kubectl -n radar-hub describe svc radar-hub-web`), fix the load balancer, and run the upgrade again, or leave out `--wait`. > [!WARNING] > > After a `failed` revision, Helm treats the last **successful** revision as the release's current state. A later `helm upgrade --reuse-values` (or `--reset-then-reuse-values`) builds on that older revision's values, so settings you added in the failed upgrade, such as `hub.agent`, are silently dropped from the new revision. The running pods can keep working for a while, which hides the problem until the next change. > > Re-run the upgrade from the values you meant to apply: > > ```bash theme={null} > helm history radar-hub -n radar-hub > (umask 077 && helm get values radar-hub -n radar-hub --revision -o yaml > radar-hub-values.yaml) > helm upgrade radar-hub skyhook/radar-hub -n radar-hub -f radar-hub-values.yaml > ``` On EKS Auto Mode, `Failed build model due to unable to resolve at least one subnet` means no subnet is tagged for load balancers. Tag the public subnets with `kubernetes.io/role/elb=1` (or the private ones with `kubernetes.io/role/internal-elb=1` for an internal load balancer). Also check the load balancer scheme: Auto Mode load balancers are internal unless it carries `service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing`, and the chart doesn't expose annotations on the web Service. For anything beyond a pilot, publish through an [Ingress or Gateway](/docs/cloud/self-hosted/configuration#exposing-the-hub-publicly) instead. ### A turn never starts or never reports * **Image pull errors** on the Job pod. Sandbox Jobs use the `default` ServiceAccount in the sandbox namespace; the chart's top-level `imagePullSecrets` doesn't apply to them. For a private mirror, set `image.agentSandbox.repository`, pre-create the sandbox namespace with a pull Secret attached to its `default` ServiceAccount, and set `hub.agent.sandbox.create=false`. * **Model calls time out.** The sandbox needs HTTPS egress to your provider. Behind an egress proxy or a private endpoint, add rules with `hub.agent.sandbox.networkPolicy.extraEgress`. * **Database connection errors** in the Job pod. The DSN in `HUB_AGENT_DB_DSN` must resolve from the sandbox namespace, and the NetworkPolicy must allow the database port (`hub.agent.sandbox.networkPolicy.postgresPort`). * **Vertex permission errors.** The service account needs `roles/aiplatform.user` on `hub.agent.vertex.project`, and the model must be enabled for that project in Model Garden. A newly granted role can keep returning `Permission 'aiplatform.endpoints.predict' denied` for a few minutes while IAM propagates; retry before changing anything. # Architecture Source: https://radarhq.io/docs/cloud/self-hosted/architecture What runs, what connects to what, and what leaves your network. ## What runs ![Radar Cloud Self-Managed architecture: your Ingress or Gateway API HTTPRoute routes to radar-hub-web, which serves the web app and reverse-proxies to the radar-hub Go API; radar-hub connects to Postgres and terminates the in-cluster Radar tunnels.](https://mintcdn.com/skyhook/MO3tZsRgyb2MPuuB/images/cloud/self-hosted/architecture.png?fit=max\&auto=format\&n=MO3tZsRgyb2MPuuB\&q=85\&s=fb5061f6ece0c3c8551c9296c8f5e89d) Three workloads in your cluster: the web app, the control plane, and Postgres (bundled for evaluation, or your own). Every Radar agent in your fleet connects outbound to the control plane; the control plane never connects into your clusters. Run one replica of the control plane. If you need more than one, contact support. ## What connects to what | From | To | When | | - | - | - | | Browsers | the web app, HTTPS on your public URL | always | | Radar agents in your clusters | the control plane, outbound WebSocket on the same URL | always | | Control plane | Postgres | always | | Control plane | Radar Cloud, HTTPS | once a day, cloud-managed deployments only: license renewal, carrying the license and the cluster count | | Control plane | GitHub, HTTPS | hourly, to show the latest Radar release; off when air-gapped or when `hub.latestRadarVersion` is set | | Control plane | your identity provider | only if OIDC is configured | | Control plane | your AI model provider | only if AI investigations are enabled | Terminate TLS at your Ingress or Gateway. The agents refuse a plaintext tunnel to anything but localhost. ## What protects each connection | Connection | Protection | | - | - | | Browser | A session cookie. Signing out or removing a member ends the session on the server. | | Agent | A per-cluster token. Revoking it from the web app cuts that one cluster's tunnel. | | Postgres | Your database credentials. Use `sslmode=require` or stricter for a managed database. | | Identity provider | TLS against the provider's certificate. | ## What leaves your network A cloud-managed deployment sends licensing and deployment metadata to Skyhook. Automatic license renewal sends the license and Hub version. A separate sync reports the hub address and connection settings, cluster count, and cluster migration IDs and status. These requests do not include workload contents. An air-gapped deployment sends nothing of its own. Skyhook does not have your cluster data, your users' identities, your audit log, or your tunnel traffic. Those stay in your cluster and your Postgres. The exception is anything you connect yourself: an [AI model provider](/docs/cloud/self-hosted/ai-agent) receives what an investigation reads, and your identity provider sees sign-ins. # Authentication Source: https://radarhq.io/docs/cloud/self-hosted/auth OIDC + the always-available break-glass admin. WorkOS is not used in Self-Managed - your IdP is the source of truth. Self-Managed has two coexisting auth paths: 1. **OIDC** - federated login against your own IdP (Okta, Auth0, Google Workspace, Azure AD, etc.). The primary path for real users. 2. **Break-glass admin** - a single email + password configured via env vars, always available regardless of OIDC state. Permanent escape hatch, not a stopgap. WorkOS is never in the loop: authentication goes straight from your Hub to the IdP you configure. The Cloud pages on [SSO](/docs/cloud/sso), domains and [IdP groups](/docs/cloud/idp-groups) describe the hosted service and do not apply here; [Sign-in and access](/docs/cloud/access-overview) explains the split. ## Break-glass admin ### Why it's permanent The break-glass admin exists for two scenarios: * **Initial bootstrap** - you need to log in BEFORE OIDC has been configured, to mint your IdP credentials and run a setup wizard. * **OIDC misconfiguration** - your IdP's discovery endpoint went down, your IdP's clock skewed past JWT expiry, somebody rotated client secrets. The break-glass account lets the on-call engineer log in without rolling back the deployment. We recommend leaving it configured permanently. One account, well-protected by a strong password (or pre-hashed bcrypt) in your secret store. ### Configuring it Two ways to set the password: ```bash theme={null} # Plaintext - hashed at boot, bcrypt cost 12, original discarded. helm install ... \ --set auth.breakGlass.email=admin@acme.example \ --set auth.breakGlass.password='' ``` ```bash theme={null} # Pre-hashed - preferred for production. Generate the hash anywhere with bcrypt. htpasswd -bnBC 12 "" '' | tr -d ':\n' # → $2y$12$XYZ... helm install ... \ --set auth.breakGlass.email=admin@acme.example \ --set 'auth.breakGlass.passwordHash=$2y$12$XYZ...' ``` Or via an existing Secret (see [Configuration → Skipping the chart's Secret](/docs/cloud/self-hosted/configuration#skipping-the-charts-secret)). ### Signing in POST to `/api/auth/break-glass/login`: ```bash theme={null} curl -i https://radar.acme.example/api/auth/break-glass/login \ -H 'Content-Type: application/json' \ -d '{"email":"admin@acme.example","password":""}' ``` Or use the web app's login page - it renders a "Sign in with email + password" panel when break-glass is configured. ### Rotating Bump the Secret + bounce the Pod: ```bash theme={null} helm upgrade radar-hub skyhook/radar-hub \ --namespace radar-hub \ --reset-then-reuse-values \ --set 'auth.breakGlass.passwordHash=' kubectl -n radar-hub rollout restart deploy/radar-hub-hub ``` Changing the password signs out every existing break-glass session: each session records a keyed hash of the credential it signed in with, and stops working when the configured credential changes. Restarting the hub with the **same** password keeps sessions, including when you set it in plaintext (re-hashed at every boot). ## OIDC ### Endpoints The control plane mounts: * `GET /api/auth/oidc/login` - top-level navigation kicks off the IdP redirect. * `GET /api/auth/oidc/callback` - your IdP redirects here on success. You register the callback URL with your IdP. It's the only URL that needs to match exactly - case-sensitive, no path normalization. ### IdP-specific recipes If your clusters already trust your IdP and you want [user tokens](/docs/cloud/self-hosted/user-tokens), use the client they trust rather than creating a new one. **Okta** 1. In Okta admin, create a new **OIDC - Web Application**. 2. Sign-in redirect URI: `https://radar.acme.example/api/auth/oidc/callback`. 3. Sign-out redirect URI: `https://radar.acme.example` (optional; only used if your IdP advertises `end_session_endpoint`). 4. Grant types: Authorization Code + Refresh Token (the refresh token is used only with [`useRefreshToken`](#keeping-groups-current-with-refresh-tokens)). ```yaml theme={null} auth: oidc: issuer: https://acme.okta.com clientID: 0oa<...> clientSecret: callbackURL: https://radar.acme.example/api/auth/oidc/callback defaultRole: member # groupsClaim defaults to "groups" and is shown here only to be # explicit; forwarded groups become radar:idp: subjects for # Kubernetes RBAC. Okta gates the claim behind a scope on a CUSTOM # authorization server - add the scope ONLY if your server defines # one by that name, since requesting an undefined scope fails the # whole sign-in. groupsClaim: groups # Only if your authorization server defines a scope by this name. # Otherwise omit the line - an undefined scope fails the sign-in. scopes: ["groups"] ``` **Auth0** 1. Auth0 dashboard → **Applications** → Create → **Regular Web Application**. 2. Allowed Callback URLs: `https://radar.acme.example/api/auth/oidc/callback`. 3. Allowed Logout URLs: `https://radar.acme.example`. ```yaml theme={null} auth: oidc: issuer: https://acme.eu.auth0.com clientID: clientSecret: callbackURL: https://radar.acme.example/api/auth/oidc/callback defaultRole: member ``` **Google Workspace** 1. Google Cloud Console → **APIs & Services** → **Credentials** → **Create Credentials** → OAuth client ID → Web application. 2. Authorized redirect URIs: `https://radar.acme.example/api/auth/oidc/callback`. 3. Google's discovery doc lives at `https://accounts.google.com/.well-known/openid-configuration`. ```yaml theme={null} auth: oidc: issuer: https://accounts.google.com clientID: .apps.googleusercontent.com clientSecret: callbackURL: https://radar.acme.example/api/auth/oidc/callback # Use the OAuth `hd` claim for domain restriction at the IdP layer. defaultRole: member # Google emits no groups. Turn passthrough off, so new clusters keep # the hub-role bindings (cluster access resolves to roles). groupsClaim: "" ``` **Microsoft Entra ID** 1. Azure Portal → **Microsoft Entra ID** → **App registrations** → New. 2. Redirect URI: `https://radar.acme.example/api/auth/oidc/callback` (type: Web). 3. Issue a client secret under **Certificates & secrets**. ```yaml theme={null} auth: oidc: issuer: https://login.microsoftonline.com//v2.0 clientID: clientSecret: callbackURL: https://radar.acme.example/api/auth/oidc/callback defaultRole: member # groupsClaim defaults to "groups" and is shown here only to be # explicit. Emit the groups claim under the app registration's # Token configuration. Do NOT add a "groups" scope - Entra rejects # the unknown scope with AADSTS70011 and sign-in fails. groupsClaim: groups ``` ### Configuring OIDC from the web app The hub ships a **Settings → SSO (Single Sign-On)** page. Enter your issuer, clientID, and clientSecret and it generates the exact `kubectl create secret generic radar-hub-oidc --from-literal=client-secret=...` plus the `helm upgrade ... --set-string auth.oidc.issuer=... --set-string auth.oidc.clientID=... --set auth.oidc.clientSecretExistingSecret=radar-hub-oidc` command for you, and shows the redirect URI to register at your IdP. It's a convenience wrapper - the Helm values remain the source of truth. ### Roles Radar has three roles: * `owner` - full admin (mint cluster tokens, manage members, view audit log). * `member` - read + cluster operations (the default). * `viewer` - read-only. The IdP does authentication (who you are); the hub does authorization (your **role**) internally. Roles are never derived from IdP groups - see [IdP group passthrough](#idp-group-passthrough) below for the separate feature that forwards groups to your clusters for Kubernetes RBAC. Role assignment: 1. The first user to sign in - via OIDC or break-glass - bootstraps the org and becomes `owner`. 2. Every subsequent new user joins as `auth.oidc.defaultRole` (default `member`). 3. An owner promotes or demotes anyone else from **Settings → Organization → Members** in the web app. Roles are stored in the hub, not derived from the ID token, so a user keeps their role across logins regardless of any IdP group change. ### IdP group passthrough Roles govern what a user can do **in the hub**. They say nothing about what that user can do **inside a cluster** - that is Kubernetes RBAC, and you author it. Group passthrough lets you bind the groups you already manage in your IdP. When enabled, each of the signed-in user's IdP group values is forwarded to your connected clusters as a `radar:idp:` subject, so a RoleBinding you already maintain keeps working instead of being re-authored per user: ```yaml theme={null} subjects: - kind: Group name: radar:idp: apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: edit apiGroup: rbac.authorization.k8s.io ``` See [IdP group-based access](/docs/cloud/idp-groups) for the full binding recipes and the identity the cluster receives - the subject vocabulary is identical to Cloud. #### The two values Passthrough is on by default; these control the details, and they answer different questions: ```yaml theme={null} auth: oidc: # WHERE the groups are in the ID token. Defaults to "groups". # Set it empty to turn passthrough off. groupsClaim: "groups" # HOW to ask the IdP for them. Provider-specific - see the table. scopes: [] ``` `groupsClaim` is a claim **name**, not a boolean, because providers disagree on where groups land. Setting it does **not** change the scopes requested, because providers also disagree on whether a scope is how you ask: | Provider | `groupsClaim` | `scopes` | Notes | | - | - | - | - | | Okta | `groups` | `["groups"]`, **if** your authorization server defines a `groups` scope | Add a groups claim to the app's ID token. Okta gates the claim behind the scope on a custom authorization server. Confirm the scope exists before adding it - requesting one the server does not define fails the whole sign-in. | | Keycloak | `groups` | depends on where the mapper sits - see below | Needs a group-membership mapper. Values arrive **full-path with a leading slash**. | | Entra ID | `groups` | *(leave empty)* | Emits groups from the app's **Token configuration**, and **rejects** an unknown `groups` scope with `AADSTS70011`. Adding it breaks sign-in. | | ADFS | often `group`, or a URI-style claim type such as `http://schemas.xmlsoap.org/claims/Group` | depends on your rule set | Inspect a token to confirm the claim name. | | Google Workspace | `""` (turn passthrough off) | *(leave empty)* | Google does not emit group claims and rejects the scope. Leaving the default `groups` makes [cluster access](#cluster-access-for-new-clusters) default to `idp`, so new clusters would grant nobody access. | Only the Keycloak row was checked against a running instance. The Okta, Entra, ADFS and Google rows follow each vendor's own documentation, so confirm against a token from your tenant before relying on them. > [!WARNING] > > A wrong **scope** is fatal, unlike a wrong claim name. Most providers reject a scope they do not > recognise and fail the entire sign-in rather than simply omitting the groups. Add a scope only > when your provider actually gates the claim behind one, and check that the name exists first. **Keycloak specifically.** Where the group-membership mapper lives decides whether you need a scope at all: * mapper on the client's **dedicated** scope - the claim is always present. Leave `scopes: []`. * mapper on an **optional** client scope named `groups` - set `scopes: ["groups"]`. Setting `scopes: ["groups"]` against a client that has no scope by that name does not merely miss the groups: Keycloak refuses the sign-in with `invalid_scope`. If group passthrough was working and you added the scope on our advice, remove it again. Keycloak also emits group values as a **full path with a leading slash**, so the subject is `radar:idp:/platform-admins`, and a nested group is `radar:idp:/parent/child`. A RoleBinding naming `radar:idp:platform-admins` silently matches nothing. On by default, because `groups` is the claim Okta, Keycloak and Entra all emit. A claim your IdP does not send doesn't break sign-in - no group subjects are forwarded - and nothing reaches a cluster until you write a RoleBinding naming one of these subjects. It does matter for [cluster access](#cluster-access-for-new-clusters), which defaults to `idp` while this is set, so check that groups arrive before installing clusters. Enabling it does not change what is requested from your IdP, so it cannot affect sign-in or token size. It only decides whether groups already present in the ID token are read and forwarded. Set `groupsClaim: ""` to turn it off. > [!NOTE] > > A wrong claim name is not fatal. You get no group subjects and sign-in is unaffected - which also means a silent no-op is the failure mode. If bindings do not match, check the claim name first. #### Which group values actually arrive A value is forwarded only if it can be represented verbatim in the request header your cluster receives, because a value that cannot round-trip cannot be bound accurately in RBAC. Values are **dropped, not escaped**: A groups claim may arrive as an array or as a single string. A string is treated as **one group**, not as a list to split - providers emit a string when the user belongs to exactly one group, which is what ADFS does. This matches how Kubernetes reads the same claim. One consequence worth knowing for Active Directory: a distinguished name such as `CN=Admins,OU=Corp,DC=example,DC=com` is one value containing commas, so it is dropped by the rule below rather than broken into parts. Emit a group ID or name instead of a DN. * printable ASCII only - no whitespace, no comma, no control or non-ASCII bytes * at most 128 bytes per value, measured on the value **as your IdP emits it**, including any prefix the provider adds - Keycloak's leading slash counts, so a Keycloak group name has 127 * at most 64 groups per user This matters for Active Directory: a display name like `Domain Admins` contains a space and is dropped silently. **Emit stable group IDs** - Entra object IDs, Okta group IDs - rather than display names. They are also immune to a rename, which would otherwise silently repoint or break every binding that names the old value. Groups are stored with the session on the server, not in the cookie, so the number of groups a user belongs to does not affect sign-in. A user in more than 64 groups signs in normally; the groups past the 64th are not forwarded. If that matters, emit fewer groups into the token - Entra's **Groups assigned to the application** instead of all groups is the usual answer. #### How long group access lasts Groups come from the ID token and are asserted only until **that token** expires. What happens then depends on [refresh tokens](#keeping-groups-current-with-refresh-tokens), which the chart turns on by default: * **With a refresh token**, the hub gets a new ID token before the old one expires, so group changes in your IdP reach clusters within one ID-token lifetime and the session lasts its full [lifetime](#sessions). * **Without one** (turned off, or your IdP issued none), a session that carries IdP groups **ends when its ID token expires** and the user signs in again, as Grafana and oauth2-proxy do. Otherwise the user would look signed in while every cluster granted through a group went empty. The ID token lifetime is your IdP's: 5 minutes on a default Keycloak, typically an hour on Okta or Entra. Sessions without groups keep the full lifetime, and the hub logs a warning once when it shortens sessions. Either way, removing a user from a group in your IdP takes effect within one ID-token lifetime. Hub **roles** are unaffected. #### Groups for AI agents over OAuth An AI agent connected over [OAuth](/docs/cloud/connected-apps) presents an access token issued by your IdP. The hub reads the same `groupsClaim` from that **access token** and forwards the groups exactly as it does for a browser session, so the agent's cluster access follows the same RoleBindings as its user. Some IdPs put the groups claim only in the ID token by default. If an agent's requests carry no `radar:idp:` subjects while the same user's browser session does, add the claim to the access token too (in Keycloak, **Add to access token** on the group-membership mapper; in Okta, a groups claim of type **Access Token** on the authorization server). Personal access tokens never carry IdP groups. #### Not available on a hub that also has WorkOS configured Passthrough is a self-managed feature. If a hub is wired with both `RADAR_HUB_OIDC_*` and WorkOS credentials, OIDC-session groups are not forwarded - group scoping there is per-WorkOS-organization - and the hub logs a warning at boot saying so. ### Sessions OIDC and break-glass sessions are stored in the hub's Postgres. The browser cookie carries only a random session id, so it stays small and a copied cookie is worth nothing once the session ends on the server. A session ends when any of these happens: * it reaches its **absolute lifetime** - 7 days from sign-in by default * it goes **unused** for 24 hours by default (activity is recorded at most every 10 minutes) * the user **signs out** * an owner **removes the user** from the org * the **break-glass password changes** (break-glass sessions only) * with [refresh tokens](#keeping-groups-current-with-refresh-tokens) on, the IdP **rejects the refresh** (OIDC sessions only) * without a refresh token, the **ID token expires** and the session carries IdP groups (see [how long group access lasts](#how-long-group-access-lasts)) The user then sees the login page and signs in again. Both limits are chart values: ```yaml theme={null} auth: session: maxLifetime: "168h" # empty = 168h (7 days) maxInactive: "24h" # empty = 24h; must be at least 20m ``` Use Go duration syntax: `168h`, not `7d` (the chart's schema rejects `7d`). A `maxInactive` below `20m` stops the hub at boot with an error naming the setting. ### Keeping groups current with refresh tokens With `useRefreshToken` (on by default in the chart), the hub keeps the IdP's refresh token (encrypted, on the server) and gets a new ID token before the old one expires: ```yaml theme={null} auth: oidc: useRefreshToken: true # chart default # Okta and Entra return a refresh token only when asked: scopes: ["offline_access"] ``` An install upgraded from an older chart with `--reuse-values` keeps its old `false`. * Group changes in your IdP reach clusters within one ID-token lifetime, and group access lasts for the whole session. * A user disabled in the IdP, or whose IdP session was revoked, is signed out of the hub at the next refresh. * If the IdP is unreachable or returns 429/5xx, the request fails with 503 and the session is kept, so an IdP outage does not sign everyone out. `useRefreshToken` does not change the scopes requested. **Okta** and **Entra** need `offline_access` in `scopes`; **Keycloak** returns a refresh token without it. The IdP client needs the Refresh Token grant enabled. If the IdP returns no refresh token, the setting has no effect and a session with groups ends at the ID token's expiry, as with it off. ### Cluster access for new clusters `auth.oidc.clusterAccess` decides how clusters installed from the hub's connect page grant Kubernetes access: ```yaml theme={null} auth: oidc: clusterAccess: "" # "" (default), roles, idp, or token ``` | Value | New installs | | - | - | | `roles` | Keep the chart's default bindings: owners get `admin`, members `edit`, viewers `view`. | | `idp` | Add `cloud.defaultRbac.create=false`: only the bindings you write in Kubernetes RBAC grant access (usually to `radar:idp:`). The connect page shows a starter binding to apply. | | `token` | Add `auth.kubernetesCredentials=user-token` and `cloud.defaultRbac.create=false`: Radar calls the Kubernetes API with each person's own ID token, so your RBAC on their OIDC username and groups decides access. Needs API servers that trust your IdP. See [User tokens](/docs/cloud/self-hosted/user-tokens). | | `""` | `roles`. | * `idp` needs group passthrough. The chart refuses `idp` without `auth.oidc.issuer` or `groupsClaim`, and the hub refuses to start when passthrough can't work (for example, WorkOS is also configured), rather than install clusters nobody can see. * Existing clusters are never changed, and only fresh installs get `cloud.defaultRbac.create=false`. When you connect a cluster that already runs Radar, turn on **Already running Radar in this cluster?** in the connect wizard to keep its bindings; the default fresh-install command removes them. **Settings → Organization → SSO** shows the mode read-only, and, in `idp` mode, how to switch an existing cluster and which connected clusters run a Radar too old for it. See [Switching an existing cluster to IdP groups](/docs/cloud/rbac#switching-an-existing-cluster-to-idp-groups). ### Offboarding * **Removed in the IdP.** With refresh tokens, they are signed out at the next refresh. Without them, a session that carries IdP groups ends at the ID token's expiry; one without groups lasts until it expires (at most 24 hours unused or 7 days in total, by default) or they sign out. * **Removed in the hub.** Removing the user under **Settings → Organization → Members** ends their sessions immediately. A user removed only in the hub **joins again** the next time they sign in through the IdP, as a new member with the default role. To keep someone out, remove or unassign them in your IdP as well. ### Sign-out The web app's sign-out button calls `POST /api/auth/signout`, which ends the session on the server and clears the cookie. When the IdP advertises `end_session_endpoint` in discovery, it also returns a URL to the IdP's logout page carrying the stored ID token as `id_token_hint`, which lets the IdP identify the session to end (Keycloak, for example, then skips its logout confirmation page). Most IdPs advertise this; Google does not. ## Personal Access Tokens PATs (`rhp_*`) work identically to Cloud: * Mint via Settings → Access tokens (logged-in user only mints their own). * Pin to a single org at mint time. * Used as `Authorization: Bearer rhp_...` for MCP, `skyhook-cli`, and any script that can't carry a browser cookie. The PAT path is decoupled from OIDC and break-glass - tokens validate against a SHA-256 hash in Postgres, no upstream IdP roundtrip per request. # Backups Source: https://radarhq.io/docs/cloud/self-hosted/backups What's recoverable from K8s state alone, and what needs Postgres backups. ## The TL;DR Back up Postgres. The control plane itself is stateless. The bundled eval Postgres (on by default) has no backups - use it for pilots only; back a production install with your own Postgres via `postgres.external.*`. ## What lives where | Data | Location | Recoverable from K8s? | | - | - | - | | Cluster registrations (id, name, token hash, status) | Postgres | ❌ | | Org members + roles | Postgres | ❌ | | Audit log | Postgres | ❌ | | Personal Access Token hashes | Postgres | ❌ | | Notification destinations | Postgres | ❌ | | Inbox (in-app notifications) | Postgres | ❌ | | Sign-in sessions | Postgres | ❌ | | Triage decisions and alert state | Postgres | ❌ | | AI investigation transcripts (if enabled) | Postgres | ❌ | | Tunnel sessions to in-cluster Radar | In-memory on the `radar-hub` Pod | ✅ - reconnect on Pod restart | | Cluster-side resource state (Pods, Deployments, etc.) | The customer's own kube-apiserver | ✅ - source of truth remains in your cluster | Live resources, logs, and terminal I/O pass through the Hub on demand. Cloud workflow records and enabled AI investigation transcripts are retained in Postgres, so back up the whole database. The official chart leaves central timeline retention unconfigured by default; if you configure it separately, back up that backend too. ## Backup recipe (managed Postgres) Use whatever your provider offers - RDS automated backups, Cloud SQL exports, Azure Flexible Server PITR. Set retention to your compliance requirement. ## Backup recipe (CloudNativePG) ```yaml theme={null} # In the same namespace as the CNPG Cluster apiVersion: postgresql.cnpg.io/v1 kind: ScheduledBackup metadata: name: radar-hub-pg-daily spec: schedule: "0 2 * * *" # 02:00 UTC daily cluster: name: radar-hub-pg immediate: false ``` CNPG handles the rest - snapshots land in the object store you configured for the cluster. ## Backup recipe (hand-rolled `pg_dump`) Cron'd from anywhere with network access to the DB: ```bash theme={null} PGPASSWORD= pg_dump -h -U radar -Fc radar_hub > /backups/radar-hub-$(date +%Y%m%d).dump ``` Restore: ```bash theme={null} # 1. Stop the control plane so writes don't race. kubectl -n radar-hub scale deploy/radar-hub-hub --replicas=0 # 2. Drop + recreate the database (or point a new install at a fresh DB). PGPASSWORD= psql -h -U radar -c "DROP DATABASE radar_hub" -d postgres PGPASSWORD= psql -h -U radar -c "CREATE DATABASE radar_hub" -d postgres # 3. Restore. PGPASSWORD= pg_restore -h -U radar -d radar_hub /backups/radar-hub-20260510.dump # 4. Bring it back up. kubectl -n radar-hub scale deploy/radar-hub-hub --replicas=1 ``` ## What you DON'T need to back up * `helm get values radar-hub --namespace radar-hub` - check this into your IaC repo and you can re-derive the install. (Strip secrets first - those should already be in your Secret manager.) * The Helm chart itself - we publish it on a versioned repo; pin the `appVersion` in your IaC and `helm upgrade` re-creates everything. * Container images - re-pulled from `ghcr.io` on every Pod start. ## Restore drill (recommended quarterly) A backup you haven't tested isn't a backup. 1. Spin up a side-by-side install in a separate namespace pointed at a fresh empty DB. 2. Restore your latest backup into that DB. 3. Sign in as the break-glass admin. Confirm the cluster list, member roster, and audit log look correct. 4. Tear down. Net result: confidence + a dated record that the procedure works. ## What's NOT backed up by Skyhook In a self-managed install Skyhook has no access to your data. Your Postgres backups, your problem. If you're running the hosted Cloud at `app.radarhq.io`, that's our problem - see [Cloud Operations](/docs/cloud/billing) for the SaaS DR posture. Self-Managed explicitly delegates this to you. # Configuration reference Source: https://radarhq.io/docs/cloud/self-hosted/configuration Every value the chart exposes and the env var it maps to in the running control plane. The chart's `values.yaml` is the source of truth for every setting; this page mirrors it and adds the env var each value lands as inside the `radar-hub` container. ## Required | values.yaml | Env var | Notes | | - | - | - | | `hub.publicURL` | `RADAR_HUB_PUBLIC_URL`, `HUB_FRONTEND_URL`, `HUB_ALLOWED_ORIGINS` | The single public URL for the web app + tunnel. **Must be `https://`** - see below. No trailing slash. Required - the hub fails fast at boot if `RADAR_HUB_PUBLIC_URL` is empty in self-managed mode. | | `hub.cookiePassword` (or `cookiePasswordExistingSecret`) | `HUB_COOKIE_PASSWORD` | 32+ bytes. `openssl rand -base64 48`. | | Auth: `auth.breakGlass.email` + `password` (or `passwordHash`) | `RADAR_HUB_BOOTSTRAP_ADMIN_EMAIL`, `RADAR_HUB_BOOTSTRAP_ADMIN_PASSWORD`, `RADAR_HUB_BOOTSTRAP_ADMIN_PASSWORD_HASH` | Always-available bootstrap. Permanent, not a stopgap. | Postgres is not in this table because the chart ships a bundled eval Postgres that runs by default - see [Postgres](#postgres) below. A license is required to use the hub, but the chart installs without it (helm won't fail) - see [License](#license). > [!WARNING] > > **`hub.publicURL` must use `https://`.** An `http://` URL installs cleanly and passes readiness, > but nothing can sign in and no cluster can connect: > > * The hub's OIDC state cookie uses the `__Host-` prefix, which requires the `Secure` attribute. > A `Secure` cookie is never sent back over plain HTTP, so the callback cannot find the state it > needs. Every sign-in bounces to `/login?error=session_expired`, and the hub logs > `OIDC callback: state cookie missing` - which does not look like a TLS problem. > * In-cluster Radar refuses a `ws://` tunnel to any non-loopback host: > `cloud WebSocket URL must use wss:// unless the host is localhost or a loopback address`. > > Terminate TLS at your Ingress, or use the chart's self-signed option > (`web.tls.selfSigned=true`) for a trial. The connect command the web app generates then > sets `cloud.insecureSkipVerify=true` for the agent. To use the Radar CLI against a > self-signed hub, trust its certificate on your workstation first: the certificate names > your public host, so once trusted it verifies. The chart does not reject an `http://` > value, so this is on you to get right. > **Why are the value keys still under `hub.*` and the env vars `HUB_*` / `RADAR_HUB_*`?** `radar-hub` is the actual Go binary's name - these are real identifiers, not user-facing prose. The chart key `hub.publicURL` configures the running `radar-hub` Pod; the binary reads it through `RADAR_HUB_PUBLIC_URL`, `HUB_FRONTEND_URL`, and `HUB_ALLOWED_ORIGINS` for the separate canonical-origin, redirect, and CORS checks. Renaming would break existing dotfiles and runbooks. ## Postgres The hub reads its DSN from `HUB_DB_DSN`. Configure exactly one source: | values.yaml | Notes | | - | - | | `postgres.bundled.*` | A bundled eval Postgres (single instance, no HA, no backups). **On by default** (`postgres.bundled.enabled=true`) - a DB is not a required input for a pilot. Tune `postgres.bundled.image`, `auth.{database,username,password}`, `persistence.{size,storageClass}`, `resources`. It always claims a 10Gi ReadWriteOnce PVC from the default StorageClass - set `persistence.storageClass` on clusters without a default (e.g. EKS), or the PVC stays `Pending`. | | `postgres.external.existingSecret` | Point at your own Postgres via a Secret you provision (key `dsn`). The production path. | | `postgres.external.cnpgCluster` | Read a CloudNativePG `Cluster`'s app-user Secret directly (key `uri` by default; override with `postgres.external.secretKey`). | Set `postgres.bundled.enabled=false` and one of the `postgres.external.*` options for production. The bundled eval Postgres is fine for pilots but has no HA or backups. ## License | values.yaml | How it reaches the hub | Notes | | - | - | - | | `license.key` (or `license.existingSecret`) | Mounted file at `/etc/radar-hub/license` | Skyhook-issued license. **Required to use the hub.** The chart installs and boots without it (so `helm install` never hard-fails), but the web app stays on a full-screen license-required screen until you supply one. See [Licensing](/docs/cloud/self-hosted/licensing). | | `hub.airGapped` | `RADAR_HUB_AIR_GAPPED` | Turns off both calls the hub makes on its own initiative - the daily license renewal and the hourly version check. For deployments that must make no outbound connections at all. An air-gapped license is issued by hand and cannot renew itself, so this is a Custom-contract variant. The chart refuses to render against a hub image that does not read this setting, rather than installing one that keeps calling out. | | `hub.latestRadarVersion` | `HUB_LATEST_RADAR_VERSION` | Pins the Radar version offered to your clusters instead of resolving it from `api.github.com` hourly. Required in practice when `hub.airGapped` is set, or the hub reports no latest version at all. | | `hub.licenseServer` | `RADAR_HUB_LICENSE_SERVER` | Where the daily license renewal goes. Defaults to `https://api.radarhq.io`; ignored entirely when `hub.airGapped` is set. | > [!NOTE] > > The license is delivered as a **file**, not an environment variable. That is > what lets it be replaced without restarting the pod - the hub re-reads the > mount within a minute. `RADAR_HUB_LICENSE_KEY` is still honoured and takes > precedence when set, but an environment variable is fixed for the life of a > process, so supplying it that way turns a hot swap back into a redeploy. ## Branding Surfaced by `GET /api/config` and rendered by the web app's TopBar / help menu / Tokens wizard. | values.yaml | Env var | Default | | - | - | - | | `branding.brandName` | `RADAR_HUB_BRAND_NAME` | `Radar` | | `branding.docsURL` | `RADAR_HUB_BRAND_DOCS_URL` | `https://radarhq.io/docs` | | `branding.supportEmail` | `RADAR_HUB_BRAND_SUPPORT_EMAIL` | `support@radarhq.io` | | `branding.feedbackEmail` | `RADAR_HUB_BRAND_FEEDBACK_EMAIL` | `feedback@radarhq.io` | `brandName` controls the wordmark in the web app's sidebar. Override to your own company name to white-label the deployment surface. The "Powered by Radar" footer remains visible per EULA terms. ## Audit retention | values.yaml | Env var | Default | | - | - | - | | `hub.auditRetentionDays` | `RADAR_HUB_AUDIT_RETENTION_DAYS` | `365` | Self-Managed uses this single retention window for both visibility and deletion. The audit sweeper runs at startup and daily to delete older rows. ## OIDC Set together; partial config is a fatal misconfiguration at boot. | values.yaml | Env var | | - | - | | `auth.oidc.issuer` | `RADAR_HUB_OIDC_ISSUER` | | `auth.oidc.clientID` | `RADAR_HUB_OIDC_CLIENT_ID` | | `auth.oidc.clientSecret` | `RADAR_HUB_OIDC_CLIENT_SECRET` | | `auth.oidc.callbackURL` | `RADAR_HUB_OIDC_CALLBACK_URL` (auto-derives from `hub.publicURL` + `/api/auth/oidc/callback` when empty; override only for split-origin Ingress setups) | | `auth.oidc.defaultRole` | `RADAR_HUB_OIDC_DEFAULT_ROLE` (one of `owner` / `member` / `viewer`; default `member`) - the role assigned to every new non-bootstrap user. The first user to sign in bootstraps as `owner`; owners manage roles from Settings → Organization → Members. Roles are not derived from IdP groups. | | `auth.oidc.groupsClaim` | `RADAR_HUB_OIDC_GROUP_CLAIM` (default `groups`; empty turns [group passthrough](/docs/cloud/self-hosted/auth#idp-group-passthrough) off) | | `auth.oidc.scopes` | `RADAR_HUB_OIDC_SCOPES` (comma-joined; extra scopes on top of `openid` + `email`. A scope your IdP does not define fails every sign-in) | | `auth.oidc.useRefreshToken` | `RADAR_HUB_OIDC_USE_REFRESH_TOKEN` (chart default `true`) - keep the IdP refresh token and refresh the ID token before it expires. Without a refresh token, a session carrying IdP groups ends when its ID token expires. See [refresh tokens](/docs/cloud/self-hosted/auth#keeping-groups-current-with-refresh-tokens). | | `auth.oidc.clusterAccess` | `RADAR_HUB_OIDC_CLUSTER_ACCESS` (`""`, `roles`, `idp` or `token`; default `""` = `roles`) - whether new cluster installs keep the chart's role bindings, leave access to the bindings you write, or call the Kubernetes API with each person's own token. See [cluster access](/docs/cloud/self-hosted/auth#cluster-access-for-new-clusters). | | `auth.oidc.k8sUsernamePrefix` | `RADAR_HUB_OIDC_K8S_USERNAME_PREFIX` - `clusterAccess: token` only. Must equal the API servers' `--oidc-username-prefix`; empty for `-`. See [User tokens](/docs/cloud/self-hosted/user-tokens). | | `auth.oidc.k8sGroupsPrefix` | `RADAR_HUB_OIDC_K8S_GROUPS_PREFIX` - `clusterAccess: token` only. Must equal the API servers' `--oidc-groups-prefix`; empty for `-`. | See [Authentication](/docs/cloud/self-hosted/auth) for IdP-specific recipes. ## MCP OAuth MCP clients (Claude Code, Cursor) sign in with your IdP instead of a personal access token. Needs OIDC and an `https` `hub.publicURL`. See [MCP clients](/docs/cloud/self-hosted/mcp-clients). | values.yaml | Env var | | - | - | | `auth.oidc.mcpOAuth.enabled` | `MCP_OAUTH_ENABLED` (default `false`) | | `auth.oidc.mcpOAuth.scopes` | `RADAR_HUB_MCP_OAUTH_SCOPES` (space-joined) - what clients request. Empty = `openid email profile offline_access`. Entra needs its API scope first. | | `auth.oidc.mcpOAuth.audience` | `RADAR_HUB_MCP_OAUTH_AUDIENCE` - the access token's `aud`. Empty = `/mcp`. Entra v2 tokens carry the app's client ID. | | `auth.oidc.mcpOAuth.requiredScope` | `RADAR_HUB_MCP_OAUTH_REQUIRED_SCOPE` - a scope every MCP access token must carry. Required when `audience` is the client ID. | | `auth.oidc.mcpOAuth.tokenExchange.strategy` | `RADAR_HUB_MCP_TOKEN_EXCHANGE` - `clusterAccess: token` only. `entra-obo` or `rfc8693`: the hub trades the MCP access token for a token the API servers trust. Empty = MCP live cluster calls are refused in token mode. | | `auth.oidc.mcpOAuth.tokenExchange.audience` | `RADAR_HUB_MCP_TOKEN_EXCHANGE_AUDIENCE` - `rfc8693` target audience. Empty = `auth.oidc.clientID`. | | `auth.oidc.mcpOAuth.tokenExchange.scopes` | `RADAR_HUB_MCP_TOKEN_EXCHANGE_SCOPES` (space-joined). Empty with `entra-obo` = `/.default`. | ## Sessions Apply to OIDC and break-glass sessions alike. Empty uses the hub default. Go duration syntax (`168h`, not `7d`). | values.yaml | Env var | Default | | - | - | - | | `auth.session.maxLifetime` | `RADAR_HUB_SESSION_MAX_LIFETIME` | `168h` - absolute lifetime from sign-in | | `auth.session.maxInactive` | `RADAR_HUB_SESSION_MAX_INACTIVE` | `24h` - signed out after this long unused. Minimum `20m`; lower values stop the hub at boot. | See [Sessions](/docs/cloud/self-hosted/auth#sessions). ## Singleton org name | values.yaml | Env var | Default | | - | - | - | | `hub.orgName` | `RADAR_HUB_ORG_NAME` | `Default` | Used only for the very first login (when the singleton is seeded). Renaming the org later happens through the web app's Settings → Organization page; this value is not re-applied on subsequent boots. ## Mode flag The chart hardcodes `RADAR_HUB_MODE=self_hosted` in the `radar-hub` Deployment - you don't set it directly. The mode controls every hosted-vs-self-managed seam (single-org gate, billing impl, audit retention source, /api/config response shape). ## Version + cluster domain | values.yaml | Env var | Default | | - | - | - | | (auto - from `Chart.AppVersion`, override via `image.hub.tag`) | `RADAR_HUB_VERSION` | the chart's appVersion | | `clusterDomain` | (used by chart template only) | `cluster.local` | `RADAR_HUB_VERSION` is what `/api/config` reports as `version.current` and what the help menu surfaces in the web app. Override only if you need a custom build identifier. `clusterDomain` is the in-cluster DNS suffix the chart uses to build `HUB_UPSTREAM` for the web container's nginx (`..svc.`). The default works for every standard Kubernetes cluster; override if your cluster uses a non-default domain (some kubeadm setups or multi-cluster meshes do). ## Resource sizing ```yaml theme={null} hub: resources: requests: cpu: 100m memory: 256Mi limits: cpu: 1 memory: 1Gi replicas: 1 ``` Single replica is correct for the operational load - the tunnel session registry is in-memory, so a second replica without a sticky-routing layer would split tunnels across pods. The control plane is intentionally single-instance; if you outgrow it, talk to us. ## Hosted AI investigations Hosted [AI investigations](/docs/features/diagnose) are opt-in on self-managed installs and use your own Anthropic, Amazon Bedrock, or Google Vertex AI account. Every setting lives under `hub.agent`: | values.yaml | Default | Notes | | - | - | - | | `hub.agent.enabled` | `false` | Creates the sandbox namespace, Secret, NetworkPolicy, and Role, and turns the engine on. | | `hub.agent.provider` | `anthropic` | `anthropic`, `bedrock`, or `vertex`. | | `hub.agent.credentials.apiKey` / `serviceAccountJSON` / `existingSecret` / `podDSN` | | The model credential and the DSN the sandbox pod uses. | | `hub.agent.anthropic.*`, `hub.agent.bedrock.*`, `hub.agent.vertex.*` | | Model, region, and project per provider. | | `hub.agent.sandbox.*` | | Sandbox namespace and NetworkPolicy. | | `hub.agent.alertAnalysis` | `false` | Lets alert rules attach an AI analysis to the issues they match. Needs `hub.agent.enabled`. | With the bundled eval Postgres, the agent also needs `postgres.bundled.auth.password` set explicitly. See [AI investigations](/docs/cloud/self-hosted/ai-agent) for each provider, enabling it on a new or existing install, and troubleshooting. ## Image overrides The chart defaults to `ghcr.io/skyhook-dev/radar-hub:` and `radar-hub-web:`. To pin or repoint at a private mirror: ```yaml theme={null} image: hub: repository: my-mirror.example/skyhook/radar-hub tag: MATCHING_RELEASE_TAG web: repository: my-mirror.example/skyhook/radar-hub-web tag: MATCHING_RELEASE_TAG imagePullSecrets: - name: my-mirror-pull-secret ``` Replace `MATCHING_RELEASE_TAG` with the selected `radar-hub` chart's `appVersion`, after confirming that tag is published for both Hub and Web. Keep the two tags identical. ## Exposing the hub publicly Expose the hub the way you already publish internal web apps on your cluster's edge: a traditional **Ingress** or a **Gateway API** HTTPRoute, with the DNS record and TLS certificate you manage. The two are mutually exclusive. Both terminate on the `radar-hub-web` Service, which serves the web app and reverse-proxies `/api`, `/agent`, `/c/{id}/api/*`, `/c/{id}/mcp`, and `/mcp` to the `radar-hub` Service via in-cluster DNS. Other `/c/{id}` paths are client-side routes served by the web app's `index.html` fallback. If you would rather not provision a certificate for a short pilot, the chart can instead expose a cloud LoadBalancer and serve a self-signed certificate - see [LoadBalancer (pilot, self-signed TLS)](#loadbalancer-pilot-self-signed-tls) below. ### Connection requirements Your ingress or gateway must pass WebSocket upgrades for `/agent` and pod terminals, and allow long-lived HTTP responses for live views and log follow. The cluster tunnel sends a keepalive every 30 seconds, so give its idle timeout headroom (at least 60 seconds). Quiet log-follow streams do not send heartbeats; use a longer idle timeout for those, as in the Ingress example below. A maximum connection duration can still force the tunnel to reconnect. Timeout behavior depends on the controller and load-balancer type. Check its streaming and WebSocket guidance; [Google Cloud's modes have different limits](https://docs.cloud.google.com/load-balancing/docs/https/request-distribution#backend_service_timeout). ### Ingress Any IngressClass (nginx, traefik, contour, ...). `hosts` and `tls` are lists: ```yaml theme={null} ingress: enabled: true className: nginx annotations: nginx.ingress.kubernetes.io/proxy-read-timeout: "600" nginx.ingress.kubernetes.io/proxy-send-timeout: "600" hosts: - host: radar.acme.example paths: - path: / pathType: Prefix tls: - secretName: radar-acme-tls hosts: [radar.acme.example] ``` ### Gateway API (HTTPRoute) If you run a Gateway API implementation (Istio, Envoy Gateway, Cilium, ...), attach an HTTPRoute to a Gateway you already manage. The Gateway and its TLS listener live outside this chart; the route just attaches to it. ```yaml theme={null} httpRoute: enabled: true defaultTimeout: "0s" # allow streaming HTTP responses parentRefs: - name: my-gateway namespace: gateway-system sectionName: https # the Gateway's TLS listener hostnames: [radar.acme.example] ``` `defaultTimeout: "0s"` disables the request timeout on the chart's default rule, which keeps streamed HTTP responses open with controllers such as Envoy Gateway. It does not configure a WebSocket idle timeout. If you supply `httpRoute.rules`, set `timeouts.request: "0s"` in each streaming rule instead; custom rules replace the default rule. On an older Gateway API, set `httpRoute.apiVersion: gateway.networking.k8s.io/v1beta1`. For multiple path prefixes, per-route timeouts, filters, or alternate backends, set `httpRoute.rules` - your rules pass through unchanged and replace the default `/` route. `httpRoute.annotations` and `httpRoute.labels` attach metadata to the route. Customers wanting `api.acme.example` + `app.acme.example` split origins should disable both and write their own routing. ### LoadBalancer (pilot, self-signed TLS) For a short pilot where you would rather not provision a certificate, the chart can expose the hub through a cloud LoadBalancer and serve a self-signed certificate, making it reachable over HTTPS with no ingress controller or cert-manager. ```yaml theme={null} service: web: type: LoadBalancer web: tls: selfSigned: true ``` Or via `--set` on install: ```bash theme={null} helm install radar-hub skyhook/radar-hub \ --namespace radar-hub --create-namespace \ --set hub.publicURL=https://radar.acme.example \ --set service.web.type=LoadBalancer \ --set web.tls.selfSigned=true \ ... # license, cookiePassword, break-glass, Postgres as above ``` After install, read the load balancer's address and point your Hub URL's DNS at it: ```bash theme={null} kubectl get svc radar-hub-web -n radar-hub ``` > [!WARNING] > > Self-signed TLS is **encrypted but not authenticated** - clients trust the certificate without verifying it, so the connection is exposed to man-in-the-middle interception. Use it for pilots only. For production, use an Ingress or Gateway with a certificate you manage (for example via cert-manager), as above. **Connecting clusters.** Connect clusters to a self-signed hub through the web app's **Connect a cluster** wizard, which generates a Helm command that adds `--set cloud.insecureSkipVerify=true` so the in-cluster connector trusts the self-signed certificate. The `radar cloud install` CLI expects a browser-trusted certificate, so use the wizard's command for self-signed pilots. ## Skipping the chart's Secret If your security model forbids any secret material in `values.yaml`, every credential supports an `existingSecret` ref: | values key | existingSecret ref | Required Secret key | | - | - | - | | `hub.cookiePassword` | `hub.cookiePasswordExistingSecret` | `cookie-password` | | `license.key` | `license.existingSecret` | `license` | | `auth.oidc.clientSecret` | `auth.oidc.clientSecretExistingSecret` | `client-secret` | | `auth.breakGlass.passwordHash` | `auth.breakGlass.passwordHashExistingSecret` | `password-hash` | | (external Postgres) | `postgres.external.existingSecret` | `dsn` | | (CNPG) | `postgres.external.cnpgCluster` | `uri` (CNPG default; override with `postgres.external.secretKey`) | | `hub.agent.credentials.*` | `hub.agent.credentials.existingSecret` (in the sandbox namespace) | `HUB_AGENT_DB_DSN` plus the provider's key - see [AI investigations](/docs/cloud/self-hosted/ai-agent#using-your-own-secret) | # Radar Cloud Self-Managed Source: https://radarhq.io/docs/cloud/self-hosted/index Run the Radar Cloud control plane inside your own VPC. The same Go API + web app we operate at app.radarhq.io, deployed via Helm in a customer-controlled cluster. The control plane (`radar-hub` Go service + `radar-hub-web` nginx + web app + Postgres) runs on your own infrastructure. The cluster-side Radar binary is unchanged - the only thing that moves on-prem is the team / tenancy / tunnel-orchestration layer. You still install Radar in each cluster you want to see, pointed at your own control plane instead of `app.radarhq.io`. ## When to choose Self-Managed Pick Self-Managed when: * Cluster metadata cannot leave your VPC for compliance reasons (regulated industries, customer-data-handling clusters, government). * Your security team requires every dependency in the auth + observability path to be inside your perimeter. * You already operate Postgres and a Helm-managed application stack and would rather add a service than buy a SaaS. Pick the hosted Cloud at `app.radarhq.io` when: * Speed-of-onboarding matters more than the deployment-shape concession. * You don't want to operate a Postgres + control-plane upgrade cadence. * Your fleet is small enough that the free tier covers it. ## What ships in this section > **[Requirements](/docs/cloud/self-hosted/requirements)** > > Kubernetes versions, Postgres versions, resource ask, network egress. > **[Install](/docs/cloud/self-hosted/install)** > > Helm walkthrough, BYO Postgres / CloudNativePG variants, license setup. > **[Configuration reference](/docs/cloud/self-hosted/configuration)** > > Full `values.yaml` reference and the `RADAR_HUB_*` env-var matrix. > **[Authentication](/docs/cloud/self-hosted/auth)** > > OIDC (Okta / Auth0 / Google / Azure AD) and the always-available break-glass admin. > **[Licensing](/docs/cloud/self-hosted/licensing)** > > How a license is verified, what happens when it lapses, and how it is renewed. > **[Upgrades](/docs/cloud/self-hosted/upgrade)** > > Helm upgrade flow, migration safety, version-skew policy. > **[Backups](/docs/cloud/self-hosted/backups)** > > `pg_dump` recipe, restore drill, what's recoverable from K8s state alone. > **[Troubleshooting](/docs/cloud/self-hosted/troubleshooting)** > > License-verification, db-migration-failed, log collection. > **[Architecture](/docs/cloud/self-hosted/architecture)** > > Component diagram, data flow, trust boundaries. ## What's the same as hosted Cloud Everything except the deployment shape: * The Radar binary you install in each customer cluster is unchanged - same Helm chart (`skyhook-io/radar`), same `cloud.url` / `cloud.token` config. * The tunnel protocol (yamux over WebSocket) is unchanged. * The web app UX is unchanged. Multi-org create + the Billing tab are hidden via `/api/config` - everything else looks identical. * Personal access tokens (`rhp_*`) work identically for MCP and CLI. ## What's different | | **Hosted** | **Self-Managed** | | - | - | - | | Control plane | `app.radarhq.io` (Skyhook) | Your infrastructure | | Auth | WorkOS AuthKit | OIDC + always-available break-glass admin | | Orgs | Multi-tenant | Single org per deployment | | Billing | Stripe per cluster | Same per-cluster price; your control plane reports its own cluster count on the daily check-in and that is what is billed. Air-gapped deployments cannot report, so their count is set in the contract and written into the license. | | Support | Public docs + `support@radarhq.io` | Per-contract | | Updates | Skyhook deploys; you see it | You run `helm upgrade` | # Install Source: https://radarhq.io/docs/cloud/self-hosted/install Install the self-managed control plane with Helm, then enroll your first cluster with the Radar CLI. ## Prerequisites * A Kubernetes cluster reachable from your team's browsers, with schedulable Linux nodes (`amd64` or `arm64`) for the control-plane workloads (1.27+; [requirements](/docs/cloud/self-hosted/requirements)). * Helm 3.14 or newer. The configuration-changing upgrade examples use `--reset-then-reuse-values`, introduced in Helm 3.14, to apply new chart defaults while preserving the release's existing user-supplied values. * For production, a Postgres instance reachable from that cluster. For a pilot the chart's bundled eval Postgres runs by default, so a DB is not required to start ([requirements](/docs/cloud/self-hosted/requirements)). * A DNS record pointing at the cluster's Ingress, with a TLS certificate. For a pilot, you can instead expose the hub through a cloud LoadBalancer with a self-signed certificate - no ingress or certificate to set up ([Exposing the hub](/docs/cloud/self-hosted/configuration#loadbalancer-pilot-self-signed-tls)). * A Skyhook-issued license. Required to use the hub - the chart installs and boots without it, but the web app stays on a license-required screen until you supply `license.key`. On **Enterprise**, generate it yourself: sign in to [Radar Cloud](https://app.radarhq.io/settings/organization/self-hosting) as an organization owner and use **Settings → Self-Managed**, which issues the license and builds the install command around it. Re-opening that page hands back the same license, so it is safe to fetch again. On a **Custom** contract - including any air-gapped deployment - the license is issued by hand and your account team provides it. * A 32+ byte secret for sealing session cookies. Generate with `openssl rand -base64 48`. ## 1. Add the chart repo ```bash theme={null} helm repo add skyhook https://skyhook-io.github.io/helm-charts helm repo update ``` ## 2. Decide on a Postgres source The chart ships a bundled eval Postgres that runs by default (`postgres.bundled.enabled=true`) - single instance, no HA, no backups. For a pilot there is nothing to set; skip to the install. For production, disable the bundled DB and point the chart at your own via `postgres.external.*`. Examples below use `--set` for clarity; production should land these in a `values.yaml` you check in (excluding secrets). **Bundled (eval only)** On by default. Nothing to configure for a pilot. Tune size or credentials if needed: ```bash theme={null} helm install radar-hub skyhook/radar-hub \ --namespace radar-hub --create-namespace \ --set postgres.bundled.persistence.size=10Gi \ ... # the rest below ``` Not for production - no HA, no backups. **Managed (RDS / Cloud SQL / etc.)** The chart never takes a connection string in Helm values - only a reference to a Secret you provision. Put your Postgres connection string in that Secret (key `dsn`), point the chart at it, and turn the bundled DB off. ```bash theme={null} kubectl create namespace radar-hub kubectl -n radar-hub create secret generic radar-hub-pg \ --from-literal=dsn='postgres://radar:@db.acme.internal:5432/radar_hub?sslmode=require' ``` ```bash theme={null} helm install radar-hub skyhook/radar-hub \ --namespace radar-hub --create-namespace \ --set postgres.bundled.enabled=false \ --set postgres.external.existingSecret=radar-hub-pg \ ... # the rest below ``` **CloudNativePG** If you already run a CNPG `Cluster`, the chart reads its app-user Secret directly (key `uri`): ```bash theme={null} helm install radar-hub skyhook/radar-hub \ --namespace radar-hub --create-namespace \ --set postgres.bundled.enabled=false \ --set postgres.external.cnpgCluster=acme-pg \ ... # the rest below ``` The chart never installs the operator - bring your own. Override the Secret key with `postgres.external.secretKey` if it isn't the CNPG default `uri`. ## 3. Install A representative invocation: ```bash theme={null} helm install radar-hub skyhook/radar-hub \ --namespace radar-hub --create-namespace \ --set hub.publicURL=https://radar.acme.example \ --set hub.cookiePassword="$(openssl rand -base64 48)" \ --set license.key="$RADAR_HUB_LICENSE" \ --set postgres.bundled.enabled=false \ --set postgres.external.existingSecret=radar-hub-pg \ --set auth.breakGlass.email=admin@acme.example \ --set auth.breakGlass.password='' \ --set ingress.enabled=true \ --set ingress.className=nginx \ --set "ingress.hosts[0].host=radar.acme.example" \ --set "ingress.hosts[0].paths[0].path=/" \ --set "ingress.hosts[0].paths[0].pathType=Prefix" \ --set "ingress.tls[0].secretName=radar-acme-tls" \ --set "ingress.tls[0].hosts[0]=radar.acme.example" ``` You can run `helm install` without `license.key` (it won't fail), but the hub sits on a license-required screen until you supply one - add it here or in a follow-up `helm upgrade`. Drop the two `postgres.*` lines to use the bundled eval Postgres for a pilot. To publish via **Gateway API** instead of an Ingress, drop the `ingress.*` lines and set `httpRoute.enabled=true` with your Gateway's `parentRefs` (the two are mutually exclusive) - see [Configuration](/docs/cloud/self-hosted/configuration#exposing-the-hub-publicly). For a **pilot without an ingress controller or certificate**, drop the `ingress.*` lines and set `service.web.type=LoadBalancer` with `web.tls.selfSigned=true` instead - the cloud provisions a load balancer and the hub serves a self-signed certificate ([Exposing the hub](/docs/cloud/self-hosted/configuration#loadbalancer-pilot-self-signed-tls)). Pilots only; see the security note there. Watch it come up. Both Deployments must reach Ready; the hub applies the database schema first, so if it does not, the `migrate` container's log says why: ```bash theme={null} kubectl -n radar-hub get pods -w kubectl -n radar-hub logs deploy/radar-hub-hub -c migrate kubectl -n radar-hub logs deploy/radar-hub-hub -c hub --tail=50 ``` ## 4. First sign-in Open `https://radar.acme.example` and sign in with the break-glass email and password from step 3. This first sign-in creates your organization and makes you its owner. The install is working when you can sign in, connect a cluster (next step), and open that cluster's pods. Nothing else on the screen is a check. ## 5. Connect your first cluster The recommended path is the Radar CLI. It inspects the selected Kubernetes cluster and opens this control plane's consent page. For a fresh install or native Helm release, it proves that your kubeconfig identity can make the required changes and then installs or updates Radar. For a supported GitOps-managed release, it generates a source-of-truth handoff instead of mutating the live cluster. > [!WARNING] > > Run the Hub and Web images at matching tags. Mixed tags are unsupported. Before you begin: * Install the [Radar CLI](/docs/install) on the operator workstation. Verify the command is present with `kubectl radar cloud install --help`. * Sign in to `https://radar.acme.example` with an account allowed to add clusters to the singleton organization. If the organization limits cluster creation to owners, an owner must approve the consent page. * Give the CLI a kubeconfig identity that can read the existing Radar deployment and Helm release, create the target namespace when needed, and create or update the rendered Radar resources, including its Secret and cluster-scoped RBAC. * Allow the workstation to reach `https://skyhook-io.github.io/helm-charts`, the customer cluster to pull the Radar image, and both the workstation and customer cluster to trust the TLS certificate for `https://radar.acme.example`. On a self-signed pilot, export the hub's certificate and add it to the workstation's trust store; the agent side is handled by the generated command. If you would rather not, use the web app's **Connect a cluster** command below instead of the CLI. The examples below use `kubectl radar`, which works with every supported CLI installation. Homebrew also provides the shorter `radar` alias. Pass the intended kubeconfig context explicitly when you know it: ```bash theme={null} kubectl radar cloud install \ --hub-url https://radar.acme.example \ --context prod-us-east-1 ``` The CLI prints the exact target and plan before changing anything. For a fresh install or native Helm adoption, it runs a server-side Kubernetes dry run plus explicit permission checks before creating a control-plane cluster or minting a token, so a missing Kubernetes permission fails before the browser approval step. A GitOps handoff does not mutate the live cluster; readable, unambiguous ownership is its local safety gate. For a fresh install or native Helm adoption, add `--dry-run` to validate discovery, chart preparation, and the planned Kubernetes mutation permissions without opening the consent flow or changing either system. For GitOps, `--dry-run` validates discovery, verified controller ownership, and the exact chart handoff; it cannot prove write access to the source repository or permissions the reconciler will use. If you omit `--context`, the CLI prints the current kubeconfig context and asks you to confirm it. `-y` / `--yes` skips only that current-context confirmation; it never grants consent to adopt an existing installation. `--namespace` defaults to `radar`, and `--release` defaults to `radar`. Either flag by itself sets the fresh-install target while keeping cluster-wide discovery enabled; one unambiguous existing Radar installation still takes precedence and supplies its actual namespace and release. Pass both `--namespace` and `--release` when you deliberately want to force an exact target. Exact targeting disables broader discovery, so do not pass both flags merely to restate the defaults when you want the CLI to find an existing installation elsewhere in the cluster. On a self-signed pilot, one more step after the CLI has installed the agent, because the agent verifies the hub's certificate and the CLI does not tell it not to: ```bash theme={null} helm upgrade radar skyhook/radar -n radar --reset-then-reuse-values --set cloud.insecureSkipVerify=true ``` The agent connects within a few seconds of the rollout. The browser page names the target organization and cluster. Approve it there. For a fresh install or native Helm adoption, leave the CLI running while it provisions Radar and confirms the outbound tunnel. For GitOps, apply the printed handoff through the source of truth while the CLI waits for the reconciled agent. On success it prints the cluster name, a direct web-app link, and a context-pinned rollout command. By default, a fresh install, native Helm adoption, and GitOps handoff target the latest published stable Radar chart. This can upgrade an older Radar release and clears an explicit image tag so the selected chart's stable Radar version runs. The plan calls this out before approval. Use `--chart-version ` to select a particular published stable chart. The selected chart must be version 1.5.4 or newer. Native Helm adoption refuses a chart downgrade; GitOps handoff does the same when the existing chart version is available from the live workload metadata. ### Existing native Helm installation When the selected cluster already has an official Radar Helm release, the CLI offers to adopt that release in place. Adoption preserves its existing values and image repository, adds the self-managed connection settings through a fixed token Secret, and performs an atomic Helm upgrade. Interactive adoption requires a separate confirmation after the plan. Self-upgrade support is enabled by default so an owner can later initiate an upgrade from the web app. Use `--no-self-upgrade` if your operating model requires every upgrade to go through the terminal or GitOps. Adoption preserves the existing `rbac.helm`, `rbac.secrets`, `rbac.podExec`, `rbac.portForward`, and `rbac.metrics` settings by default. Pass `--enable-cloud-features` only when you explicitly want to turn all five capabilities on. Fresh installs already enable them. ```bash theme={null} kubectl radar cloud install \ --hub-url https://radar.acme.example \ --context prod-us-east-1 ``` For non-interactive automation, `--adopt-existing` is the separate assertion that the existing release may be changed; `-y` alone remains insufficient: ```bash theme={null} kubectl radar cloud install \ --hub-url https://radar.acme.example \ --context prod-us-east-1 \ --adopt-existing ``` After a successful adoption, the CLI prints a rollback sequence that restores the previous Helm revision before removing the connection token Secret or connected control-plane cluster. Keep that sequence with your change record. ### GitOps-managed installation When an official Radar chart Deployment carries exactly one verified Argo CD or Flux ownership signal, the CLI does not run an imperative Helm upgrade that the reconciler would undo. It prints the detected owner, the selected stable chart target, and a values handoff containing the connection Secret reference and `cloud.*` settings. Approve the browser consent, commit the generated changes to the source of truth, and let the reconciler apply them. The CLI can remain open for up to five minutes to confirm the tunnel, or you can verify the cluster later in the web app. The GitOps handoff forces `rbac.selfUpgrade=false` because a live image patch would drift from Git. It leaves the other feature RBAC settings unchanged unless you pass `--enable-cloud-features`, which adds the five explicit capability opt-ins to the merge fragment. An ambiguous or only weakly inferred GitOps owner is not mutated automatically. Resolve the ownership ambiguity or update the source manually. ### Manual Helm alternative Use the web app's **Connect a cluster** wizard when you need to integrate the values into another workflow yourself. It mints the same `rhc_*` cluster credential and generates the Helm values for this self-managed endpoint: ```bash theme={null} helm repo add skyhook https://skyhook-io.github.io/helm-charts helm repo update skyhook kubectl create namespace radar --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic radar-cloud-config -n radar \ --from-literal=token= \ --dry-run=client -o yaml | kubectl apply -f - helm upgrade --install radar skyhook/radar \ --namespace radar \ --set cloud.enabled=true \ --set cloud.url="wss://radar.acme.example/agent" \ --set cloud.clusterName= \ --set cloud.existingSecret=radar-cloud-config \ --set auth.mode=proxy \ --set rbac.helm=true \ --set rbac.secrets=true \ --set rbac.podExec=true \ --set rbac.portForward=true \ --set rbac.metrics=true \ --set rbac.selfUpgrade=true ``` The wizard generates this command with the real token and cluster ID. For an existing release it also offers a `--reset-then-reuse-values` variant that keeps your settings and picks up the new chart's defaults. Alternatively, translate the generated values and Secret into your GitOps source. Do not put the raw token in Helm values checked into Git. ## 6. (Optional) Add OIDC Once break-glass is verified working, layer OIDC on top: ```bash theme={null} helm upgrade radar-hub skyhook/radar-hub \ --namespace radar-hub \ --reset-then-reuse-values \ --set auth.oidc.issuer=https://acme.okta.com \ --set auth.oidc.clientID=$OIDC_CLIENT_ID \ --set auth.oidc.clientSecret=$OIDC_CLIENT_SECRET \ --set auth.oidc.callbackURL=https://radar.acme.example/api/auth/oidc/callback \ --set auth.oidc.defaultRole=member ``` Or use the hub's **Settings → SSO** page: enter issuer / clientID / clientSecret and it generates the exact `kubectl create secret` and `helm upgrade` commands for you, plus the redirect URI to register at your IdP. The Helm values stay the source of truth. Break-glass remains available regardless - the route stays mounted as long as `auth.breakGlass.email` is set. See [Authentication](/docs/cloud/self-hosted/auth) for IdP-specific recipes. ## 7. (Optional) Enable AI investigations Hosted AI investigations are off by default. Enable them with your own Anthropic, Amazon Bedrock, or Google Vertex AI account, at install or later with `helm upgrade` - see [AI investigations](/docs/cloud/self-hosted/ai-agent). With the bundled eval Postgres, enabling them later needs one extra step to carry over the generated database password. ## 8. (Optional) Pin to a specific release The chart and images are one release unit. Pin the chart version and both image tags when you need to stage an exact release: ```bash theme={null} RADAR_HUB_CHART_VERSION="CHART_VERSION" RADAR_HUB_RELEASE="MATCHING_RELEASE_TAG" helm show chart skyhook/radar-hub --version "$RADAR_HUB_CHART_VERSION" helm upgrade radar-hub skyhook/radar-hub \ --namespace radar-hub \ --reset-then-reuse-values \ --version "$RADAR_HUB_CHART_VERSION" \ --set-string image.hub.tag="$RADAR_HUB_RELEASE" \ --set-string image.web.tag="$RADAR_HUB_RELEASE" ``` Replace `CHART_VERSION` with the selected `radar-hub` chart version and `MATCHING_RELEASE_TAG` with the `appVersion` shown by `helm show chart`, after confirming that tag is published for both Hub and Web. Never mix a chart with images from another release, or mix Hub and Web release tags. ## Common gotchas * **`HUB_COOKIE_PASSWORD must be at least 32 bytes`** - the value passed to `hub.cookiePassword` is shorter than 32 ASCII characters. Use `openssl rand -base64 48`. * **Every sign-in returns to the login page with `session_expired`** - `hub.publicURL` is `http://`, not `https://`. The OIDC state cookie is `__Host-` prefixed and therefore `Secure`, so it is never returned over plain HTTP and the callback cannot complete. The hub logs `OIDC callback: state cookie missing`. Serve the hub over TLS; the chart accepts an `http://` value without complaint. * **Agent logs `cloud WebSocket URL must use wss://`** - same root cause. In-cluster Radar refuses a plaintext tunnel to any non-loopback host. For a self-signed trial, set `cloud.insecureSkipVerify=true` on the agent. * **OIDC redirect URI mismatch** - your IdP must have `https:///api/auth/oidc/callback` registered as an exact match. No path normalization. * **No Postgres configured** - the bundled eval Postgres runs unless you set `postgres.bundled.enabled=false` and configure one of the `postgres.external.*` sources. Set exactly one external source when you disable the bundled DB. * **Migrate initContainer stuck pulling** - the hub Pod stays in `Init:` status. Check `kubectl describe pod` for image-pull errors against your registry mirror. * **Control plane logs `license is invalid (bad signature or claims)`** - confirm the JWT was pasted whole (no whitespace) and the build was cut against the matching public key. See [Licensing](/docs/cloud/self-hosted/licensing). ## Air-gapped install Use this when the cluster cannot reach the internet. Everything above still applies; these are the additions. Before you start: * **Mirror the images** to a registry the cluster can pull from, and point the chart at it: | Image | Chart value | | - | - | | `ghcr.io/skyhook-dev/radar-hub` | `image.hub.repository` | | `ghcr.io/skyhook-dev/radar-hub-web` | `image.web.repository` | | `postgres:17-alpine`, if you use the bundled database | `postgres.bundled.image` | | `ghcr.io/skyhook-dev/radar-hub-ai-agent-sandbox`, if you enable AI investigations | `image.agentSandbox.repository` | | `ghcr.io/skyhook-io/radar`, the agent that runs in each cluster | `image.repository` on the `radar` chart | Set `imagePullSecrets` if the mirror needs credentials. * **Fetch the chart** somewhere with internet access and carry it over: `helm pull skyhook/radar-hub` writes a `.tgz`; install from that file instead of the repository. * **Get an air-gapped license** from us. See [Licensing](/docs/cloud/self-hosted/licensing#air-gapped). * **Your identity provider and, if enabled, your AI model provider** must be reachable from inside the network. The control plane connects to them where you point it. Install with the values above plus: ```bash theme={null} --set hub.airGapped=true --set license.key="" ``` Optionally set `hub.latestRadarVersion` to the Radar release you have mirrored. The control plane shows it as the latest available, since it cannot ask. The install is working when the control plane starts, you can sign in, connect a cluster and open its pods. With a license that does not permit air-gapped operation the control plane refuses to start and its log says why. If you are audited on egress: the control plane opens no connection outside your network. # Licensing Source: https://radarhq.io/docs/cloud/self-hosted/licensing Install, replace and recover a self-managed license. A self-managed control plane runs on a license issued by Skyhook. This page is what you do with it. ## Install a license Get the license: * **Cloud-managed deployment**: generate it in Radar Cloud under **Settings → Self-Managed**. It renews itself once a day from there. * **Air-gapped deployment**: ask us. It is issued by hand with the cluster count in your contract, and it carries the permission for `hub.airGapped=true`. Supply it to the chart in one of two ways: ```bash theme={null} # inline helm upgrade radar-hub skyhook/radar-hub -n radar-hub --reset-then-reuse-values \ --set license.key="" # or from a Secret you manage, with the token under the key `license` kubectl -n radar-hub create secret generic radar-hub-license \ --from-literal=license="" helm upgrade radar-hub skyhook/radar-hub -n radar-hub --reset-then-reuse-values \ --set license.existingSecret=radar-hub-license ``` The control plane starts without a license, but the console shows a license screen until one is installed. ## Replace a license If the license is in your values, upgrade with the new value. The upgrade restarts the hub for you: ```bash theme={null} helm upgrade radar-hub skyhook/radar-hub -n radar-hub --reset-then-reuse-values \ --set license.key="" ``` If it is in your own Secret, update the Secret, then restart the hub so it reads the new file: ```bash theme={null} kubectl -n radar-hub create secret generic radar-hub-license \ --from-literal=license="" --dry-run=client -o yaml | kubectl apply -f - kubectl -n radar-hub rollout restart deploy/radar-hub-hub ``` Nothing is interrupted while you do this. Your clusters, the agents and the API keep working throughout. A cloud-managed deployment does not need this for renewal. It fetches a fresh license every day on its own. ## If a license key leaks Contact support and ask for the key to be rotated. The leaked key stops renewing from that moment and expires on its own schedule. Install the replacement as above. ## When a license lapses The control plane keeps working in full for a grace period. When the grace period ends, the web console closes for everyone until a valid license is installed. Not affected at any point: your clusters and their data, the agents, the tunnel, personal access tokens and the API. Break-glass sign-in still works, but it does not reopen the console. To recover, install a valid license as above. This needs access to the Helm release or the Secret. There is no way to do it from the console. ## Air-gapped Set `hub.airGapped=true`. The control plane then makes no outbound calls of its own: no license renewal and no check for Radar releases. Anything you configure yourself, such as an identity provider or an AI model provider, still connects where you point it. It needs a license that permits it. With any other license the control plane refuses to start and says why. To install without internet access, see [Air-gapped install](/docs/cloud/self-hosted/install#air-gapped-install). An air-gapped license carries a cluster count. Clusters already connected keep working; connecting one beyond that count is refused, and the refusal names both numbers. To run more, ask us for a new license and install it as above. ## Billing Clusters on a self-managed control plane are billed through your Radar Cloud organization. See [Billing](/docs/cloud/billing). # Requirements Source: https://radarhq.io/docs/cloud/self-hosted/requirements What you need before installing Radar Cloud Self-Managed. ## Kubernetes * **Version:** 1.27 or newer. We test against 1.29 + 1.30. * **Architecture:** The official Hub and Web images are published for both `linux/amd64` and `linux/arm64`, so Graviton, Ampere and mixed-architecture clusters are supported. The chart pins only the OS by default and does not restrict the architecture. * **Resources reserved for the control plane:** * `radar-hub` (Go API + tunnel terminator): 100m CPU / 256Mi memory request, 1 CPU / 1Gi limit. Single replica is fine for hundreds of clusters. * `radar-hub-web` (nginx + web app bundle): 50m / 64Mi request. * Migration initContainer: shares the `radar-hub` image and runs to completion in the same Pod before the main hub container starts, so it doesn't need its own resource budget. * **Storage:** the control plane itself is stateless; durable state lives in Postgres, and production installs (external Postgres) claim no PVCs. The **bundled eval Postgres** (on by default) is the exception - its StatefulSet requests a ReadWriteOnce PVC (10Gi from your default StorageClass unless you set `postgres.bundled.persistence.size` / `.storageClass`). GKE and AKS ship a default StorageClass, so this works out of the box. A fresh **EKS** cluster often has none: install the EBS CSI driver add-on and set a default class, or set `postgres.bundled.persistence.storageClass` explicitly (e.g. `gp3`) - otherwise the PVC stays `Pending` and Postgres never starts. **EKS Auto Mode** uses the `ebs.csi.eks.amazonaws.com` driver, which the legacy `gp2` class doesn't use - see [Troubleshooting](/docs/cloud/self-hosted/troubleshooting#eks-auto-mode-postgres-pvc-stays-pending) for a StorageClass that works. * **AI investigations (optional):** each investigation turn runs as a short-lived Job in a sandbox namespace and needs a CNI that enforces NetworkPolicy plus egress to your model provider. See [AI investigations](/docs/cloud/self-hosted/ai-agent#requirements). * **Ingress class:** any (nginx, contour, traefik, gateway-api). Sample annotations in the install doc target nginx. ## Helm * **Version:** 3.14 or newer. The configuration-changing upgrade examples use `--reset-then-reuse-values`, introduced in Helm 3.14, so new chart defaults apply without dropping the release's existing user-supplied values. ## Postgres The chart ships a bundled eval Postgres that runs by default (`postgres.bundled.enabled=true`), so a DB is not a required input for a pilot. For production, disable it and point the chart at your own via `postgres.external.*`. Options: | Option | Best for | | - | - | | Bundled eval Postgres (`postgres.bundled.*`, on by default) | Pilots only. Single instance, no HA, no backups. | | Managed Postgres (RDS, Cloud SQL, Azure Flexible Server) via `postgres.external.existingSecret` | Production. Zero ops impact on your cluster. | | CloudNativePG `Cluster` via `postgres.external.cnpgCluster` | If you already operate CNPG. Chart reads its connection Secret directly. | | Crunchy / Zalando operator via `postgres.external.existingSecret` | Same shape as CNPG - point at the operator-generated Secret. | **Version:** Postgres 14 or newer. We test against 16 + 17. **Sizing:** the control plane's working set is tiny - a few thousand rows for clusters + members + audit. 1 vCPU / 2Gi RAM is plenty for hundreds of customer clusters; the bottleneck is audit-log retention, sized at `RADAR_HUB_AUDIT_RETENTION_DAYS` \* average events/day. **Connection string:** any standard `postgres://user:pass@host:5432/db?sslmode=require` URL. We recommend `sslmode=require` or stricter for managed databases. ## Network The control plane needs: * **Inbound:** browsers (your team) and customer in-cluster Radars reach `` over HTTPS. * **Outbound:** * Your IdP discovery endpoint for OIDC. * `ghcr.io` (or your private mirror) for image pulls. * `api.radarhq.io` for the daily license renewal, on a cloud-managed deployment. This one is easy to leave out of an egress policy and the failure is quiet: the license stops renewing, and thirty days after it expires the web console stops accepting sign-ins. An **air-gapped** deployment (`hub.airGapped=true`) makes no outbound calls at all and does not need it - see [Licensing](/docs/cloud/self-hosted/licensing). * `api.github.com`, hourly, to resolve the latest Radar version offered to your clusters. Optional: pin `hub.latestRadarVersion` and it is never called. The control plane does NOT need to reach customer clusters - the in-cluster Radar always initiates the WebSocket tunnel outbound. ## Browser * Same matrix as the hosted Cloud: latest Chrome / Edge / Firefox / Safari. No IE. ## DNS + TLS * One DNS record (e.g. `radar.acme.example`) pointing at your Ingress. This URL **must** be set as `hub.publicURL` in the Helm values - the control plane fails fast at boot if it's not configured. Used for sign-in redirects, agent tunnel registration, OAuth-MCP resource publishing, and any other canonical-URL surface. * A TLS cert via cert-manager / your existing wildcard / a manually-provisioned Secret. The chart's Ingress template accepts a pre-provisioned `tls.secretName` and is unopinionated about the cert source. ## Auth provider (one of) * **OIDC IdP** with client credentials registered for `/api/auth/oidc/callback` as a redirect URI. Tested against Okta, Auth0, Google Workspace, Azure AD. * **Break-glass admin only** for evaluation pilots; configurable via env vars. Permanent escape hatch even when OIDC is enabled. ## License A license from Skyhook, required to use the hub. The chart installs and boots without it, but the app stays on a license-required screen until you supply one. Enterprise organizations generate their own from **Settings → Self-Managed** in [Radar Cloud](https://app.radarhq.io/settings/organization/self-hosting); Custom contracts are issued by hand. See [Licensing](/docs/cloud/self-hosted/licensing). # Troubleshooting Source: https://radarhq.io/docs/cloud/self-hosted/troubleshooting What to check when things go wrong. Failure modes from real installs, with the fix at the top. ## First: what to grab before opening a support ticket Three pieces of context cover 80% of cases: ```bash theme={null} # 1. hub container logs (last 200 lines). kubectl -n radar-hub logs deploy/radar-hub-hub -c hub --tail=200 # 2. migrate initContainer logs (initContainer of the same hub Pod). kubectl -n radar-hub logs deploy/radar-hub-hub -c migrate # 3. Pod state. kubectl -n radar-hub get pods,deploy,svc,ingress ``` Attach those to `support@radarhq.io` along with your license id (visible in the log line `radar-hub: license verified license_id=lic_...`). ## "Can't reach the web app" / first-install issues ### Pod stuck pending ```bash theme={null} kubectl -n radar-hub describe pod ``` Most common causes: * **Image pull error** - your registry mirror isn't reachable, or the `imagePullSecrets` ref is missing. Confirm `kubectl -n radar-hub describe pod ` shows the secret under `imagePullSecrets`, and read the pull error in its events. * **No node fits** - `nodeSelector` / `tolerations` / `affinity` settings exclude every node. Drop them while debugging. * **PVC pending** - only the bundled eval Postgres claims a PVC (10Gi ReadWriteOnce from the default StorageClass). `kubectl -n radar-hub describe pvc` shows why. With no default StorageClass, set `postgres.bundled.persistence.storageClass` or create a default class. See [EKS Auto Mode](#eks-auto-mode-postgres-pvc-stays-pending) for the common EKS case. ### EKS Auto Mode: Postgres PVC stays `Pending` EKS Auto Mode provisions volumes with the `ebs.csi.eks.amazonaws.com` driver. A cluster whose only StorageClass is the legacy `gp2` (`kubernetes.io/aws-ebs` provisioner), with none marked default, never binds the bundled Postgres PVC, so Postgres and the hub never start. Create a default StorageClass for the Auto Mode driver, then delete the pending PVC and Postgres pod (or reinstall): ```yaml theme={null} apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: auto-ebs-sc annotations: storageclass.kubernetes.io/is-default-class: "true" provisioner: ebs.csi.eks.amazonaws.com volumeBindingMode: WaitForFirstConsumer parameters: type: gp3 encrypted: "true" ``` Or keep your cluster default unchanged and set `postgres.bundled.persistence.storageClass=auto-ebs-sc` on the release. ### Hub Pod stuck in Init: migrate failed ```bash theme={null} kubectl -n radar-hub logs deploy/radar-hub-hub -c migrate ``` Failure modes: * **`relation "X" does not exist`** - extremely rare since the chart's `000_baseline.sql` migration bootstraps the schema automatically. Do not drop the database. Collect the `migrate` initContainer logs (`kubectl -n radar-hub logs deploy/radar-hub-hub -c migrate`) and contact support; if this is a fresh install with no data yet, a clean empty database and a pod restart re-applies the schema. * **`password authentication failed`** - the DSN is wrong. `kubectl get secret -o yaml | grep dsn` to confirm. * **`connection refused`** - the DB isn't reachable from this namespace. Test with a one-off `kubectl run -ti --rm pgcheck --image=postgres:17-alpine -- psql -c 'SELECT 1'`. ### Hub Pod CrashLoopBackOff at boot ```bash theme={null} kubectl -n radar-hub logs deploy/radar-hub-hub -c hub --previous ``` (Use `--previous` because the main container dies on misconfig; the current pod may not have logs yet.) * **`no auth configured: set WORKOS_API_KEY ... or RADAR_HUB_OIDC_* ... or RADAR_HUB_BOOTSTRAP_ADMIN_* ...`** (`log.Fatal` at boot) - neither WorkOS nor OIDC nor break-glass is wired. At least one is required. Fix in chart values: `auth.breakGlass.email` + password or OIDC `issuer` + secret. * **`HUB_COOKIE_PASSWORD must be at least 32 bytes`** - the value is too short. Regenerate with `openssl rand -base64 48`. ### Control plane starts but `/api/config` 503s ```bash theme={null} kubectl -n radar-hub logs deploy/radar-hub-hub -c hub --tail=50 ``` * **`db connection: ...`** - container is up, DB is unreachable mid-flight. Same checks as the migrate failures above. ### Web app loads but every request 401s The web app fetches `/api/account` on mount. If that 401s in a fresh browser: * **Cookie not set** - confirm you actually completed the break-glass login or OIDC roundtrip. The login response should `Set-Cookie: radar-hub-session=...`. * **CORS / SameSite mismatch** - if your web app host differs from the control plane host (split origins), the cookie won't flow. Single-host install is the supported MVP shape. * **`HUB_ALLOWED_ORIGINS` doesn't include your origin** - the control plane strips disallowed origins server-side. ## Auth issues ### "Invalid credentials" on break-glass login * Email is case-insensitive, but trim whitespace. * Bcrypt hash format must start with `$2y$`, `$2a$`, or `$2b$`. If it starts with anything else, the chart will reject it at boot - check `kubectl -n radar-hub logs deploy/radar-hub-hub | grep break-glass`. * Plaintext password with a `'` or `$` not properly escaped via `--set` will produce a hash for a different string than you intended. Use `--set-string` or move secrets to a Secret. ### OIDC redirect loop * `auth.oidc.callbackURL` must EXACTLY match what's registered with the IdP. Case, scheme, port, trailing slash all matter. * Discover-doc errors at boot: `kubectl -n radar-hub logs | grep oidc`. Common cause: corporate proxy intercepting outbound HTTPS - the control plane needs direct egress to the IdP's `.well-known/openid-configuration`. ### "OIDC silent refresh not supported" That's not an error. It comes from the Cloud refresh path, which self-managed does not use. Self-Managed sessions end after 7 days, or 24 hours unused, and the user signs in again through the IdP. To refresh IdP groups during a session, see [refresh tokens](/docs/cloud/self-hosted/auth#keeping-groups-current-with-refresh-tokens). ### Users are signed out after a day That's the idle limit: a session unused for 24 hours ends. Raise `auth.session.maxInactive` (for example `"72h"`) - see [Sessions](/docs/cloud/self-hosted/auth#sessions). ### 503 errors with `useRefreshToken` on The hub could not reach the IdP to refresh a user's ID token, or the IdP returned 429/5xx. The session is kept and the next request retries. Check egress from the hub to the IdP's token endpoint. `kubectl -n radar-hub logs deploy/radar-hub-hub | grep -i refresh` shows the error. ### A user has the wrong role Roles are managed in the hub, not from IdP groups. The first user to sign in bootstraps as `owner`; everyone else joins as `auth.oidc.defaultRole` (default `member`). An owner promotes or demotes anyone from Settings → Organization → Members in the web app. ## License issues ### "license is invalid (bad signature or claims)" * The build was cut against a different public key than the one signed your JWT. Confirm the `radar-hub` image tag matches what your account team issued the license for. * The JWT was truncated when pasted. Whole token is one base64 line - watch for `\n` mid-string. ### "license is expired" The control plane keeps serving in full for a grace period, then the web console closes for everyone until a valid license is installed. Break-glass sign-in still works, but it does not reopen the console. Your clusters, the tunnel, your agents and API/token traffic are unaffected throughout. To recover, install a valid license through the Secret or the Helm values; see [Rotation](/docs/cloud/self-hosted/licensing#replace-a-license). There is no way to do it from the console. If this deployment is **cloud-managed**, it renews itself. Check outbound HTTPS to your configured license server (`hub.licenseServer`, default `https://api.radarhq.io`). To replace the license by hand: 1. Get a renewed JWT from your account team. 2. Update the Helm value (`license.key`) or rotate the existing Secret. 3. Restart the hub pod so it reads the new file: `kubectl -n radar-hub rollout restart deploy/radar-hub-hub`. ### "embedded license public key is the all-zero placeholder" The `radar-hub` binary was built without the production public key embedded. This shouldn't happen in any image you pulled from `ghcr.io/skyhook-dev/radar-hub`. If you see it, you're probably running a self-built image - the chart's image references should point at the official release. ## Cluster-side issues ### "Cluster shows disconnected" but the in-cluster Radar is running In the customer cluster: ```bash theme={null} kubectl logs -n radar deploy/radar --tail=50 ``` Look for: * `cloud: dial wss://radar.acme.example/agent: ...` - DNS / network from the customer cluster to the control plane. * `cloud: token rejected` - the cluster token was rotated or revoked. Re-mint via the web app's "Add cluster" wizard. * `cloud: tls handshake failed` - the control plane's TLS cert doesn't validate from the customer cluster. Confirm cert chain. ### Cluster connects but tunnel calls hang * The control plane's `proxy_read_timeout` is 600s by default. Long Helm catalog scans or large audit pulls can exceed that. Bump via `ingress.annotations` for nginx-ingress. * Some on-prem load balancers strip `Connection: upgrade` at L7. The Ingress class needs to support WebSocket. nginx-ingress does by default; some HA-Proxy / F5 deployments need explicit config. ## Performance issues ### `/api/fleet/*` is slow Fleet calls fan out to every connected cluster in parallel with a 7s per-cluster timeout + 12s overall cap. If you have hundreds of clusters and the slowest one dominates, the call returns partial results with the slow ones flagged. The web app's coverage footer shows offline / errored clusters - that's the signal, not a bug. ### `radar-hub` Pod CPU at 100% * Audit reads filter on the retention window, and a daily sweep deletes rows older than `RADAR_HUB_AUDIT_RETENTION_DAYS`. Watch `radarhub_audit_events_total` - if it's into the millions and queries are slow, reduce `RADAR_HUB_AUDIT_RETENTION_DAYS`. * Tunnel-session count: hundreds of clusters connected to a single replica is fine; thousands isn't tested. ### Postgres slow on audit reads Index on `audit_events(org_id, created_at DESC)` is shipped by default. If a customer's audit volume is enormous, ensure that index is healthy: ```sql theme={null} SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'audit_events'; ``` ## Last resort * `helm get values radar-hub --namespace radar-hub` to confirm what's actually configured. * `kubectl -n radar-hub describe deploy/radar-hub-hub` for the full env-var list as Kubernetes sees it. * Toggle break-glass admin if OIDC is wedged - it's always available even when OIDC is misconfigured. That's its whole purpose. If none of the above gets you unblocked, email `support@radarhq.io` with the three artifacts at the top of this page. # Upgrade Source: https://radarhq.io/docs/cloud/self-hosted/upgrade Helm upgrade flow, migration safety, version-skew policy. ## The flow ```bash theme={null} helm repo update helm upgrade radar-hub skyhook/radar-hub \ --namespace radar-hub ``` Omitting `--reuse-values` is deliberate. Because this command passes no new value overrides, Helm combines the new chart defaults with the release's existing user-supplied values instead of carrying forward defaults from the old chart. Commands below that add explicit overrides use `--reset-then-reuse-values` to perform the same merge before applying those overrides. What happens: 1. Helm renders the new manifests from the new chart defaults plus your existing user-supplied values. 2. The `radar-hub` Deployment rolls (default rolling update; one new pod up + healthy before the old terminates). Each new pod's `migrate` initContainer applies any pending schema migrations against your Postgres before the main `hub` container starts. 3. The `radar-hub-web` Deployment rolls similarly. Rolling-update strategy is deliberately conservative - the control plane is single-replica today, so the rollout briefly serves out of one new pod before the old terminates. Brief 503s during the cut are normal; the web app's auto-retry covers them. ## Pinning a specific release ```bash theme={null} RADAR_HUB_CHART_VERSION="CHART_VERSION" RADAR_HUB_RELEASE="MATCHING_RELEASE_TAG" helm show chart skyhook/radar-hub --version "$RADAR_HUB_CHART_VERSION" helm upgrade radar-hub skyhook/radar-hub \ --namespace radar-hub \ --reset-then-reuse-values \ --version "$RADAR_HUB_CHART_VERSION" \ --set-string image.hub.tag="$RADAR_HUB_RELEASE" \ --set-string image.web.tag="$RADAR_HUB_RELEASE" ``` Replace `CHART_VERSION` with the selected `radar-hub` chart version and `MATCHING_RELEASE_TAG` with the `appVersion` shown by `helm show chart`, after confirming that tag is published for both Hub and Web. The chart and images ship as one release unit. Never mix a chart with images from another release, or mix Hub and Web release tags. ## Migration safety All migrations land in `db/migrations/NNN_*.sql` and follow these rules: * **Forward-only.** No down-migrations. Helm rollback does not undo schema changes; restore a database backup only when the previous binary cannot run against the migrated schema. * **Additive-by-default.** New columns are nullable or safely defaulted and backfilled before constraints are enforced; new tables don't break existing reads. * **Idempotent.** `IF NOT EXISTS`, `IF EXISTS`, `ON CONFLICT DO NOTHING`. A re-run never fails. * **Tracked.** `schema_migrations` records every applied version; the runner skips applied ones. Destructive migrations are called out in release notes with explicit downtime, backup, and rollback requirements. The 1.1.0 migration set is additive. ## Version-skew policy * The chart and the images ship together. Use `helm upgrade` (which pins both via `appVersion`) rather than rolling images independently. * The control plane speaks to in-cluster Radar via the tunnel protocol, which is **forward-compatible** by design - older Radar versions talk to newer control planes. We test 2 minor versions back. If you see protocol-level errors, check the Helm chart pinned in customer clusters: `radar` chart ≥ `1.5.4` is required for cloud-mode RBAC. * The chart defaults Hub and Web to the same `appVersion`. Explicit image overrides can bypass that default; keep both tags identical because mixed versions are unsupported. ## Pre-upgrade checks ```bash theme={null} # 1. Verify the chart is reachable + the new version is what you expect. helm search repo skyhook/radar-hub # 2. Render the new chart against your live values to catch value errors before upgrading. helm get values radar-hub --namespace radar-hub > /tmp/live-values.yaml helm template radar-hub skyhook/radar-hub \ --namespace radar-hub \ -f /tmp/live-values.yaml > /tmp/rendered.yaml # 3. (Recommended) Take a backup of your Postgres. pg_dump -h -U radar radar_hub | gzip > radar-hub-pre-upgrade.sql.gz ``` ## After upgrade The first thing to check post-upgrade: ```bash theme={null} kubectl -n radar-hub logs deploy/radar-hub-hub --tail=30 ``` Look for: * `radar-hub starting addr=:8080 mode=self_hosted ...` - the control plane booted. * `license verified` - the control plane loaded and verified your license. * No `database schema not found` / `pg_query: relation "X" does not exist` - the `migrate` initContainer applied successfully before the main container started. Sign in and open a cluster. If both work, the upgrade is done. ## Upgrading to a chart with cluster access modes One default changes when you upgrade to a `radar-hub` chart that has `auth.oidc.clusterAccess`. Because the flow above applies new chart defaults, it takes effect unless you set it: * **`auth.oidc.useRefreshToken` defaults to `true`.** Sessions keep IdP groups current for their full lifetime. Your IdP client needs the Refresh Token grant, and Okta and Entra need `offline_access` in `scopes`; without a refresh token, a session that carries groups ends when its ID token expires. See [refresh tokens](/docs/cloud/self-hosted/auth#keeping-groups-current-with-refresh-tokens). `auth.oidc.clusterAccess` itself changes nothing when unset: new clusters keep the hub-role bindings (`roles`). Set `idp` to have [IdP groups](/docs/cloud/self-hosted/auth#cluster-access-for-new-clusters) decide access, or `token` for [user tokens](/docs/cloud/self-hosted/user-tokens). Either applies only to clusters you connect afterwards. Upgrade connected clusters' **Radar Helm charts** as well as the hub. Alerts and the timeline need [background access](/docs/cloud/rbac#radar-cloud-background-identity); hosted investigations require Radar 1.16.0+ and the appropriate [investigation access](/docs/cloud/rbac#ai-investigation-access). Use `--reset-then-reuse-values` to pick up new default grants while keeping your supplied values. ## Rollback ```bash theme={null} helm rollback radar-hub --namespace radar-hub ``` **Caveat:** Helm rollback rolls back the manifests, NOT the database. If the previous release had a different schema, the rolled-back control plane will fail against the migrated DB. We don't ship down-migrations. Practical guidance: * For chart-template-only changes (annotations, resources, ingress): rollback is safe. * For binary-version rollbacks within the same migration cohort: rollback is safe. * For binary-version rollbacks across a destructive migration: restore from backup. Tag releases that include destructive migrations clearly in our release notes so you know which rollbacks need a DB restore. ## Cadence * **Patch (0.x.Y):** as needed, typically weekly. Bug fixes only. Minor or no migration. * **Minor (0.X.0):** monthly. Feature work + migrations as needed. * **Major (X.0.0):** breaking changes, e.g. cookie format change, mandatory new env var. We'll publish a migration guide ahead of these. We support the current minor + the previous one. A 6-month-old version is out of support and may not get patches. # User tokens Source: https://radarhq.io/docs/cloud/self-hosted/user-tokens Clusters call the Kubernetes API with each person's own SSO token, so the RBAC you already have decides what they can do. With `auth.oidc.clusterAccess: token`, in-cluster Radar stops impersonating people. Every live Kubernetes call carries the person's own ID token, issued by the OIDC client your API servers already trust. The API server authenticates that token and applies the RBAC you already have, so each person can do in Radar exactly what they have permission to do anywhere else. This page assumes your clusters already accept tokens from your IdP (Microsoft Entra ID, Okta, Keycloak, or any OIDC provider), on any platform that lets the API server trust an external OIDC issuer. You connect the hub to the same OIDC client, and change nothing on the API servers or in your RBAC. If your clusters don't trust your IdP, [Hub roles or IdP groups](/docs/cloud/self-hosted/auth#cluster-access-for-new-clusters) are simpler. | | Hub roles / IdP groups | User tokens | | - | - | - | | Who calls the API server | Radar's ServiceAccount, impersonating the person | The person's own ID token | | Who decides access | Chart bindings, or your bindings on `radar:idp:` | Your existing bindings on the person's OIDC username and groups | | API server change | None | None, if it already trusts your IdP | | Personal access tokens | Work | Can't make live cluster calls: they carry no token | ## Requirements * Radar Hub 1.12.0 or newer, and Radar 1.18.0 or newer on every cluster. * Every cluster trusts the **same** OIDC client (one issuer, one client ID). The hub signs in with one client, so clusters that each trust their own client can't share it. * The API servers take the username from the `email` claim. The hub builds the username the same way, so Radar's checks on cached reads match the API server. * `hub.publicURL` on `https` with a certificate the clusters can verify: not `web.tls.selfSigned`, and not `localCluster`. Radar only accepts tokens over a verified tunnel. * The API server itself checks the IdP token. Not AKS with Microsoft Entra integration (its API server only trusts the AKS app), and not Identity Service for GKE (a proxy in front of the API server checks the token). ## 1. Read what your clusters expect The hub signs in with the same client your API servers trust. Get its issuer and client ID from your IdP: **Microsoft Entra ID** **App registrations** → the app your clusters trust → **Overview**: * Client ID: **Application (client) ID**. * Issuer: `https://login.microsoftonline.com//v2.0`. **Keycloak** * Client ID: the **Client ID** of the client your clusters trust. * Issuer: `https:///realms/`. **Other IdPs** The client's ID, and the issuer URL from your IdP's `/.well-known/openid-configuration`. Then ask a cluster who you are, to get the prefixes the API server adds: ```bash theme={null} kubectl auth whoami ``` ```text theme={null} ATTRIBUTE VALUE Username oidc:alice@example.com Groups [platform-admins system:authenticated] ``` The text before your email is the username prefix (`oidc:` here). The text before each of your IdP groups is the groups prefix (none here; ignore `system:authenticated`). If you see your email and your groups, the ID tokens already carry the claims the hub needs. > [!NOTE] > > With an empty groups prefix, an IdP group named `system:masters` reaches the API server as that group. The hub logs a warning at startup. Setting a prefix needs the same change on the API servers and in your bindings, so plan it separately. ## 2. Add the hub to the OIDC client Use the client from step 1. The hub needs a redirect URI and a client secret on it. **Microsoft Entra ID** In the portal, on the app registration: 1. **Authentication** → **Web** platform → add the redirect URI `https:///api/auth/oidc/callback` → **Save**. Keep the redirects already there. 2. **Certificates & secrets** → **New client secret**. A separate secret for the hub lets you rotate it on its own. Or with the Azure CLI. `--web-redirect-uris` replaces the whole list, so the command passes the existing redirects back with the new one, and `--append` keeps the existing secrets: ```bash theme={null} APP="" HUB="https://" az ad app update --id "$APP" --web-redirect-uris $(az ad app show --id "$APP" --query 'web.redirectUris[]' -o tsv) "$HUB/api/auth/oidc/callback" az ad app credential reset --id "$APP" --append --display-name radar-hub --query password -o tsv ``` **Keycloak** On the client: **Settings** → **Valid redirect URIs** → add `https:///api/auth/oidc/callback` → **Save**. **Client authentication** must be on; the secret is under **Credentials**. Keycloak has one secret per client, so don't regenerate it: that breaks everything else using it. **Other IdPs** Add `https:///api/auth/oidc/callback` as a redirect URI on the client, keeping the existing ones, and get a client secret for the hub. If your IdP supports more than one secret per client, use a separate one for the hub. ## 3. Configure the hub Store the client secret from step 2 in a Secret, then point the hub at the same client with the values from step 1: ```bash Helm command theme={null} kubectl create secret generic radar-hub-oidc -n radar-hub \ --from-literal=client-secret='' \ --dry-run=client -o yaml | kubectl apply -f - helm upgrade radar-hub skyhook/radar-hub \ --namespace radar-hub \ --reset-then-reuse-values \ --set-string auth.oidc.issuer='' \ --set-string auth.oidc.clientID='' \ --set auth.oidc.clientSecretExistingSecret=radar-hub-oidc \ --set 'auth.oidc.scopes={offline_access}' \ --set auth.oidc.clusterAccess=token \ --set-string auth.oidc.groupsClaim=groups \ --set-string auth.oidc.k8sUsernamePrefix=oidc: \ --set-string auth.oidc.k8sGroupsPrefix= ``` ```yaml Values theme={null} auth: oidc: issuer: "" clientID: "" clientSecretExistingSecret: radar-hub-oidc # key: client-secret scopes: ["offline_access"] clusterAccess: token # From step 1. Prefixes are empty if there's none. groupsClaim: groups k8sUsernamePrefix: "oidc:" k8sGroupsPrefix: "" ``` In Radar, **Settings → Organization → SSO** generates the Secret and the first three values for you, if you prefer to start there. | Value | Env var | Notes | | - | - | - | | `auth.oidc.issuer`, `auth.oidc.clientID` | `RADAR_HUB_OIDC_ISSUER`, `RADAR_HUB_OIDC_CLIENT_ID` | From step 1. | | `auth.oidc.clientSecretExistingSecret` | `RADAR_HUB_OIDC_CLIENT_SECRET` | A Secret with the key `client-secret`. | | `auth.oidc.scopes` | `RADAR_HUB_OIDC_SCOPES` | See below. | | `auth.oidc.clusterAccess` | `RADAR_HUB_OIDC_CLUSTER_ACCESS` | `token`. The hub refuses to start without OIDC or `useRefreshToken`, or with WorkOS configured. | | `auth.oidc.groupsClaim` | `RADAR_HUB_OIDC_GROUP_CLAIM` | Must equal the API server's groups claim. Default `groups`. | | `auth.oidc.k8sUsernamePrefix` | `RADAR_HUB_OIDC_K8S_USERNAME_PREFIX` | Must equal the API server's username prefix. | | `auth.oidc.k8sGroupsPrefix` | `RADAR_HUB_OIDC_K8S_GROUPS_PREFIX` | Must equal the API server's groups prefix. | Neither prefix may be `-` or start with `radar:`, `cloud:` or `system:`. Sessions last past the first ID token only with a refresh token. Okta and Entra ID return one only when `offline_access` is in `auth.oidc.scopes`; Keycloak returns one by default, so you can leave `scopes` out. Add only scopes your IdP defines: an unknown scope fails sign-in (Entra ID rejects `groups`). To check, open Radar → **Settings → Organization → SSO**: the **Cluster access** card now shows **User tokens** (read-only). From now on, clusters already connected don't open until you switch them (step 4), so do steps 3 and 4 together. ## 4. Connect clusters * **New clusters**: Radar's **Connect a cluster** page installs Radar 1.18.0 or newer with `auth.kubernetesCredentials=user-token` and `cloud.defaultRbac.create=false`. * **Existing clusters**: the hub refuses to open a cluster whose Radar still impersonates, until it's switched. In Radar, **Settings → Organization → SSO → Switch clusters you already connected** has the command for each cluster: ```bash theme={null} helm upgrade radar skyhook/radar -n radar --version '>=1.18.0' \ --reset-then-reuse-values \ --set-string image.tag="" \ --set cloud.defaultRbac.create=false \ --set auth.kubernetesCredentials=user-token ``` Radar adds no role bindings for people in this mode: your existing bindings decide access. The cluster's ServiceAccount loses its impersonate permission, and `rbac.podExec` and `rbac.portForward` are ignored: shells and port-forwards run on the person's own token. ## 5. Check access Sign in to Radar as someone whose permissions you know, and open a cluster. They should see and do the same as with any other tool that signs in through the IdP: a namespace they can't list is missing in Radar too, and an action RBAC denies is refused. If someone sees less than expected after a change in the IdP, have them sign out of Radar and in again. ## AI agents and MCP clients Personal access tokens can't make live cluster calls in this mode. MCP clients that sign in with your IdP need the hub to exchange their access token for one the API servers trust: set `auth.oidc.mcpOAuth.tokenExchange`. See [MCP clients](/docs/cloud/self-hosted/mcp-clients#clusters-on-user-tokens). ## Troubleshooting | Symptom | Cause | | - | - | | Hub doesn't start: `token needs OIDC configured, WorkOS not configured, and auth.oidc.useRefreshToken ... on` | Turn on `useRefreshToken`, and make sure OIDC is fully configured. | | Hub doesn't start: `RADAR_HUB_OIDC_K8S_*_PREFIX` | A prefix is `-`, or starts with `radar:`, `cloud:` or `system:`. | | Sign-in fails with a redirect URI mismatch (Entra ID: `AADSTS50011`) | The hub's callback isn't registered on the client (Entra ID: under the **Web** platform). | | A cluster won't open and asks for a switch (`user_token_unsupported`) | Its Radar still impersonates. Run the switch command above. | | Live calls fail with `Unauthorized` | The API server doesn't trust the token: the hub signs in with a different issuer or client ID than the API server trusts. | | Everything is forbidden, but the same person has access through other tools | The hub's prefixes or groups claim differ from the API server's, or the API server's username claim isn't `email`. | | Lists show less than RBAC allows, but actions work | A group was dropped from Radar's checks on cached reads: its name has spaces or commas, or is over 128 bytes with the prefix. | | Sessions end after about an hour | No refresh token. Add `offline_access` to `auth.oidc.scopes` (Okta, Entra ID). | # SSO (SAML / OIDC) Source: https://radarhq.io/docs/cloud/sso Self-serve SAML and OIDC setup via the WorkOS Admin Portal. Owner-only. Once configured, users in your IdP authenticate without going through email magic-link. SAML / OIDC SSO is available on the **Enterprise** and **Custom** plans (see [Billing & plans](/docs/cloud/billing)). Orgs that subscribed to a plan that included SSO before the current pricing keep it - grandfathered plans keep their features. Radar Cloud uses [WorkOS](https://workos.com) for SSO. Owners configure their IdP self-serve via the WorkOS Admin Portal - Okta, Azure AD, Google, OneLogin, generic SAML, generic OIDC. On save, WorkOS provisions the connection and notifies the Radar Cloud control plane. ## Two ways to route a sign-in to your IdP * **By email domain.** A person types their address on the sign-in page; if its domain is one your organization has verified, they are sent to your IdP. Set up below. * **By your organization's link.** With SSO configured, `app.radarhq.io/login/` shows a sign-in page for your organization and sends whoever signs in there to your IdP, whatever their email domain - no domain to verify. See [Per-organization sign-in link](#per-organization-sign-in-link). Most organizations use both. Which path applies to your situation is laid out in [Sign-in and access](/docs/cloud/access-overview). Connecting SSO adds a door; it does not close the Cloud sign-in page for members who already have an account. A **Require single sign-on** setting is not available yet. ## Route by domain: verify your email domain SSO routing needs your organization's email domain. Add it under **Settings → Organization → Domains** (owner-only) *before* opening SSO setup - e.g. `company.com` for users signing in as `alice@company.com` - and verify it: the page shows a DNS-TXT record (`verification_token=...`) to publish at the domain you are verifying - for a subdomain, the guided setup gives the exact DNS host - and checks it in the background. A verified domain powers both SSO routing and [domain auto-join](/docs/cloud/members); a domain that is pending or failed is claimed but inert until the record is in place. Already opened SSO setup before adding a domain (or added one later)? Add and verify the domain on the Domains page; it is registered with WorkOS when you add it, and its verification state refreshes from there. You don't need to reopen SSO setup. ## Set up SSO **Settings → Organization → SSO** (owner-only). Click **Open SSO setup** (the button reads **Manage SSO** once a connection already exists): 1. Radar lazy-creates a WorkOS organization for you (one per Cloud org) and opens the WorkOS Admin Portal in a new tab. 2. Pick your IdP from the menu. WorkOS will walk you through the IdP-specific steps: * For SAML: SAML metadata URL or XML upload, attribute mapping. * For OIDC: discovery URL, client ID / secret. * For named IdPs (Okta, Azure AD): OAuth-style consent flow that handles the rest. 3. Save. The **SSO** card flips to **Connected**, with the IdP name and the last sign-in that used it, after the first successful sign-in through the connection - that sign-in is what tells Radar the connection works. After setup, users from your IdP sign in at **app.radarhq.io/login** by typing an address at a verified domain, or through your organization's sign-in link. A new user whose sign-in WorkOS routes to your organization joins it with the **Default invite role** on first sign-in - no invitation needed; an existing member keeps their role. ## Domain matching SSO sign-ins are routed by email domain. The domains come from **Settings → Organization → Domains** and must match the domain(s) your IdP issues identities for - list and verify every domain your users sign in with. A domain is registered with WorkOS when you add it and routes sign-ins once verified; removing a verified domain stops routing its sign-ins through SSO and stops its auto-join. See [Members](/docs/cloud/members) for how the same list drives domain auto-join, and [SSO for external users](/docs/cloud/sso-external-users) for contractors who sign in through your IdP with another employer's address. ## Per-organization sign-in link Alongside domain routing, each org has a readable sign-in link that names the organization: `app.radarhq.io/login/` (e.g. `app.radarhq.io/login/acme-corp`). Opening it shows a **"Sign in to Acme Corp"** page and routes straight to your IdP - the user doesn't type a domain or pick an organization. Manage it under **Settings → Organization → SSO**: the sign-in link card shows the link with a **Copy** button and lets you rename the **handle**. The handle is generated from your org name and is editable; handles are unique across Radar. The link is a **routing hint only** - it decides which IdP to send the user to, never who they are. The sign-in callback still derives the real identity from your IdP's response. A wrong or stale handle falls through to the ordinary sign-in page with a notice that the link isn't valid; a handle another organization has since taken routes to that organization's sign-in, where your IdP is not involved. This link is what makes SSO work for users whose email domain isn't yours - see [SSO for external users](/docs/cloud/sso-external-users). ## Cluster access for new clusters With SSO connected, **Settings → Organization → SSO** has a **Cluster access** setting (owner-only) that decides how **new** clusters grant Kubernetes access: * **Hub roles** (default) - the chart binds each Radar role to a Kubernetes role: owners get `admin`, members `edit`, viewers `view`. * **IdP groups** - new installs leave those bindings out, and only the bindings you write in Kubernetes RBAC grant access (usually to `radar:idp:`). The install page shows a starter binding to apply. Until one exists, nobody - including you - sees the cluster's resources in Radar; the cluster still connects. Existing clusters are never changed. The setting applies to fresh installs only. When you connect a cluster that already runs Radar, turn on **Already running Radar in this cluster?** in the connect wizard to keep its bindings: the default command is a fresh install, and on an existing release it removes the tier bindings. If no IdP groups reach your clusters from your own session, the setting warns you: usually the `groups` attribute mapping is missing (see [IdP group-based access](/docs/cloud/idp-groups#2-emit-a-stable-group-claim-from-your-idp)). With IdP groups selected, the page also shows how to switch an existing cluster and lists clusters whose Radar is too old for it. See [RBAC under Cloud mode](/docs/cloud/rbac#choosing-a-governance-model). Your Radar role (owner / member / viewer) still decides what someone can do **in Radar** - members, billing, settings - in both modes. ## What about JIT / SCIM provisioning? * **JIT (just-in-time provisioning)** - works today. The first time a user signs in via SSO, they're created as a Cloud user, and a sign-in WorkOS routes to your organization joins it with the default role (domain auto-join does the same for verified domains). * **SCIM** - directory-based user / group sync. Included in the Enterprise and Custom plans; the provisioning endpoints are rolling out. Contact us if you need SCIM enabled for compliance today. ## Disconnecting SSO Owners can manage the connection through the WorkOS Admin Portal (the same link the SSO card opens). Removing the connection there stops new SSO sign-ins; existing sessions remain valid until cookie expiry. Disabling a user in your IdP does not remove their Radar organization membership automatically. Remove them from **Settings → Organization → Members** as well, and [revoke their credentials](/docs/cloud/members#removing-a-member). Existing requests and streams are not terminated. ## Audit The [audit log](/docs/cloud/audit-log) records SSO portal launches as `org.sso.portal_opened` and membership provisioning as `org_member.sso_joined`. Changes inside the WorkOS portal do not emit a separate configuration-completed event. ## Plan availability SAML / OIDC SSO is included in the **Enterprise** and **Custom** plans. Orgs grandfathered on a legacy plan that included SSO keep it. Google / GitHub sign-in works on every plan, including Free. ## See also * [Sign-in and access: which path applies](/docs/cloud/access-overview) - how sign-in and membership combine. * [Organizations & roles](/docs/cloud/organizations). * [Members & invitations](/docs/cloud/members) - including domain auto-join. * [IdP group-based access](/docs/cloud/idp-groups) - reuse your IdP groups to grant Kubernetes access. * [SSO for external users](/docs/cloud/sso-external-users) - contractors and guests who keep their employer email. * [Audit log](/docs/cloud/audit-log). # SSO for external users Source: https://radarhq.io/docs/cloud/sso-external-users Onboard contractors, partners, and guest users through SSO - guests who keep their own employer email sign in through your identity provider with their real address, using a per-organization sign-in link. You already give external contractors and partners access to your systems as **guests in your own identity provider** - added to your Microsoft Entra (or Okta, or generic OIDC / SAML) tenant, but signing in with their employer's email like `dana@partner.com`. This page shows how those same guests sign into Radar Cloud through your IdP, with their **real employer address**, and land with exactly the access their IdP group grants. It builds on three pages you should read first: * [SSO](/docs/cloud/sso) - the OIDC / SAML connection this rides on. Either protocol works here; pick whichever fits how you run your IdP. * [IdP group-based access](/docs/cloud/idp-groups) - how an IdP group becomes Kubernetes access. * [RBAC under Cloud mode](/docs/cloud/rbac) - the deny-by-default posture external users need. ## Why the standard path can't route a guest Radar routes each sign-in to the right organization by the **domain of the email** the person signs in with. That works for your own staff on `example.com`. It breaks for a guest who keeps their employer address: 1. The guest's domain (`partner.com`) isn't yours, so the normal login can't match them to your organization. 2. Their address is outside your verified domains, so the sign-in service cannot take your IdP's word for it and asks the guest to confirm it with a one-time code. Two things address this, and the setup below covers them: * A **per-organization sign-in link** that names your org, so routing never depends on the guest's email domain. * **The emailed code.** Radar collects it and completes the sign-in, so an out-of-domain guest is admitted on their real address (see [The email code](#the-email-code)). > [!NOTE] > > The end-to-end path: the guest opens your link, Radar routes to your IdP, they authenticate there, confirm their email with a one-time code, and land in Radar with their real address and their group access. ## What an external user experiences It feels like signing into any tool that uses their company login - no Radar password, no account to create. * They open your readable link, `app.radarhq.io/login/acme-corp`, which shows a **"Sign in to Acme Corp"** page so they know they are in the right place. * Clicking through routes them straight to your IdP sign-in - no "which organization?" picker, no domain to type. * They enter the code emailed to their address on the **"Enter your verification code"** screen (see [The email code](#the-email-code)). * They land in Radar as a normal member carrying the IdP groups they were assigned. ## Set it up A one-time setup by an owner, then one step per contractor. > [!NOTE] > > SSO is a paid feature, on the **Enterprise** and **Custom** plans (see [Billing & plans](/docs/cloud/billing)). Until your org is on a plan that includes it, **Settings → Organization → SSO** shows only an upgrade prompt - the connection setup, the org handle, and the sign-in link card won't appear yet. Upgrade first, then the steps here become available. ### 1. Connect SSO and send the groups claim Follow [SSO](/docs/cloud/sso) end to end to connect your IdP. OIDC is the recommended default; SAML works the same way for this page. Then configure the **groups claim** so each guest's security-group membership reaches Radar - see [IdP group-based access](/docs/cloud/idp-groups) for the claim setup and the RBAC recipes. Without the groups claim, a guest signs in but no group reaches the cluster and their group-based access stays empty. Two Entra details that catch people, whichever protocol you chose: * **Send groups by ID.** Return security groups by the immutable object ID (GUID), not the display name, under a claim named plain `groups`. Then, in Radar's SSO setup portal, open **Attribute mappings** and map `groups` → `groups`; without that mapping the claim is silently ignored. * **Let the contractor use the app.** If the enterprise app has **Assignment required = Yes** (check **Properties**), anyone not assigned is rejected by Entra before any sign-in reaches Radar. Either assign the contractor's group under **Users and groups**, or set it to **No**. The onboarding steps below assume the group is assigned. > [!TIP] > > SSO setup involves long round-trips to your IdP admin console, and the hosted setup link can time out while you are away. If it shows "This page isn't available," reopen **Manage SSO** from **Settings → Organization → SSO** to get a fresh link and continue. ### 2. Set your organization handle and share the link In **Settings → Organization → SSO**, the sign-in link card shows your readable link, `app.radarhq.io/login/`, and lets you rename the handle. Copy it and hand it to contractors - it routes them straight to your IdP regardless of their email domain. The normal login page cannot route an out-of-domain guest, so they must use this link. See [SSO](/docs/cloud/sso#per-organization-sign-in-link) for how the handle works. > [!WARNING] > > Renaming the handle changes the link. The old one stops routing to your organization - it lands on the ordinary sign-in page with "This sign-in link isn't valid", or, if another organization later takes that handle, on their sign-in page - and neither can route your guest. If you have already shared the link with contractors, re-share the new one. ### 3. Lock down default access External users should get **only** what their IdP group grants. Getting there is two decisions - the **Cloud role** they land with, and the **chart's default tier bindings** - and both matter. **A contractor's baseline access follows their Cloud role, not their groups.** The chart binds each role to a Kubernetes tier out of the box: `radar:owner` → `admin`, `radar:member` → `edit` (writes), `radar:viewer` → `view`. So the group-only outcome only holds if the contractor lands in a tier you have actually turned off. > [!WARNING] > > Disabling the **viewer** tier alone is not enough if contractors land as **members**. A contractor who joins through your link gets your org's **Default role** (Settings → Organization → General, under "How people join"), which is typically `member` - and a member keeps the `radar:member` → `edit` binding no matter what the viewer flags say. Put the group-only population in a tier you disable: > > * Either set the **Default role** to **viewer** before they first sign in (a contractor joins through the link with that role, and an invitation accepted afterwards does not change it; you can also change their role on the Members page), then disable the viewer tier, **or** > * if members should also be group-only, disable the member tier too: `cloud.defaultRbac.member: false`. Then turn off that tier in your `radar` Helm chart values, so a contractor lands in Radar but sees nothing until a group grants it. Disabling a tier removes both of its default bindings - the namespaced role and the cluster-wide infra read (Nodes, PVs, ...) - and the flag lives **under `cloud:`**: ```yaml theme={null} cloud: defaultRbac: viewer: false # no radar:viewer -> view binding, and no cluster-wide infra read for viewers ``` Set it with `--set cloud.defaultRbac.viewer=false` if you prefer. Note the `cloud:` parent: Helm ignores mistyped keys silently, so a wrong path looks like it applied but leaves the contractor with `view` plus cluster-wide read. (`cloud.defaultRbac.clusterScopedRead.` is a separate switch for keeping a tier on but dropping only its infra read; it is not needed here.) The flags, the per-tier model, and the full-IdP governance option are in [RBAC under Cloud mode](/docs/cloud/rbac#overriding-the-defaults) and [IdP group-based access](/docs/cloud/idp-groups#4-bind-the-groups-in-kubernetes-rbac). > [!NOTE] > > To leave the tier bindings out of **every** new cluster instead of editing each one, set **Settings → Organization → SSO → Cluster access** to **IdP groups** ([SSO](/docs/cloud/sso#cluster-access-for-new-clusters)). Either way, Radar Cloud's own background identity (`radar:system`) keeps a read-only binding for alerts and the timeline; contractors never carry that group. > [!NOTE] > > These are cluster-install settings in the chart values where Radar runs, not a hub toggle. The Cloud role is set in Radar (the invite you send, or the **Default role** under Settings → Organization → General); the tier bindings are set in the chart. ### 4. Know what SSO does not do yet Connecting SSO adds a door; it does not close the Cloud sign-in page for members who already have an account. A **Require single sign-on** switch is not available yet. Until it is, the lock-down in step 3 - default Kubernetes bindings turned off, group bindings carrying the real permissions - limits what a stray sign-in can reach in your clusters; Cloud-side permissions (members, billing, SSO, cluster lifecycle) still follow the Cloud role the account holds. ## The email code A contractor's address is outside the domains you verified, so the sign-in service cannot accept your IdP's assertion of it on its own. After they authenticate with your IdP, it emails them a one-time code; they enter it on the **"Enter your verification code"** screen, and Radar completes the sign-in. Expect it on the first sign-in, and it may be asked for again on later ones. > [!NOTE] > > A guest whose IdP account is passwordless (for example a personal Microsoft account added as a guest) may also receive a sign-in code from **Microsoft** - that one is your IdP's own step, separate from Radar's, and is controlled on your side. ## Alternative: land guests on a subdomain you own If you would rather guests never see the code, or you want their Radar identity to live on your own domain, your IdP can rewrite a guest's address onto a subdomain you have verified in Radar (`ext.example.com`). An address at a verified domain, asserted by your connection, is admitted with no challenge. The costs: the recipe below is a **SAML** claim transformation in the Entra UI (we have not documented an OIDC equivalent), the subdomain has to be verified under **Settings → Organization → Domains**, and the contractor's real address then travels as a separate claim so Radar can show it. Everything else on this page - the link, the lock-down, onboarding - is the same. > [!NOTE] > > The subdomain only needs to **exist and be verified** - it is a subdomain of a domain you already own. You do **not** create mailboxes on it, and no mail is ever delivered to it. The rewritten addresses (`dana-partner.com@ext.example.com`) are sign-in identifiers only; each contractor's real, reachable address is kept separately on file. Leave the subdomain unverified and a rewritten guest is challenged to prove an address that receives no mail, and cannot get in. The examples use Microsoft Entra; any SAML IdP works the same way. Open the Radar enterprise application in your tenant: 1. **Pick SAML.** Enterprise applications → your Radar app → **Single sign-on → SAML**. Paste the **Identifier** and **Reply / ACS URL** from Radar's SSO setup screen into the Basic SAML Configuration. 2. **Split the email claim by user type.** In **Attributes & Claims**, edit the email address claim so members keep their real address and guests get the rewrite: | User type | Source | Value | | - | - | - | | Members | Attribute | `user.mail` | | All guests | Transformation | rewrite onto your subdomain (next step) | Order matters - the guest rule must not sit under a broader rule that would catch guests first. 3. **Build the rewrite, and keep it unique per guest.** The rewritten address becomes the guest's identity, so it must be distinct for every guest - fold their **whole** employer address into the local part, not just the mailbox name, so two people are never merged. Entra's only transformation that can do this is **`RegexReplace()`** (there is no plain `Replace()`). On the guest row, set: | Field | Value | | - | - | | Transformation | `RegexReplace()` | | Attribute name | `user.mail` | | Regex pattern | `(?'local'^[^@]+)@(?'domain'.+)$` | | Replacement pattern | `{local}-{domain}@ext.example.com` | That turns `dana@partner.com` into `dana-partner.com@ext.example.com`, and `dana@other.com` into `dana-other.com@ext.example.com`, which keeps the two apart. It is not collision-proof in theory - `a-b@c.com` and `a@b-c.com` both become `a-b-c.com@ext.example.com` - so if that matters to you, use a longer separator that is unlikely in a real address, such as `_at_`. Use Entra's **Test transformation** to confirm the output before you save. > [!WARNING] > > Do **not** rewrite to just the mailbox name (`ExtractMailPrefix(user.mail)` on its own). Two guests with the same name at different employers - `dana@partnerA.com` and `dana@partnerB.com` - would both become `dana@ext.example.com` and collapse into a **single** Radar identity: whoever signs in second lands in the first person's account. Folding the source domain into the local part keeps guests apart. 4. **Send the groups claim** as in step 1 above - by object ID, under a claim named plain `groups`. By default Entra emits it under a namespaced URI (`http://schemas.microsoft.com/ws/2008/06/identity/claims/groups`); in the group claim's **Advanced options**, tick **Customize the name of the group claim** and set the name to `groups` with the namespace left blank. 5. **Carry the real address.** Add one more claim, source **Attribute `user.mail`**, with no condition, so Radar keeps each contractor's real address. Set its **Name** to `contactEmail` with the **Namespace left blank** - the same rule as the group claim, so it emits as plain `contactEmail` and matches the mapping in the next step (a claim under a namespace URI won't match). 6. **Map the attributes on the Radar side.** In Radar's SSO setup portal, open **Attribute mappings → Edit attribute mappings** and set both, or they silently stay empty and nothing flows: * `groups` → `groups` (without this, no group reaches the cluster and RBAC is inert). * `contact_email` → `contactEmail` (without this, the real address never appears in Members). > [!NOTE] > > The setup portal may suggest creating `groups`/`contactEmail` as Microsoft Graph directory-extension properties. You don't need that path - the SAML claims you added above are enough. Map the attributes to those claim names and skip the Graph API. On this path the contractor signs in as `dana-partner.com@ext.example.com`, Radar shows `dana@partner.com` from the `contactEmail` claim on top of it in **Members**, and an invitation sent to the real address matches through that claim. The deployment's WorkOS JWT template must map the claim to `contact_email` for that to work; it does on Radar Cloud. ## Onboard and offboard Per contractor, the whole flow is three steps you already know: 1. Add them as a **guest** in your IdP tenant - they keep their employer email, the same way you bring in any partner. 2. Put them in the **security group** that maps to the namespaces they should reach. 3. Send them your per-tenant link (`/login/`). That is all they need - no invite to accept, no password to manage. To offboard, do both: * **Remove them in your IdP** - from the **group** to drop their group access (it takes effect when their Radar session next refreshes its token, not instantly), or from the **tenant** to stop them authenticating through your IdP at all. * **Remove them from Settings → Organization → Members.** Their Radar account and membership outlive your IdP: the same person can still reach the Cloud sign-in page with an emailed code to their address, and an existing membership admits them with whatever Cloud role it holds. > [!WARNING] > > Neither step is enough alone. Removing them from **Members** ends the membership, but whoever your IdP admits through your organization's link joins again on their next sign-in, with the default role - so if they are still a guest in your tenant and still assigned to the app, they are back in. Removing them in the **IdP** stops that, but leaves the membership they already have. ## Tokens and AI tools IdP groups travel with the **signed-in session** - the web app and its built-in AI assistant carry them to the cluster. A **personal access token** does not: anything that authenticates with one (a script, an external AI agent connected over MCP) reaches the cluster with the Cloud role's bindings and any binding on the user itself, but nothing granted through a group. A contractor whose access comes entirely from IdP groups gets that access in the web app, not through a token. ## The real address on file Contractors sign in with their real employer address, so that address is what appears in **Settings → Organization → Members** and what an invitation sent to their employer email matches against. If your IdP also asserts a separate `contactEmail` claim (the [subdomain alternative](#alternative-land-guests-on-a-subdomain-you-own) relies on it), Radar keeps it and shows it on top. See [Members](/docs/cloud/members#contact-email-for-external-users). ## What each account carries | | Employee (member) | Contractor (guest) | | - | - | - | | Email Radar signs them in with | `dana@example.com` | `dana@partner.com` (or `dana-partner.com@ext.example.com` on the subdomain alternative) | | Email-verification code | none - the domain is verified | yes - the domain is not yours (none on the subdomain alternative) | | Cloud role | `member` (or whatever you assign) | a tier you have disabled, so baseline is nothing | | Access | their `member` tier (`edit`) plus any groups | their groups only (after step 3) | Once you have locked down default access (step 3) and no other binding names them, a contractor with no matching IdP group lands in Radar but sees nothing until they are added to a group. That is intended - access follows group membership, managed in your IdP. This holds only when the contractor's Cloud role tier has its default binding turned off; otherwise they inherit that tier's access (for example a `member` keeps `edit`) regardless of their groups. ## Requirements for your Radar admin * The IdP connection (OIDC or SAML) must send a **groups claim**, mapped in the SSO setup portal, for group-based access to work (see [IdP group-based access](/docs/cloud/idp-groups)). * The `radar` Helm chart values control baseline RBAC in the cluster where Radar runs. The default tier bindings are **on**; deny-by-default is what you opt into by disabling them (step 3). ## Plan availability SSO is included in the **Enterprise** and **Custom** plans (see [Billing & plans](/docs/cloud/billing)). Orgs grandfathered on a legacy plan that included SSO keep it. ## See also * [SSO](/docs/cloud/sso) - OIDC / SAML self-serve and the per-organization sign-in link. * [IdP group-based access](/docs/cloud/idp-groups) - the groups claim and the RBAC recipes external users rely on. * [RBAC under Cloud mode](/docs/cloud/rbac#locking-down-for-security-conscious-orgs) - deny-by-default in depth. * [Members & invitations](/docs/cloud/members) - the real address on file and invitation matching. # Authentication Source: https://radarhq.io/docs/configuration/authentication Radar's two auth modes - proxy headers and OIDC. Writes run as the user via Kubernetes impersonation; reads are authorized by your namespace access. Radar supports optional user authentication with per-user authorization powered by Kubernetes RBAC. When enabled, each user sees only the namespaces they have access to, and write operations (restart, scale, exec, Helm, etc.) are executed with the user's identity - so K8s RBAC controls what each user can do. > **No auth by default.** When running locally or without `--auth-mode`, everything works exactly as before - no login, no restrictions. ## How It Works ``` User → [Auth Layer] → Radar Backend → K8s API (as user, via impersonation) ``` 1. **Authentication** identifies the user (proxy headers or OIDC login) 2. **Reads** are filtered by namespace - Radar discovers which namespaces the user can access via `SubjectAccessReview` and only returns resources from those namespaces. Cluster-scoped resources (Nodes, PersistentVolumes, StorageClasses, ClusterRoles when `rbac.viewRBAC` is on, cluster-scoped CRDs, etc.) have no namespace to filter on, so they are gated per-kind via `SubjectAccessReview`: a user sees them only if their own RBAC permits listing that kind. Cluster-wide pod visibility does **not** imply Node/PV visibility - each cluster-scoped read goes through its own check. 3. **Writes** use K8s impersonation - Radar makes the K8s API call as the authenticated user, so K8s RBAC decides whether it's allowed 4. **UI adapts** - capability checks run per-user, so buttons (exec, restart, scale, Helm) only appear if the user has permission Radar doesn't have its own role/permission system. It delegates everything to K8s RBAC, which means permissions are managed with standard K8s tooling (`kubectl`, Terraform, GitOps, etc.). ### Read granularity: namespace-level For **namespaced** resources, reads are authorized at **namespace granularity**: if your RBAC lets you access a namespace, Radar shows the namespaced resources in it. Radar does **not** additionally check per-resource-kind RBAC for most namespaced kinds - so a user who can read Pods in a namespace also sees ConfigMaps, Services, etc. in that namespace through Radar, even if their RBAC wouldn't allow `kubectl get configmaps` directly. This is a deliberate tradeoff that follows from how Radar reads the cluster. Rather than calling the Kubernetes API on every request, Radar watches each resource kind once under its own ServiceAccount and serves reads from an in-memory cache shared by all users - that's what makes it fast on large clusters and able to render topology and real-time views. Because the cache is populated by Radar's identity, not yours, per-user access has to be re-derived by asking Kubernetes "is this user allowed?" via `SubjectAccessReview`. Doing that at namespace granularity is one such check per namespace; doing it per-resource-kind would be a check for *every kind in every namespace* on *every* read - many times the API-server load, and worst exactly on the large, multi-namespace clusters where the shared cache matters most. So namespace membership is the per-user filter, and per-kind checks are reserved for the cases below where the exposure is highest. These kinds are gated more tightly, per-resource-kind, because the shared cache can hold data the user's own RBAC wouldn't grant: * **Secrets** (and Secret-derived data such as TLS certificate metadata) - shown only if the user can list Secrets in that namespace. * **Trivy Operator reports** (the `aquasecurity.github.io` group: VulnerabilityReports, ExposedSecretReports, ConfigAuditReports, SbomReports and the rest): shown only if the user can list that report kind in that namespace, and across all namespaces at once only with a cluster-wide grant. An ExposedSecretReport holds the secret Trivy found in an image, so reports are treated like Secrets in lists, detail pages, counts, search and the MCP tools. * **Cluster-scoped resources** (Nodes, PersistentVolumes, ClusterRoles, cluster-scoped CRDs, etc.) - shown only if the user's RBAC permits listing that kind. Issues follow the same rule: you see an issue only if you could open its resource. A namespaced resource needs namespace access, a Secret also needs Secret access, and a cluster-scoped resource needs list on its kind. A cluster-scoped resource's issues are composed across namespaces, so a Node's issues can draw on Pods anywhere, but only rows from namespaces you can access feed them: another namespace's Pods never appear among its members or in its blast radius. Issues roll up to a resource's owner, such as a Pod's Deployment, only if you could open the owner; otherwise the issue stays on the resource. A finding drawn from a Secret's history reaches you without that history if you can't read the Secret: a Pod still running with environment values from a Secret that changed is flagged, with the restart it needs, but not which Secret or keys changed, or when. A finding that something your resource names does not exist (an Ingress's TLS Secret or IngressClass, a PVC's StorageClass, a RoleBinding's ClusterRole) is shown with namespace access: it says only that a name you can already read is missing, and Kubernetes reports a missing StorageClass in the PVC's events too. The Home dashboard's problem list and the MCP tools follow the same rules. The per-resource Issues view says when something was withheld, rather than showing an empty list as "none". **If namespace-level isn't tight enough for you**, scope the boundary at the cache instead of at read time: run a Radar instance per trust boundary and give each one a **namespace-scoped ServiceAccount** (a `Role`/`RoleBinding`, no `ClusterRole`) limited to that boundary's namespaces. Radar detects the restricted permissions at startup and only watches and caches what its ServiceAccount can list - so the instance simply never holds another team's data, and there's nothing to over-expose. Point each team at their own instance (an ingress or auth proxy can route them). See [In-Cluster Deployment → namespace-scoped RBAC](/docs/configuration/in-cluster) for the `rbac.create: false` + custom `Role` setup. Deletion-impact previews require read access to the root resource and use the caller's visible topology. Related resources describe observed management relationships, not a guarantee of Kubernetes garbage collection. Policy coverage metadata is filtered alongside its referenced workloads; it does not reveal names from hidden namespaces. ### Image inspection Authenticated requests (proxy, OIDC, or Radar Cloud) to `/api/images/metadata`, `/api/images/inspect`, and `/api/images/file` must name a `pod` and `namespace` (400 otherwise), so Radar only contacts the registry of an image a Pod runs. The caller must have `get pods` in that namespace, and the image must match a regular, init, or ephemeral container's spec image or status imageID digest reference. Authorization failures deny access; an unavailable authorization check returns 503. Registry credentials come from that Pod's `spec.imagePullSecrets`, each used only if the caller can `get` that exact Secret or `create pods/exec` in the namespace. Radar's own registry credentials (its Docker config and Google application default credentials) are used only for callers with `create pods/exec`, and only to fetch the digest the kubelet recorded as pulled for one of the Pod's containers. This covers images pulled with the node's cloud identity and no pull secret, such as Artifact Registry on GKE. Denied or non-authoritative checks drop the credential, with anonymous fallback. The `pullSecrets` query parameter and ServiceAccount pull secrets are ignored. Every registry request, including redirects and token endpoints, is checked before Radar connects: link-local addresses (cloud metadata) are refused, and loopback is refused in-cluster (locally it is allowed only for the registry the Pod's image names). Private addresses are allowed, because self-hosted registries often serve tokens or layers from a second internal host. Credentials are sent only over HTTPS, as the node's container runtime does by default; a registry that needs credentials over plain HTTP can't be inspected. Behind an HTTP proxy, a request Radar can't pin to an address it has checked is refused rather than left to the proxy. A refused request returns 403. Downloaded layers are cached for a few minutes and shared by all users, keyed by the image's repository and digest, so a cached image is only served after the caller has read its manifest from that same repository. Without an authenticated user, local image inspection keeps its existing credential discovery and network behavior. ### Prometheus metrics Charts of a specific resource are gated like the resource itself: you see them only if you can read what they chart. The surfaces that can't be tied to one resource - raw PromQL, metric discovery, alert rules and the cluster-wide aggregate - are gated on listing Pods in **every** namespace, because a PromQL expression can't be namespace-filtered by inspecting it. Kubernetes' built-in `view` ClusterRole meets that bar, so binding `view` also grants read access to everything the Prometheus backend holds, `kube-system` and node-level series included. Two consequences worth knowing before you deploy: * A Role scoped with `resourceNames` doesn't pass the check, which needs the whole kind. It fails closed, so the user sees less rather than more. * Radar queries Prometheus with its own credentials; only the *authorization* is per user. If several tenants share one Prometheus, that backend needs its own isolation - Radar's gate isn't what separates them. ## Auth Modes | Mode | Flag | When to Use | | - | - | - | | `none` | `--auth-mode=none` | Local use, single-user, no restrictions needed (default) | | `proxy` | `--auth-mode=proxy` | You already have an auth proxy (oauth2-proxy, Pomerium, Cloudflare Access, etc.) in front of your ingress | | `oidc` | `--auth-mode=oidc` | You want Radar to handle login directly - no separate auth proxy needed | **How to choose:** If your organization already routes internal tools through an auth proxy or SSO gateway, use **proxy** mode - it's zero config on the Radar side. If you want a standalone deployment with built-in login, use **OIDC** mode. ### Proxy Mode Use this when you already have (or plan to deploy) an auth proxy like [oauth2-proxy](https://oauth2-proxy.github.io/oauth2-proxy/), Pomerium, Authelia, or similar. The proxy authenticates users and forwards their identity to Radar via HTTP headers. **Flow:** ``` Browser → Auth Proxy → Radar sets X-Forwarded-User: alice@company.com sets X-Forwarded-Groups: sre-team,platform-eng ``` **Helm values:** ```yaml theme={null} auth: mode: proxy # Optional: customize header names (these are the defaults) # proxy: # userHeader: X-Forwarded-User # groupsHeader: X-Forwarded-Groups ``` **CLI flags:** ```bash theme={null} radar --auth-mode=proxy \ --auth-user-header=X-Forwarded-User \ --auth-groups-header=X-Forwarded-Groups ``` > **Security:** Your ingress must strip `X-Forwarded-User` and `X-Forwarded-Groups` headers from external requests to prevent spoofing. The auth proxy should be the **only** path to Radar. Radar logs a warning at startup as a reminder. > **Proxies:** Preserve the browser-facing `Host` header when proxying Radar. Radar accepts changes and pod exec sessions only from its own page, and it recognizes that page by comparing the browser's `Origin` with `Host` (or by `Sec-Fetch-Site`, which browsers send over HTTPS). If your proxy must rewrite `Host`, list the address users open Radar at in `trustedOrigins` (Helm) or `RADAR_TRUSTED_ORIGINS`. See [Changes fail with "Radar refused this request"](/docs/configuration/in-cluster#changes-fail-with-radar-refused-this-request). > **Local terminal:** The host-level local terminal requires both a loopback-bound Radar listener and a loopback URL, and is unavailable when authentication is enabled. Unlike pod exec, it runs as the Radar process's operating-system user and cannot be safely impersonated per caller. **Logout behavior:** The user menu always shows a **Logout** button. Clicking it clears Radar's session cookie. On its own, that isn't enough to switch users: your proxy re-injects the identity header on the next request and signs the same user back in. To make logout actually switch users, point Radar at your proxy's sign-out URL with `--auth-proxy-logout-url` (or `auth.proxy.logoutURL` in Helm) - e.g. oauth2-proxy's `/oauth2/sign_out`. Radar then redirects the browser there after clearing its own cookie, so the upstream session is torn down too. When unset, the menu shows a note that the proxy may re-authenticate automatically. > **Note:** HTTP Basic Auth has no reliable logout mechanism - browsers cache credentials and resend them. If you need user-switching, prefer a proxy with a real sign-out endpoint (oauth2-proxy, Authelia) over plain Basic Auth. ### OIDC Mode Use this when you want Radar to handle login directly - no separate auth proxy needed. Radar redirects to your identity provider (Google, Okta, Dex, Keycloak, etc.), validates the token, and creates a session cookie. We recommend registering a dedicated OIDC client for Radar rather than reusing the client ID your Kubernetes API server trusts. Radar's session holds the user's ID token for logout, so with a shared client a leaked Radar session could also be used against the API server. **Flow:** ``` Browser → Radar → redirects to IdP → user logs in → callback → session cookie ``` **Helm values:** ```yaml theme={null} auth: mode: oidc secret: "" # HMAC key for session cookies (auto-generated if empty, but sessions won't survive pod restarts) oidc: issuerURL: https://accounts.google.com # Your OIDC provider clientID: your-client-id clientSecret: your-client-secret redirectURL: https://radar.example.com/auth/callback # usernameClaim: preferred_username # Default: email, falling back to sub groupsClaim: groups # JWT claim containing group membership # scopes: ["openid", "profile", "email", "groups"] # Default - uncomment to override (e.g., drop "groups" for Google) ``` **Scopes:** by default Radar requests `openid profile email groups` at the authorization endpoint. The `groups` scope is required by Dex, Keycloak, and most IdPs to actually include the groups claim in the ID token. If your IdP rejects unknown scopes (Google in particular doesn't define `groups`), override via `auth.oidc.scopes` / `--auth-oidc-scopes` to drop it or substitute the provider-specific equivalent. **Username claim:** by default Radar uses the token's `email` claim as the Kubernetes username, falling back to `sub`. If your API server authenticates the same users with OIDC, set `auth.oidc.usernameClaim` / `--auth-oidc-username-claim` to the claim it maps (`--oidc-username-claim` or `claimMappings.username.claim`), and `usernamePrefix` to its prefix. Otherwise RBAC bindings that work in `kubectl` won't match in Radar. When the claim is set and missing from a token, Radar refuses the login rather than falling back to another claim. **Unverified email:** when the username comes from `email` and the token's `email_verified` claim is `false`, Radar refuses the login, the same way kube-apiserver rejects such a token. Tokens without `email_verified` are accepted. If your users' addresses are not verified at the IdP, verify them there or set `usernameClaim` to a claim that doesn't depend on email, such as `sub`. Radar checks this only at sign-in. If you set a fixed `auth.secret` or `auth.existingSecret`, sessions from before the upgrade keep working as long as they stay active; change the secret to sign everyone out. **Split public/internal provider URLs:** Kubernetes deployments sometimes need the browser to use the canonical issuer URL while the Radar pod talks to the IdP through service DNS. Keep `issuerURL` set to the canonical browser-facing issuer; Radar still validates the token `iss` claim against that value. Set `internalIssuerURL` when the internal endpoint has the same path layout, and Radar will fetch discovery internally while deriving server-side token, userinfo, and JWKS URLs from the internal base: ```yaml theme={null} auth: mode: oidc oidc: issuerURL: http://authentik.example.com/application/o/radar/ internalIssuerURL: http://authentik-server.authentik.svc.cluster.local/application/o/radar/ clientID: radar redirectURL: https://radar.example.com/auth/callback ``` If the endpoints cannot be derived from the two issuer bases, override them explicitly. `authorizationURL` is browser-facing; `tokenURL`, `userInfoURL`, and `jwksURL` are server-side URLs used by the Radar process: ```yaml theme={null} auth: mode: oidc oidc: issuerURL: http://authentik.example.com/application/o/radar/ authorizationURL: http://authentik.example.com/application/o/radar/authorize/ tokenURL: http://authentik-server.authentik.svc.cluster.local/application/o/radar/token/ userInfoURL: http://authentik-server.authentik.svc.cluster.local/application/o/radar/userinfo/ jwksURL: http://authentik-server.authentik.svc.cluster.local/application/o/radar/jwks/ ``` **Logout behavior:** When a user clicks logout, Radar clears the local session cookie and - if the identity provider supports it - redirects the browser to the provider's logout endpoint ([RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)) to terminate the SSO session as well. This prevents the common issue where the user appears to log out but is silently re-authenticated on the next visit. Radar discovers the provider's `end_session_endpoint` automatically from the OIDC discovery document at startup. If the provider supports it (Okta, Keycloak, Azure AD), Radar redirects the browser there to terminate the SSO session. If the provider doesn't advertise `end_session_endpoint` (e.g., Google), Radar uses `prompt=login` on the next authorization request to force the IdP to show a login screen instead of silently re-authenticating. Check the startup logs for confirmation: ``` [oidc] RP-Initiated Logout enabled (end_session_endpoint discovered) # or [oidc] IdP does not advertise end_session_endpoint - will use prompt=login on next auth after logout ``` To redirect users back to Radar after IdP logout, set `--auth-oidc-post-logout-redirect-url` (or `auth.oidc.postLogoutRedirectURL` in Helm). This URL **must be registered** with your identity provider as a valid post-logout redirect URI. **Back-Channel Logout (IdP-initiated session revocation):** When an admin disables a user at the IdP level (e.g., disables an Okta account), Radar has no way to know - the existing session cookie remains valid until it expires. Back-Channel Logout ([spec](https://openid.net/specs/openid-connect-backchannel-1_0.html)) solves this: the IdP POSTs a signed `logout_token` to Radar's `/auth/backchannel-logout` endpoint, and Radar immediately revokes the matching session. To enable, set `--auth-oidc-backchannel-logout` (or `auth.oidc.backchannelLogout: true` in Helm), then register `https://radar.example.com/auth/backchannel-logout` as the Back-Channel Logout URI in your IdP. If Radar is served under a base path, the route sits at `{basePath}/auth/backchannel-logout` - register that. An unprefixed URI is not routed to Radar, and the IdP's POST is silently lost: disabling a user leaves their Radar session live until the cookie expires, which is the exact failure this feature exists to prevent. ```yaml theme={null} auth: mode: oidc oidc: issuerURL: https://your-idp.example.com clientID: radar backchannelLogout: true ``` Check the startup logs to confirm IdP support: ``` [oidc] Backchannel Logout enabled (sid-based revocation) # or [oidc] WARNING: --auth-oidc-backchannel-logout is set, but IdP does not advertise backchannel_logout_supported. ``` **Limitations:** * **Single-replica only.** Revocations are stored in memory and not shared across pods. If Radar runs multiple replicas behind a load balancer, a revocation hitting one pod won't affect sessions on other pods. * **Lost on restart.** If Radar restarts, all pending revocations are lost. The session will expire naturally at cookie TTL (4h default). * **`sid` required for targeted revocation.** If the IdP's logout\_token contains only `sub` (no `sid`), Radar cannot target the specific session - it logs a warning and the session expires at cookie TTL. Most major IdPs (Okta, Keycloak, Auth0, Azure AD) include `sid`. * **Google does not support back-channel logout.** For Google, session exposure is bounded by cookie TTL only. **Using K8s Secrets for sensitive values:** For production, use K8s Secrets instead of storing credentials in Helm values (which are visible in Helm release history): ```yaml theme={null} auth: mode: oidc # Session signing key from a K8s Secret existingSecret: radar-auth-secret # K8s Secret containing the HMAC key existingSecretKey: auth-secret # Key within the Secret (default) oidc: issuerURL: https://accounts.google.com clientID: your-client-id # OIDC client secret from a K8s Secret existingSecret: radar-oidc-credentials # K8s Secret containing the client secret clientSecretKey: client-secret # Key within the Secret (default) redirectURL: https://radar.example.com/auth/callback ``` You can also mix approaches - use `existingSecret` for the client secret but pass `clientID` as a plain value (it's not sensitive). **TLS configuration for self-signed certificates:** If your OIDC provider uses a self-signed or internal CA certificate (e.g., on-prem Keycloak), use one of: ```yaml theme={null} auth: oidc: caCert: /etc/radar/oidc-ca.crt # Path to CA certificate file (secure) # insecureSkipVerify: true # Skip TLS verification (dev/test only) ``` When using `caCert` in Kubernetes, mount the CA certificate into the pod via a ConfigMap or Secret volume. ### Radar Cloud mode If you see `RADAR_CLOUD_MODE` or `cloud.*` values in the chart, they control a specialized deployment mode used by [Radar Cloud](https://radarhq.io) - a hosted SaaS that lets a single Cloud frontend manage many in-cluster Radar instances over an outbound tunnel. You don't need to use it to run Radar standalone; leave `cloud.enabled: false` (the default). Under cloud-mode (`RADAR_CLOUD_MODE=true`, set automatically by the chart when `cloud.enabled=true`), Radar: * Forces `--auth-mode=proxy` with pinned `X-Forwarded-User` / `X-Forwarded-Groups` headers. Radar accepts those headers only on requests marked in-process by its authenticated Cloud tunnel; the ordinary pod TCP listener cannot assert Cloud identity. * Ships three default ClusterRoleBindings mapping Cloud's `radar:owner` / `radar:member` / `radar: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 infrastructure the built-in `view`/`edit`/`admin` roles exclude - Nodes, PersistentVolumes, StorageClasses, IngressClasses, PriorityClasses, RuntimeClasses, Leases (namespaced; the spec is only a holder and renew times - `edit`/`admin` already include them, so only viewers and the view-based system and AI grants gain), ClusterTrustBundles, CRDs, and admission webhook configurations - plus list-only Upgrade impact evidence for CSIDrivers, CSIStorageCapacities, legacy PodSecurityPolicies, and API flow-control configuration. When the matching collection is enabled, it also grants APIServices, PrometheusRules, Karpenter kinds, and node metrics. It does not grant Secrets, RBAC objects, API-server metrics, or kubelet proxy access. 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`). * Creates the default read grant for `radar:system`, Radar Cloud's alerts worker and timeline puller: a chart-owned role aggregated to match `view`, the cluster-read and integration-read add-ons, and cluster-wide Secret read. Helm release alerts and Secret changes in the timeline need Secret read; Helm stores releases as Secrets. No writes. Controlled by `cloud.systemRbac` (default `true`). * Creates the default read grant for alert-triggered AI investigations, recorded in Kubernetes audit logs as `radar:ai:`: `radar:ai:reader` receives permissions based on `view` plus the cluster-read and integration-read add-ons. Stock `view` excludes Secrets, but includes pod logs and ConfigMaps; roles labelled `aggregate-to-view` widen this grant too. Controlled by `cloud.aiRbac` (default `true`). Requests a person starts or follows up on use that person's identity and groups, including follow-ups on alert-triggered investigations. MCP clients also use the user's permissions. * Treats `cloud.aiRbac` and `cloud.systemRbac` as controls for the chart's default grants, independent of `cloud.defaultRbac`. False or absent (including after a plain `--reuse-values` upgrade from an older chart) creates no default grant and leaves customer-created bindings untouched. These settings do not disable background jobs or automatic analysis; they control their default Kubernetes access. * Requires cluster-admin-equivalent authority or `escalate` on ClusterRoles to create or update the AI/system aggregated roles, in addition to normal chart installation permissions. Restricted installers can set `cloud.aiRbac=false` and `cloud.systemRbac=false` and supply their own read-only group bindings. * Uses the Cloud role (`radar:owner` / `radar:member` / `radar:viewer`) only to gate Radar's own configuration (settings, integrations, audit settings, registered OCI chart sources, self-upgrade), which runs as Radar's ServiceAccount. A Cloud request with no role group (such as the Hub's `radar:system` background identity) is refused there. Operations that run as the impersonated user - Helm reads and writes, in-cluster probe pods - are decided by Kubernetes RBAC alone. * Does **not** grant `PodCertificateRequest` access through the default Cloud tier bindings. Radar surfaces the API when Kubernetes 1.37 serves it, but a Cloud user can browse requests only when their Kubernetes RBAC independently grants `get/list` access. To opt in, bind a Role or ClusterRole granting `get/list/watch` on `certificates.k8s.io/podcertificaterequests` to the intended `radar:*` groups. This stays separate because certificate-request metadata is more sensitive than ordinary workload inventory. * Restricts the ordinary pod/ClusterIP TCP listener to `/api/health`; the full handler is served only over yamux streams from the outbound Cloud tunnel. Cloud mode also omits `/debug/pprof/*` and narrows auth exemptions to health only. **Optional Upgrade impact metrics evidence.** The default Cloud bindings deliberately omit the broader API server `/metrics` endpoint and kubelet `nodes/proxy` access. Upgrade impact reports checks that depend on those sources as **Incomplete**. To enable deprecated-API request evidence for a tier, bind a separate ClusterRole granting `get` on `nonResourceURLs: ["/metrics"]` to its canonical `radar:viewer`, `radar:member`, or `radar:owner` group. Keep this opt-in separate from the default cluster-read role so operators can make the broader disclosure decision explicitly. **Helm-specific bindings (when `rbac.helm=true`).** Helm's pre-flight existence check needs cluster-scoped reads/writes that the K8s built-in `admin`/`edit`/`view` ClusterRoles don't grant. The chart emits two add-on ClusterRoles, split by trust tier: * `radar-helm` - CRDs, StorageClasses, RuntimeClasses, PriorityClasses, PodDisruptionBudgets, Namespaces. Bound to `radar:owner` AND `radar:member`. * `radar-helm-admin` - RBAC objects (Roles/Bindings, Cluster variants), validating/mutating webhooks, ApiServices. Bound to `radar:owner` ONLY. Granting these to a tier weaker than owner would let a member self-promote to cluster-admin in one `ClusterRoleBinding` write, collapsing the owner/member distinction. A `radar:member` attempting to install a chart that bundles its own RBAC will get a typed `rbac_preflight` 403 with an actionable "ask an owner" message. Day-to-day app charts and operator-CRD installs still work for members. #### User-token mode (no impersonation) Set `auth.kubernetesCredentials: user-token` (requires `cloud.enabled: true`; flag `--auth-k8s-credentials=user-token`, env `RADAR_AUTH_K8S_CREDENTIALS`) to stop Radar from impersonating anyone. The chart then grants the ServiceAccount no `impersonate` permission. Radar treats each tunneled request as follows: * **With the user's token** (`X-Forwarded-Kubernetes-Token`): Radar asks the API server who the token belongs to (SelfSubjectReview) and makes every live call - writes, logs, exec, Helm - with that token alone, so the API server applies your own RBAC and audit logs show the real user. The forwarded username must match the token's username, or the request is refused. * **Without a token, as a person** (for example cross-cluster views): Radar serves its cache, filtered by that person's permissions, and refuses every live call with a 403 (`reason: user_token_required`). * **Without a token, as `radar:system:*` (group `radar:system`) or `radar:ai:` (group `radar:ai:reader`)**: Radar reads with its ServiceAccount, after a SubjectAccessReview of every request against that identity's bindings (`cloud.systemRbac`, `cloud.aiRbac`). The `radar:*` role groups gate only Radar's own settings in this mode; they are never part of the identity Radar checks Kubernetes permissions for. The chart requires `cloud.defaultRbac.create=false`, because the API server would otherwise apply the tier bindings to any token whose IdP groups happen to be named `radar:owner`, `radar:member` or `radar:viewer`. Some Radar features read with the ServiceAccount in every mode and keep doing so: Prometheus queries, permission lookups for other subjects, and the in-cluster reachability check. They never write. The chart drops the ServiceAccount's `pods/exec` and `pods/portforward` grants in this mode (shells and port-forwards run on each person's token), so integrations that fall back to a ServiceAccount port-forward (Hubble, OpenCost) only work over their in-cluster Service. Your API servers must accept the ID tokens your identity provider issues to your self-managed Radar Cloud control plane's sign-in client. The tunnel must be verified: `wss://` with `cloud.insecureSkipVerify=false` (the chart and the binary both refuse otherwise). Tokens whose groups start with `radar:`, `cloud:` or `system:` (other than `system:authenticated`) are refused, because the API server would apply the chart's `radar:system` and `radar:ai:reader` bindings, or `system:masters`, to them; give your API server a non-empty `--oidc-groups-prefix`. Background identities only read through Radar: any other verb is refused before it reaches the API server. Cached reads still rely on the identity Radar Cloud forwards, as in every mode: the cache is filled by the ServiceAccount (Secrets included under auth) and filtered per person, so a compromised control plane could read the cache as anyone it names, although it can't make live calls without that person's token. The ServiceAccount keeps its cluster-wide reads (including Secrets under auth), as in every mode. Customer-facing documentation for Radar Cloud lives on [radarhq.io](https://radarhq.io). The authoritative reference for the Cloud-mode chart values is the comment block in [`deploy/helm/radar/values.yaml`](https://github.com/skyhook-io/radar/blob/main/deploy/helm/radar/values.yaml) under `cloud:`. ## Setting Up User Permissions Radar delegates authorization entirely to K8s RBAC via impersonation. It doesn't have its own role system - permissions are managed with standard K8s tooling (kubectl, Terraform, Helm, GitOps, etc.). **If your cluster already has RBAC bindings** (most production clusters do), they work automatically. Radar impersonates the authenticated user, and K8s evaluates their existing bindings. You only need to create new bindings if users don't already have the access they need. ### How It Works with Cloud Providers Cloud-managed clusters typically map their IAM to K8s RBAC automatically: * **GKE**: Google Groups for RBAC maps Google Workspace groups to K8s groups. IAM roles (`roles/container.viewer`, etc.) grant K8s access. The username is the user's Google email. * **EKS**: The `aws-auth` ConfigMap maps IAM roles/users to K8s users and groups. The username is typically the IAM role ARN or a mapped alias. * **AKS**: Azure AD integration maps Azure AD groups directly to K8s groups. The username is the Azure AD user principal name. In all cases, the username and groups that Radar receives from your auth layer (proxy headers or OIDC token) must match what K8s RBAC expects. Usually this means using the same email/UPN your cloud provider uses. ### Understanding the Chain ``` Identity Provider (Google, Okta, etc.) → returns: username=alice@company.com, groups=[sre-team] Auth Layer (proxy headers or OIDC token) → Radar extracts: user="alice@company.com", groups=["sre-team"] Radar backend → creates impersonated K8s client: act as alice@company.com in group sre-team K8s API server → checks RBAC: does any RoleBinding/ClusterRoleBinding grant "sre-team" this permission? → allow or deny ``` K8s RBAC doesn't require users to have ServiceAccounts. It supports binding roles to `User` (a string like `alice@company.com`) and `Group` (a string like `sre-team`). These strings just need to match what the identity provider returns. ### Using Built-in K8s Roles K8s ships with built-in ClusterRoles that work well with Radar. You don't need to create custom roles unless you want fine-grained control: | Built-in Role | What Users Can Do in Radar | | - | - | | `view` | See resources, topology, events, logs. No restart, scale, or Helm writes. | | `edit` | Everything in `view` + restart, scale, edit resources. Does NOT include exec or port-forward (those require `pods/exec` and `pods/portforward` subresources - use the custom `radar-operator` role below). | | `admin` | Everything in `edit` + manage RBAC within namespaces. Same exec/port-forward caveat as `edit`. | | `cluster-admin` | Full access to everything across all namespaces, including exec and port-forward. | ```yaml theme={null} # Example: give the dev-team group read-only access to all namespaces apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: dev-team-view subjects: - kind: Group name: dev-team apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: view apiGroup: rbac.authorization.k8s.io ``` For more control, create a custom ClusterRole: ### Step 1: Create a ClusterRole with the permissions you want ```yaml theme={null} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: radar-operator rules: # View workloads (most users) - apiGroups: ["", "apps", "batch"] resources: ["pods", "deployments", "statefulsets", "daemonsets", "services", "configmaps", "jobs", "cronjobs", "replicasets", "events"] verbs: ["get", "list", "watch"] # Restart and scale workloads - apiGroups: ["apps"] resources: ["deployments", "statefulsets", "daemonsets"] verbs: ["patch", "update"] # View logs - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] # Exec into pods (optional - omit for read-only users) - apiGroups: [""] resources: ["pods/exec"] verbs: ["create"] ``` ### Step 2: Bind it to your users or groups **By group** (recommended - matches group from your identity provider): ```yaml theme={null} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: sre-team-radar-operator subjects: - kind: Group name: sre-team # Must match the group string from IdP apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: radar-operator apiGroup: rbac.authorization.k8s.io ``` **By user:** ```yaml theme={null} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: alice-radar-operator subjects: - kind: User name: alice@company.com # Must match the username from IdP apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: radar-operator apiGroup: rbac.authorization.k8s.io ``` **Per-namespace** (use RoleBinding instead of ClusterRoleBinding): ```yaml theme={null} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: dev-team-radar-operator namespace: staging # Only grants access in this namespace subjects: - kind: Group name: dev-team apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: radar-operator apiGroup: rbac.authorization.k8s.io ``` ### Step 3: Verify After deploying with auth enabled, you can verify impersonation works: ```bash theme={null} # Check what alice can do (from a machine with cluster-admin access) kubectl auth can-i --list --as=alice@company.com --as-group=sre-team # Check specific permission kubectl auth can-i get pods -n production --as=alice@company.com --as-group=sre-team ``` ## What Users See When auth is enabled: * A **username** appears in the Radar header with a logout option * The **namespace selector** only shows namespaces the user can access * **Topology, resources, events, dashboard** are filtered to accessible namespaces * **Cluster-scoped resources** (Nodes, PersistentVolumes, StorageClasses) are gated per-kind via `SubjectAccessReview` - a user sees them only if their own RBAC permits listing that kind, independent of namespace access * **Helm releases** are read with the user's own identity, like everything else. Helm stores releases as Secrets, so a user sees a release only where their RBAC grants `list secrets` in the storage namespace - and the K8s built-in `view` role deliberately does not. Radar resolves the namespaces the user can actually read release storage in and lists only there, returning 403 when there are none. Write operations (install, upgrade, rollback, uninstall) are impersonated and need the matching RBAC. * **Write buttons** (restart, scale, exec, Helm install, etc.) only appear if the user has permission * Write operations return **403** from K8s if RBAC denies them (shown as an error toast) * The **/api/auth/me** endpoint returns the current user info and whether auth is enabled ## ServiceAccount RBAC When auth is enabled, Radar's ServiceAccount needs two additional permissions (added automatically by the Helm chart): ```yaml theme={null} # Impersonate users and groups - apiGroups: [""] resources: ["users", "groups"] verbs: ["impersonate"] # Check permissions for specific users - apiGroups: ["authorization.k8s.io"] resources: ["subjectaccessreviews"] verbs: ["create"] ``` The ServiceAccount's existing read permissions (list pods, watch deployments, etc.) continue to power the shared cache. Impersonation is used for every call made on a user's behalf rather than served from the cache: writes, exec, logs, port-forward, and Helm release reads. **Treat the ServiceAccount as privileged.** Kubernetes RBAC can't limit `impersonate` to a subset of users or groups. Radar enforces the reserved-identity rule above where it accepts identities, but anyone holding the ServiceAccount's token can call the API server directly as any user or group, which is effectively cluster-admin. Run Radar in a dedicated namespace, restrict network access to it with NetworkPolicies, and make your auth proxy or ingress the only path to it. ## Session Cookies Radar uses stateless HMAC-SHA256 signed cookies for sessions. The cookie contains the username and groups - no server-side session storage. * **Cookie TTL**: 4 hours by default (sliding), configurable with `--auth-cookie-ttl` or `auth.cookieTTL` in Helm values. Sessions auto-extend while you're active; idle sessions expire after the configured TTL. Active users won't notice - Radar's frontend polling keeps the session alive automatically. * **Proxy mode**: When the cookie expires, the middleware transparently re-creates the session from proxy headers on the next request, so the shorter default TTL has no UX impact. * **Session secret**: Set `auth.secret` or `RADAR_AUTH_SECRET` env var. If empty, a random key is generated at startup (sessions won't survive pod restarts) * **For production**: Use `auth.existingSecret` to reference a K8s Secret, so sessions survive restarts ## Configuration Reference | Parameter | CLI Flag | Helm Value | Default | | - | - | - | - | | Auth mode | `--auth-mode` | `auth.mode` | `none` | | Session secret | `--auth-secret` | `auth.secret` | auto-generated | | Cookie TTL | `--auth-cookie-ttl` | `auth.cookieTTL` | `4h` (sliding) | | User header (proxy) | `--auth-user-header` | `auth.proxy.userHeader` | `X-Forwarded-User` | | Groups header (proxy) | `--auth-groups-header` | `auth.proxy.groupsHeader` | `X-Forwarded-Groups` | | Proxy logout URL (proxy) | `--auth-proxy-logout-url` | `auth.proxy.logoutURL` | - | | OIDC issuer | `--auth-oidc-issuer` | `auth.oidc.issuerURL` | - | | OIDC internal issuer | `--auth-oidc-internal-issuer` | `auth.oidc.internalIssuerURL` | - | | OIDC authorization URL | `--auth-oidc-authorization-url` | `auth.oidc.authorizationURL` | - | | OIDC token URL | `--auth-oidc-token-url` | `auth.oidc.tokenURL` | - | | OIDC userinfo URL | `--auth-oidc-userinfo-url` | `auth.oidc.userInfoURL` | - | | OIDC JWKS URL | `--auth-oidc-jwks-url` | `auth.oidc.jwksURL` | - | | OIDC client ID | `--auth-oidc-client-id` | `auth.oidc.clientID` | - | | OIDC client secret | `--auth-oidc-client-secret` | `auth.oidc.clientSecret` | - | | OIDC client secret (K8s Secret) | - | `auth.oidc.existingSecret` | - | | OIDC client secret key | - | `auth.oidc.clientSecretKey` | `client-secret` | | OIDC redirect URL | `--auth-oidc-redirect-url` | `auth.oidc.redirectURL` | - | | OIDC username claim | `--auth-oidc-username-claim` | `auth.oidc.usernameClaim` | `email`, falling back to `sub` | | OIDC groups claim | `--auth-oidc-groups-claim` | `auth.oidc.groupsClaim` | `groups` | | OIDC scopes | `--auth-oidc-scopes` | `auth.oidc.scopes` | `openid,profile,email,groups` | | OIDC post-logout redirect | `--auth-oidc-post-logout-redirect-url` | `auth.oidc.postLogoutRedirectURL` | - | | OIDC username prefix | `--auth-oidc-username-prefix` | `auth.oidc.usernamePrefix` | - | | OIDC groups prefix | `--auth-oidc-groups-prefix` | `auth.oidc.groupsPrefix` | - | | OIDC CA certificate | `--auth-oidc-ca-cert` | `auth.oidc.caCert` | - | | OIDC skip TLS verify | `--auth-oidc-insecure-skip-verify` | `auth.oidc.insecureSkipVerify` | `false` | | OIDC backchannel logout | `--auth-oidc-backchannel-logout` | `auth.oidc.backchannelLogout` | `false` | ## Troubleshooting ### User is logged back in immediately after logout (OIDC) This happens when the identity provider's SSO session is not terminated during logout. Check: 1. **Check the startup logs.** Look for `[oidc] RP-Initiated Logout enabled` or the `prompt=login` fallback message. Both approaches should prevent silent re-authentication. 2. **Is `post_logout_redirect_uri` registered?** If you set `--auth-oidc-post-logout-redirect-url`, it must be registered as a valid post-logout redirect URI with your identity provider. If not registered, the IdP may show an error instead of redirecting. 3. **Browser extensions or aggressive caching?** Some browser extensions may interfere with the redirect to the IdP's logout endpoint. ### Users get 401 on every request * **Proxy mode**: Check that the auth proxy is setting `X-Forwarded-User`. Inspect with: ```bash theme={null} kubectl logs -n radar -l app.kubernetes.io/name=radar | grep "auth" ``` * **OIDC mode**: Verify the issuer URL, client ID, and redirect URL are correct. Check Radar logs for OIDC errors. ### Users authenticate but see empty dashboard / no resources The user will see a "No Namespace Access" message if they have no K8s RBAC bindings. Radar uses `SubjectAccessReview` to discover which namespaces each user can access - with no bindings, the result is zero namespaces. **Verify the user's access:** ```bash theme={null} # Check if the user can list pods in a specific namespace kubectl auth can-i list pods -n \ --as= --as-group= # Check cluster-wide access kubectl auth can-i list pods --all-namespaces \ --as= --as-group= ``` **Fix:** Create a RoleBinding or ClusterRoleBinding for the user/group (see examples above). ### All users see empty data even with correct RBAC Radar's ServiceAccount may be missing the `subjectaccessreviews` permission needed to check user access. When this fails, Radar denies all namespace access (fail-closed). Verify: ```bash theme={null} kubectl auth can-i create subjectaccessreviews \ --as=system:serviceaccount:radar:radar ``` If this returns `no`, the Helm chart's RBAC was not applied. Re-install with `rbac.create: true` (the default) or add the rule manually. ### Write operations return 403 The user's K8s RBAC doesn't include the required verb. Check with: ```bash theme={null} kubectl auth can-i patch deployments -n \ --as= --as-group= ``` ### Impersonation errors in Radar logs Radar's ServiceAccount is missing impersonation permissions. Verify: ```bash theme={null} kubectl auth can-i impersonate users \ --as=system:serviceaccount:radar:radar ``` If using `rbac.create: false` in Helm, make sure your custom ClusterRole includes the impersonation rules. # CLI reference Source: https://radarhq.io/docs/configuration/cli Commands and flags for running Radar, starting AI investigations, connecting a cluster to Radar Cloud, and tuning cluster access. `radar` (or `kubectl radar` when installed via Krew) starts the local server by default. Defaults come from `~/.radar/config.json` when present - see [Configuration files](/docs/configuration/files). ## Commands | Command | Purpose | | - | - | | `radar [flags]` | Start the Radar server and open the UI. | | `radar diagnose / [-n namespace]` | Run an AI investigation using a local Claude Code, Codex, Cursor, or OpenCode CLI. Uses a running Radar when available, otherwise starts a temporary one. See [AI investigations](/docs/features/diagnose). | | `radar cloud install` | Install or adopt Radar in the selected kubeconfig context and connect it to Radar Cloud. | | `radar cloud status` | Inspect the installation, agent readiness, ownership, and Cloud tunnel health without changing the cluster. | ### `radar diagnose` | Flag | Description | | - | - | | `-n`, `--namespace` | Namespace of the target resource. | | `--agent` | Agent backend: `claude`, `codex`, `cursor-agent`, or `opencode`. Empty uses Radar's configured choice. | | `--profile` | `safeguarded` or `full-local`, where the selected agent supports both. | | `--server` | Radar server URL. Empty discovers the running instance from `~/.radar/mcp-port`. | | `--standalone` | Start a temporary in-process Radar instead of using a running instance. | | `--kubeconfig` | Kubeconfig used with `--standalone`. | | `--json` | Print the final verdict as JSON; progress stays on stderr. | | `--open` | Also open the investigation in the Radar UI. | | `--yes` | Acknowledge the first-run consent disclosure non-interactively. | ### `radar cloud` `cloud install` supports `--context`, `--namespace`, `--release`, `--chart-version`, `--name`, `--hub-url`, `--dry-run`, `--no-browser`, and `--browser`. When adopting an existing installation, non-interactive runs require `--adopt-existing`; interactive runs can ask for consent. For an adopted installation, `--enable-cloud-features` adds optional Cloud feature RBAC. On any install, `--no-self-upgrade` omits the self-upgrade Role and RoleBinding. `-y` / `--yes` skips current-context confirmation but never grants adoption consent. `cloud status` accepts `--context`. Pass `--namespace` and `--release` together to inspect one exact installation instead of discovering visible installations. ## General | Flag | Default | Description | | - | - | - | | `--port` | `9280` | HTTP port the UI / API serve on. | | `--listen-address` | `127.0.0.1` | Bind to loopback by default. Set `0.0.0.0` only for an intentionally shared listener. | | `--base-path` | `""` | Serve under a URL prefix such as `/radar` when an ingress forwards the prefix without stripping it. Not supported in Radar Cloud mode. | | `--no-browser` | `false` | Don't auto-open the browser. | | `--browser` | `""` | Browser to open the UI in, overriding the OS default launcher. Local runs only (in-cluster and Cloud can't open a user browser). On macOS, app names and `.app` bundles work, e.g. `--browser Safari` or `--browser "Google Chrome"`. Empty uses the OS default. | | `--namespace` | `""` | Initial namespace filter (empty = all namespaces). | | `--namespaces` | `""` | Initial namespace filters as a comma-separated list. Useful when the identity can list resources in selected namespaces but cannot list namespaces cluster-wide. | | `--version` | - | Print version and exit. | | `--debug-events` | `false` | Verbose event-drop logging. | ## Cluster connection | Flag | Description | | - | - | | `--kubeconfig` | Path to a kubeconfig file. Falls back to `KUBECONFIG` env or `~/.kube/config`. | | `--kubeconfig-dir` | Comma-separated directories whose kubeconfigs are merged. Mutually exclusive with `--kubeconfig`. | ## Timeline storage | Flag | Default | Description | | - | - | - | | `--timeline-storage` | `memory` | `memory` or `sqlite`. SQLite persists across restarts. | | `--timeline-db` | `~/.radar/timeline.db` | Path to the SQLite database when `--timeline-storage=sqlite`. | | `--timeline-retention` | `168h` | Age limit for SQLite events. `0` disables age-based cleanup. | | `--history-limit` | `10000` | Max events retained in the timeline (memory mode). | | `--timeline-max-size` | `1Gi` | Max SQLite timeline size (DB + WAL) before the oldest events are pruned, e.g. `800Mi`, `8Gi`. `0` disables size-based pruning. | ## Metrics & traffic | Flag | Description | | - | - | | `--prometheus-url` | Manual Prometheus / VictoriaMetrics URL. Skips auto-discovery (handy for out-of-cluster Prometheus). | | `--prometheus-header` | HTTP header to send with Prometheus requests, e.g. `'Authorization=Bearer '` (repeatable). Required for auth-protected backends. | | `--prometheus-header-from-env` | Like `--prometheus-header`, but the value is read from an env var, e.g. `'Authorization=PROMETHEUS_TOKEN'` (repeatable) - keeps secrets out of `ps`. | | `--beyla-job-selector` | PromQL job-label matcher used to scope Grafana Beyla traffic series. Empty matches the built-in Beyla / Alloy job-name defaults. | ## Feature toggles | Flag | Effect | | - | - | | `--no-mcp` | Disable the MCP server (AI tools won't be able to connect). | | `--mcp-catalog-only` | Start only the MCP endpoint for registry / inspector catalog introspection (e.g. Glama); skips Kubernetes initialization. | | `--mcp-catalog-stdio` | Start only the MCP catalog over stdio for registry / inspector introspection; skips Kubernetes initialization. | | `--disable-helm-write` | Read-only Helm: hide install / upgrade / rollback / uninstall buttons. | | `--disable-exec` | No pod terminal / debug shell. | | `--disable-local-terminal` | Disable the local-machine terminal pane. | | `--pod-shell-default` | Override the default pod exec shell (runs as `sh -c `; empty falls back to `bash -il → ash → sh`). | | `--debug-image` | Image for ephemeral debug containers and node debug pods. Defaults to `busybox:latest`; point at a mirror for air-gapped / private-registry clusters. | | `--reachability-image` | Image for in-cluster reachability probe Jobs. Empty uses Radar's running pod image when discoverable, then `RADAR_IMAGE`, then the version-matched published Radar image. | | `--list-page-size` | Paginate the initial LIST of high-cardinality kinds (Pods, ReplicaSets) at this size. Defaults to `0` (single LIST). Helps very large clusters whose initial sync fails; only used when WatchList streaming is unavailable on the apiserver. Try `2000`. | | `--namespace-scope` | Pin namespaced informer caches to a **single** namespace instead of watching the whole cluster - for very large clusters where cluster-wide caches are too heavy. Cluster-scoped resources (Nodes, Namespaces, PVs, and cluster-scoped CRDs) still load cluster-wide. Requires `--namespace`, a namespace on the kubeconfig context, or a saved local pick. Scoping to multiple namespaces is not supported yet. Local sessions can switch the scoped namespace from the picker (rebuilds the cache in place); auth / shared deployments lock it to the startup namespace so one user can't reshape the shared cache for everyone. | | `--ai-history` | Persist AI investigation transcripts and verdicts to `~/.radar/ai-runs.db`. Defaults to `true`. | ## Tuning for slow or large clusters Clusters reached over high-latency tunnels, or with thousands of namespaces, can trip Radar's default deadlines. Each flag also reads from the matching `RADAR_*` env var. | Flag | Default | Env | Description | | - | - | - | - | | `--context-switch-timeout` | `30s` | `RADAR_CONTEXT_SWITCH_TIMEOUT` | Max time a kubeconfig context switch may take. Widen to `120s`+ for clusters over high-latency tunnels. | | `--first-paint-backstop` | `5m` | `RADAR_FIRST_PAINT_BACKSTOP` | Hard upper bound on the initial critical-cache sync wait before Radar falls through to a partial-data render. | | `--namespace-list-timeout` | `5s` | `RADAR_NAMESPACE_LIST_TIMEOUT` | Timeout for the cluster-wide namespace LIST that decides if you're RBAC-namespace-restricted. Widen to `30s`+ on slow control planes - a timeout here is otherwise misreported as "Limited list - RBAC". | | `--max-scope-candidates` | `20` | `RADAR_MAX_SCOPE_CANDIDATES` | Cap on the namespace-fallback probe fanout for users who can list namespaces cluster-wide but not a specific kind. Raise above 20 for clusters with more namespaces to avoid silently marking kinds denied. | ## Authentication See [Authentication](/docs/configuration/authentication) for the full setup. Common flags: | Flag | Description | | - | - | | `--auth-mode` | `none` (default), `proxy`, or `oidc`. | | `--auth-secret` | HMAC secret for session cookies. Auto-generated when empty - set this for multi-replica deployments. | | `--auth-cookie-ttl` | Sliding session TTL. Default `4h`. | **Proxy mode (oauth2-proxy / Pomerium / etc.):** | Flag | Default | Description | | - | - | - | | `--auth-user-header` | `X-Forwarded-User` | Header carrying the username. | | `--auth-groups-header` | `X-Forwarded-Groups` | Header carrying the comma-separated groups. | | `--auth-proxy-logout-url` | `""` | Upstream proxy logout URL. Empty clears only Radar's session cookie. | **OIDC mode (Google / Okta / Keycloak / etc.):** | Flag | Description | | - | - | | `--auth-oidc-issuer` | OIDC issuer URL. | | `--auth-oidc-internal-issuer` | Internal issuer base used for discovery and server-side endpoints while tokens are still validated against `--auth-oidc-issuer`. | | `--auth-oidc-authorization-url` | Browser-facing authorization endpoint override. | | `--auth-oidc-token-url` | Server-side token endpoint override. | | `--auth-oidc-userinfo-url` | Server-side userinfo endpoint override. | | `--auth-oidc-jwks-url` | Server-side JWKS endpoint override. | | `--auth-oidc-client-id` | Client ID from your IdP. | | `--auth-oidc-client-secret` | Client secret. | | `--auth-oidc-redirect-url` | The `/auth/callback` URL registered with the IdP. | | `--auth-oidc-username-claim` | ID token claim used as the Kubernetes username - must match kube-apiserver's `--oidc-username-claim`. Default: `email`, falling back to `sub`. When set, a token without the claim is refused. | | `--auth-oidc-groups-claim` | JWT claim that carries groups. Default `groups`. | | `--auth-oidc-scopes` | Comma-separated scopes. Defaults to `openid,profile,email,groups`. | | `--auth-oidc-post-logout-redirect-url` | URL to redirect to after IdP logout (must be registered with the IdP). | | `--auth-oidc-username-prefix` | Username prefix for K8s impersonation - must match kube-apiserver's `--oidc-username-prefix`. | | `--auth-oidc-groups-prefix` | Groups prefix for K8s impersonation - must match kube-apiserver's `--oidc-groups-prefix`. | | `--auth-oidc-insecure-skip-verify` | Skip TLS verification on the IdP (dev / test only). | | `--auth-oidc-ca-cert` | Path to a custom CA bundle for the IdP. | | `--auth-oidc-backchannel-logout` | Enable the back-channel logout endpoint. Single-replica only. | | `--auth-oidc-enable-pkce` | Enable PKCE (S256) for the authorization-code flow. Opt-in. | ## Radar Cloud These flags wire an in-cluster Radar to a hosted Radar Cloud. Each is also readable from the matching `RADAR_CLOUD_*` env var (so secrets stay out of `ps`). | Flag | Env | Description | | - | - | - | | `--cloud-url` | `RADAR_CLOUD_URL` | Cloud WS endpoint (e.g. `wss://api.radarhq.io/agent`). Empty = local-only. | | `--cloud-token` | `RADAR_CLOUD_TOKEN` | Cluster token from the Cloud install wizard (`rhc_`). | | `--cluster-name` | `RADAR_CLOUD_CLUSTER_NAME` | Human-readable name (required with `--cloud-url`). | | `--cloud-insecure-skip-verify` | `RADAR_CLOUD_INSECURE_SKIP_VERIFY` | Skip Hub tunnel TLS verification for a Radar Cloud Self-Managed trial using a self-signed certificate. Insecure: traffic is encrypted but the Hub is not authenticated. | Setting `RADAR_CLOUD_MODE=true` additionally forces `--auth-mode=proxy` and pins `X-Forwarded-User` / `X-Forwarded-Groups` so the in-cluster Radar impersonates the Cloud-authenticated user against the K8s API. Don't set this for local installs. ## Internal / development | Flag | Description | | - | - | | `--dev` | Serve the frontend from disk instead of the embedded bundle. | | `--fake-in-cluster` | Pretend Radar is running in-cluster (useful for testing the in-cluster UX off-cluster). | # Configuration files Source: https://radarhq.io/docs/configuration/files Where Radar stores defaults and preferences, kubeconfig precedence, and how to switch between clusters and contexts. This document covers Radar's cluster connection behavior. For commands and flags, see the [CLI reference](/docs/configuration/cli). ## MCP exposure MCP is enabled with read and write tools by default. `--no-mcp` disables all MCP endpoints. `--mcp-read-only` keeps `/mcp-readonly` and the internal `/mcp-investigation` endpoint, while `/mcp` returns 404. Both CLI and Desktop support these flags. This controls MCP exposure only; UI and REST writes are unchanged. Use `RADAR_MCP_READ_ONLY=1` or `"mcpReadOnly": true` in `~/.radar/config.json` for persistent deployments. CLI overrides environment, which overrides file settings. Explicitly selecting disabled or read-only mode overrides a competing mode from a lower-priority source. Explicit false clears only its own setting. Contradictory flags, or contradictory settings within the config file, are errors. See [MCP configuration](/docs/features/mcp#enabling--disabling) for examples, boolean parsing and alternate entrypoints. ## HTTP Listener Radar listens on `127.0.0.1:9280` by default, so an unauthenticated local instance is reachable only from the same network namespace. `localhost` is accepted as an equivalent spelling. Requests to this loopback-only, unauthenticated listener must also use a loopback `Host`; Radar rejects other hostnames so DNS rebinding cannot turn an untrusted site into a local client. The reserved `*.localhost` family is accepted; arbitrary local DNS and `/etc/hosts` aliases are not. To put a non-loopback hostname or reverse proxy in front of Radar, enable Radar authentication; do not switch to `0.0.0.0` merely to bypass this check. To reach Radar through a VM, WSL, dev container, jump host, or another machine, opt into a shared listener explicitly: ```bash theme={null} radar --listen-address=0.0.0.0 ``` To bind only a specific local IP, use e.g. `--listen-address=192.168.1.5` or `--listen-address=::1`. IPv6 addresses are passed without brackets; URLs use brackets, e.g. `http://[::1]:9280`. The address must be assigned on the machine. Hostnames other than `localhost`, interface names, CIDRs, and IPv6 zone IDs are not accepted. The existing `0.0.0.0` wildcard retains dual-stack behavior where supported by the OS; `::` selects an IPv6 wildcard. Browser launch and built-in AI investigations use the selected address. `radar diagnose` discovers the address through `~/.radar/mcp-port`. Use `--server http://
:` to select an instance explicitly (with brackets around IPv6 addresses). External MCP clients should use that address in their configured URL too. This flag affects Radar's HTTP server; port-forward address options remain `127.0.0.1`/`localhost` and `0.0.0.0`. A non-loopback listener can be reached by non-browser clients; CORS is not an authentication boundary. Enable Radar authentication and restrict network access whenever binding a non-loopback address. The loopback `Host` protection above does not apply to a shared listener, and Origin checks alone do not stop DNS rebinding there. Treat an unauthenticated shared listener as accessible to any browser that can reach the network and do not expose it outside a fully trusted network. The host local terminal is unavailable on a shared listener even if a client sends a loopback `Host` value. The Docker image and Helm chart set `0.0.0.0` explicitly because their HTTP listener must be reachable through a published container port or Kubernetes Service. Desktop Radar and temporary `radar diagnose` servers remain loopback-only. ## Desktop Window Behavior On macOS, closing the Radar window hides the app rather than quitting it. The Dock icon stays; a Dock click, Cmd+Tab, or Radar → Show All brings the window back with the session intact. File → Close Window (Cmd+W) hides it the same way the close button does. To quit, use Radar → Quit Radar (Cmd+Q). Radar keeps running while hidden. That is the point - MCP clients stay connected across a window close - but it means a hidden Radar still: * serves its loopback HTTP and MCP endpoints, under your kubeconfig identity; * holds watches open against the API server for every cached resource kind; * holds the memory backing those caches. None of that stops until you quit. If you close the window expecting Radar to release its cluster access, quit explicitly. On Linux and Windows, closing the window always quits. Neither gives Radar anything to reopen from - no Dock, and Wails v2 ships no tray icon - so hiding there would leave a running process with no way to reach it. ## AI Investigations Agent CLI Built-in AI investigations run an agent CLI installed on the machine Radar runs on: Claude Code (`claude`), Codex (`codex`), Cursor (`cursor-agent`), or OpenCode (`opencode`). Radar looks for them on the `PATH` it was started with, then in these install directories: | OS | Directories | | - | - | | macOS, Linux | `~/.local/bin`, `~/.claude/local`, `~/.opencode/bin`, `~/.volta/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/home/linuxbrew/.linuxbrew/bin`, `/usr/bin` | | Windows | `%LOCALAPPDATA%\Microsoft\WinGet\Links`, `%LOCALAPPDATA%\cursor-agent`, `%LOCALAPPDATA%\Programs\OpenAI\Codex\bin`, `%USERPROFILE%\.local\bin` | Radar picks up a newly installed CLI automatically, or when you click **Check again** in AI investigations. The startup output has an `AI investigations` line that says which agent Radar will use, or why investigations are off. If the CLI is somewhere else, for example an npm global install under nvm, start Radar from a terminal where the CLI works, or set `RADAR_AI_CLI_BIN` to its full path: ```bash theme={null} RADAR_AI_CLI_BIN="$(command -v claude)" radar ``` ```powershell theme={null} $env:RADAR_AI_CLI_BIN = "C:\path\to\claude.exe"; radar ``` With the variable set, Radar uses only that CLI and stops looking for others. It picks the driver from the file name: a name containing `cursor` runs as Cursor, `codex` as Codex, `opencode` as OpenCode, and anything else as Claude Code. If the path is not an executable Radar can run, investigations stay off until a working CLI is at that path, or the variable is changed and Radar restarted. The startup line names the path. ## Persistent Configuration Local CLI and Desktop Radar store machine defaults (`config.json`), personal preferences (`settings.json`), and per-context integration connections (`clusters.json`) under `~/.radar/`. Shared OSS installations (in-cluster or authentication-enabled) expose these installation settings read-only; configure them through Helm values or startup configuration instead. See [installation settings](/docs/configuration/in-cluster#installation-settings) for provisioning, upgrades, personal preferences, and the separate Cloud behavior. The local files below are not a substitute for Helm configuration. Non-Helm shared OSS still reads `config.json` as startup defaults (including previously saved integration endpoints); flags override them. It does not adopt UI-saved audit policy or OCI sources from `settings.json`: move those into `RADAR_OPERATOR_SETTINGS_FILE` before upgrading. Radar logs a warning when it ignores those saved settings. Radar does not rewrite `config.json` or `settings.json` during this transition. ### Config File (`~/.radar/config.json`) Persistent defaults for CLI flags. CLI flags override these values. Managed via the Settings dialog in the UI or `PUT /api/config`. For local CLI/Desktop, `prometheus*`, `argoCd*`, `costSource`, and `kubecost*` fields below are **legacy import sources**, not active integration defaults. Configure these in [local integration connections](#local-integration-connections) instead. `opencostCurrency` remains a machine preference. Shared OSS and Cloud retain installation-scoped configuration; the integration-field descriptions below describe that installation-scoped behavior. ```json theme={null} { "kubeconfig": "", "kubeconfigDirs": [], "namespace": "", "namespaces": [], "port": 9280, "noBrowser": false, "browser": "", "timelineStorage": "memory", "timelineDbPath": "~/.radar/timeline.db", "timelineMaxSize": "0", "historyLimit": 10000, "prometheusUrl": "", "opencostCurrency": "", "costSource": "auto", "kubecostUrl": "", "kubecostClusterId": "", "kubecostApiKey": "", "prometheusHeaders": {}, "mcp": true, "debugImage": "" } ``` All fields are optional - omitted fields use built-in defaults. | Field | Description | | - | - | | `kubeconfig` | Primary kubeconfig file (same as `--kubeconfig`) | | `kubeconfigDirs` | Directories containing additional kubeconfig files (same as `--kubeconfig-dir`) | | `restoreLastDesktopContext` | Desktop app only: reopen on the cluster last used (default: enabled). `false` always opens on the kubeconfig's `current-context` - see [Startup Context](#startup-context) | | `namespace` | Initial namespace filter | | `namespaces` | Initial namespace filters as a list (same as `--namespaces ns1,ns2,ns3`) | | `port` | Server port (default 9280) | | `noBrowser` | Don't auto-open browser | | `browser` | Browser for automatic launch (same as `--browser`; on macOS, app names like `Google Chrome` are supported) | | `timelineStorage` | `memory`, `sqlite`, or `postgres` | | `timelineDbPath` | Path to SQLite database (sqlite only) | | `timelineMaxSize` | Max SQLite DB + WAL size before pruning oldest events (`0` disables; sqlite only) | | `historyLimit` | Max timeline events to retain (memory only) | | `prometheusUrl` | Manual PromQL-compatible query URL - works with Prometheus, VictoriaMetrics, Thanos, Mimir, and similar backends. Skips auto-discovery; useful when the backend is not in the same cluster or uses a non-standard service name. Leave it empty and Radar looks for a Prometheus-like Service on startup and after each context switch. While it looks, the Metrics status says "discovering". If it finds a backend it cannot reach, and it can see a NetworkPolicy that blocks the connection, the status names that policy. | | `opencostCurrency` | Optional ISO 4217 override for values produced by OpenCost or Kubecost. Empty reads `currencyCode` from the pricing ConfigMap referenced by an active OpenCost/Kubecost workload, or literal `DISPLAY_CURRENCY` from an active Kubecost Deployment or StatefulSet, when the selected cost source is tied to the connected cluster; otherwise it falls back to `USD`. In local Settings → Cost, this preference saves automatically for all local clusters, independently of source testing, so it can be changed while a source is unavailable. Radar labels values but does not convert them. Equivalent CLI: `--opencost-currency`; an explicit CLI value remains authoritative while Radar runs and after restart. | | `costSource` | `auto` (default), `prometheus`, or `kubecost`, shown in Settings as **Automatic**, **OpenCost metrics** and **Kubecost**. Automatic prefers usable OpenCost metrics, then tries Kubecost 3. Local Settings saves the choice per context. See [fallback and retry behavior](https://github.com/skyhook-io/radar/blob/main/docs/integrations.md#opencost-and-kubecost) and the connection checks below. | | `kubecostUrl` | Optional Kubecost 3 Aggregator base URL. Empty discovers an active local Aggregator Service and tries its named `tcp-api` port (9004). When that port requires SAML/OIDC and no API key is configured, Radar can fall back to the same Service's exact `tcp-api-rbac` port (9008). Federated agent-only clusters need the central URL; root API URLs and URLs ending in `/model` are accepted. | | `kubecostClusterId` | Cluster ID used to filter a central Aggregator. Empty detects one distinct literal `CLUSTER_ID` from an active FinOps Agent or Aggregator; indirect or conflicting values require an override. Local Settings stores the override with this context's Cost settings; copying another cluster's backend never copies its cluster ID. | | `kubecostApiKey` | Optional Kubecost service-account key sent as `X-API-KEY`. Values are redacted from settings responses. An explicit key is never bypassed through an auto-discovered unauthenticated port: authentication failure remains visible. Local CLI/Desktop stores new keys in `clusters.json`, with explicit reuse and origin-change protection described below; this `config.json` field is only an import source locally. In Helm, provision a Kubernetes Secret with `cost.kubecost.existingSecret`; Settings is read-only. | | `prometheusHeaders` | HTTP headers sent with every Prometheus request. Required for auth-protected backends - e.g. `{"X-Scope-OrgID": "my-org"}`. Equivalent CLI: `--prometheus-header Key=Value` (repeatable). Stored in plain text in `config.json` - protect the file accordingly. **Requires `prometheusUrl`**: headers carry credentials, and auto-discovery probes every Service that looks like Prometheus, so with headers configured and no URL Radar refuses to discover (the Metrics status names the rule) rather than send them to endpoints you never named. Settings rejects saving that combination. | | `argoCdUrl` | Manual argocd-server URL for the Argo CD API integration - skips auto-discovery. | | `argoCdToken` | Argo CD API token (get-only account recommended). Stored in plain text - the file is written `0600`; the token is redacted from `GET /api/config`. | | `argoCdInsecureTls` | Skip TLS verification for argocd-server (self-signed default installs). Scoped to the Argo CD client only. | | `prometheusHeadersFromEnv` | Header values read from environment variables at startup - e.g. `{"Authorization": "PROMETHEUS_TOKEN"}`. Equivalent CLI: `--prometheus-header-from-env Key=ENV_VAR` (repeatable). Use this with Kubernetes Secret-backed env vars in Helm deployments. Same rule as `prometheusHeaders`: requires `prometheusUrl`. | | `mcp` | Enable/disable MCP server for AI tools (default: enabled) | | `debugImage` | Image for ephemeral debug containers and node debug pods (same as `--debug-image`). Empty = `busybox:latest`; point at a mirror for air-gapped / private-registry clusters. | The PostgreSQL DSN is **runtime-only** - set it through the `RADAR_TIMELINE_POSTGRES_DSN` environment variable. It is intentionally never written to `config.json` so credentials do not persist to disk in the settings file. For declarative deployments, `RADAR_COST_SOURCE`, `RADAR_KUBECOST_URL`, `RADAR_KUBECOST_CLUSTER_ID`, and `RADAR_KUBECOST_API_KEY` override these cost source fields. When any is set, the source controls are read-only in Settings; edit the deployment and restart Radar. `RADAR_KUBECOST_URL` does not carry an API key over from the config file; set `RADAR_KUBECOST_API_KEY` explicitly when the environment-managed endpoint requires one. The currency override remains separate. For workload charts, see [Workload metrics](/docs/features/workload-metrics) for the per-chart data requirements, automatic endpoint discovery and attribution, optional scope overrides, and coverage limits. Finding a Prometheus endpoint does not imply that it contains application HTTP metrics. ### Settings File (`~/.radar/settings.json`) User preferences for the UI. Managed via the Settings dialog or `PUT /api/settings`. ```json theme={null} { "theme": "system", "pinnedKinds": [ { "name": "Deployments", "kind": "Deployment", "group": "" } ] } ``` | Field | Values | Description | | - | - | - | | `theme` | `light`, `dark`, `system` | UI theme preference | | `pinnedKinds` | Array of `{name, kind, group}` | Resource kinds pinned to the sidebar | | `lastDesktopContext` | `{name, sourceFile, inFileName}` | Written by the Desktop app for itself: the cluster its window last used, reopened on the next launch. Stripped from `/api/settings`, and never read by `kubectl radar` or the `radar` CLI - see [Startup Context](#startup-context) | ## Cluster Connection Precedence Radar resolves configured sources first, then falls back to the same environment, in-cluster, and default-file sources as `kubectl`: | Priority | Source | Description | | - | - | - | | 1 | Configured kubeconfig file and directories | The primary file loads first, followed by valid files found in configured directories | | 2 | `KUBECONFIG` env var | Used only when neither a configured primary file nor directories are present | | 3 | In-cluster config | Tried when no configured source or `KUBECONFIG` exists | | 4 | `~/.kube/config` | Used when the in-cluster attempt is unavailable | The Settings values and their matching flags form one source pair. With no explicit flags, Radar uses both saved values. Passing only `--kubeconfig` replaces saved directories; passing only `--kubeconfig-dir` replaces the saved primary file. Passing both flags explicitly combines both sources. For compatibility with existing directory-mode installations, directories configured without a primary file suppress ambient `KUBECONFIG`. Radar reports that suppression in startup logs and diagnostics. ## KUBECONFIG vs In-Cluster Detection When Radar runs inside a Kubernetes pod, Kubernetes automatically sets the `KUBERNETES_SERVICE_HOST` environment variable. This normally triggers in-cluster configuration using the pod's service account credentials. However, **explicit kubeconfig takes precedence**. If you set `KUBECONFIG` or pass `--kubeconfig`, Radar uses that instead of in-cluster config. Configured directories also prevent in-cluster detection. This allows you to: * Run Radar inside a pod but connect to a different cluster * Use specific credentials instead of the pod's service account * Test with a custom kubeconfig while developing inside a cluster **Example: Override in-cluster config** ```bash theme={null} # Inside a pod, connect to a different cluster export KUBECONFIG=/path/to/other-cluster.yaml kubectl radar ``` This behavior matches `kubectl` and follows the [Kubernetes client-go precedence rules](https://github.com/kubernetes/kubernetes/issues/43662). ## Multiple Kubeconfig Files `KUBECONFIG` can contain multiple file paths (colon-separated on Linux/macOS, semicolon-separated on Windows): ```bash theme={null} export KUBECONFIG=~/.kube/config:~/.kube/staging-config:~/.kube/prod-config kubectl radar ``` Alternatively, use `--kubeconfig-dir` to load valid kubeconfig files from one or more directories. Discovery is non-recursive: ```bash theme={null} kubectl radar --kubeconfig-dir ~/.kube/configs/ ``` The primary file and directories can be combined: ```bash theme={null} kubectl radar --kubeconfig ~/.kube/config --kubeconfig-dir ~/.kube/configs/ ``` Radar keeps every file isolated rather than merging their cluster and user maps. This prevents identical user or cluster names in different files from selecting the wrong credentials. Context names remain unchanged unless two files use the same name; later collisions receive a source suffix in the context switcher. Saved namespace selections are keyed by that visible context name. Integration settings are keyed by kubeconfig source file and in-file context name instead (see [cluster identity and recovery](#cluster-identity-and-recovery)). If adding an earlier source causes a collision suffix to appear, the renamed context does not inherit preferences stored under its former name; Radar reports the rename in startup logs and diagnostics so it can be reconfigured. Files are ordered with primary paths first, followed by directory order and then filename order. A primary file's `current-context` wins when it declares one; otherwise Radar uses the first source in order that declares a current context. Leading `~/` paths are expanded, and references to the same underlying file are loaded once even when they use different absolute, relative, or symlink paths. Directory membership is scanned at startup. Changes inside a file that was already discovered, including added or removed contexts, are reflected while Radar is running; deleting that file also removes its contexts. A brand-new file added to a configured directory is discovered after Radar restarts. Directory entries must resolve to regular files: symlinks to regular kubeconfigs are accepted, while directories, sockets, pipes, and device files are ignored. An unusable additional directory does not prevent a valid primary file from loading. A configured primary source group that contains no usable contexts fails initialization rather than silently connecting to a directory cluster. Desktop Radar keeps its window open so the source can be repaired in Settings. ## Context Switching Radar supports switching between Kubernetes contexts at runtime through the UI. Click the context selector in the header to switch between available contexts. In shared OSS installations (in-cluster or authentication-enabled), context switching is disabled. The operator selects the connection at startup. This includes Helm installations and Pods using an explicit kubeconfig on a non-loopback listener; a personal loopback CLI in a development Pod remains local. Switching contexts in the UI never rewrites your kubeconfig - `kubectl` keeps pointing wherever it pointed before. ### Expired credentials If an active context's credentials expire or are rejected, Radar disconnects cluster-backed work and retries automatically. After you re-authenticate, exec-based credentials are re-probed and static credentials are reloaded from kubeconfig on disk, so Radar can reconnect without a restart. Retries start after 30 seconds and back off to 5 minutes; a credential plugin that stops responding is retried less frequently. ### Integration settings when switching clusters | Setting | Local CLI / Desktop scope | | - | - | | Metrics URL, authentication and tenant headers | Stored directly for this kubeconfig context | | Argo CD URL, token and TLS verification | Stored directly for this context, including discovery credentials | | Cost source (Automatic / OpenCost metrics / Kubecost) | Context-specific | | Kubecost URL and API key | Stored directly for this context, including discovery credentials | | Kubecost cluster ID | Context-specific, **never** inherited from another cluster's settings | | Prometheus-based costs | Use this context's metrics connection | | Workload-metrics scope evidence / assertion | Rechecked; process-local assertions are not stored in a connection | | Namespace selection | Remembered per visible kubeconfig context in `settings.json` | | Currency, UI preferences, OCI sources | Existing machine/personal scope; not per-cluster connections | ### Local integration connections In **Settings → Metrics / Argo CD / Cost**, configure the selected context and choose **Save changes**. **Discard** resets the form without changing saved settings. Switching A → B → A restores A's settings. A context with no saved settings uses discovery. Local CLI and Desktop share `~/.radar/clusters.json`. Local Prometheus discovery can fall back to port-forwarding. If access is denied, the error names the missing permission when available, such as `list pods` or `create pods/portforward`. Grant it in the Prometheus Service’s namespace, or configure a reachable URL. In-cluster Radar uses Service addresses instead. **Use auto-discovery**, below the backend URL, stages an empty connection and names what saving will remove from this context: the endpoint and credentials. On Cost the same action is **Reset to Automatic**: it also returns the cost source to Automatic and removes the Kubecost cluster mapping. **Save changes** applies it; **Discard** restores the saved settings. The action is disabled when already automatic without overrides. **Copy:** on another context, choose **Copy settings from…** and select the source from the searchable picker: another context, or **Previous global settings** (see below). The current form becomes an unsaved draft; adjust the endpoint or credentials before choosing **Save changes**. Choosing another source replaces the draft without saving it. **Discard** restores the previous settings. Replacing an existing connection requires confirmation when saving. This makes an independent copy, not a shared reference. Later edits affect only the selected cluster. Another context appears as a source only when it has a saved explicit endpoint (URL) for that integration; its discovery-only credentials cannot be copied, so enter them again on each context. Previous global settings are listed separately and can include URL-less sources. Only copy a backend that serves the destination cluster. A reachable endpoint does not prove it contains that cluster's data. Authentication and tenant headers are copied server-side on Save; their values are never returned to the browser. You can replace or remove them in the draft. If the source changes before Save, Radar rejects the stale copy rather than silently using different credentials. Kubecost's source cluster mapping is never copied: keep or edit the destination ID, or allow discovery to detect it. Environment-backed headers retain references, not resolved secret values; both copies can still depend on the same environment variable. **Edit:** the regular form always edits this context only. Each context stores its own endpoint and credentials. **Credentials:** Settings displays header names and whether a token/key exists, never saved values. Type directly into a credential field to replace its value; leave it untouched to keep it, or choose **Remove** to clear it when saving. Changing URL origin (scheme, host or port) requires replacing or clearing every retained credential, including after copying. Plain HTTP is supported but does not encrypt credentials in transit; use HTTPS outside trusted local paths. **Connection checks:** Metrics saves first and then tests reachability; an unreachable backend remains saved with a warning. Saving an Argo CD endpoint or token, Kubecost mode, or Automatic with any Kubecost override (URL, API key or cluster ID) tests an isolated candidate first; a failed test blocks the save and leaves the previous connection active. Switching to auto-discovery and confirming a changed cluster save without a test. Automatic with no Kubecost overrides, or Prometheus-based cost mode, saves that preference without claiming a successful backend test. Explicit Kubecost connections check this cluster's mapping and data readiness; cost queries require a narrow cluster filter. Central Argo connectivity does not add cross-cluster resource discovery. **Cleanup:** switching to discovery or replacing saved settings explicitly removes this context's previous credentials. Other contexts are unchanged. Missing kubeconfigs never trigger automatic deletion. **Settings → Connection → Integration settings by cluster** lets you explicitly remove an integration for a kubeconfig entry no longer loaded, including discovery credentials and mappings. For the current context, use its integration tab. An entry marked **Kubeconfig not loaded** comes from a kubeconfig file this Radar run didn't load; it may still be in use by another Radar process. Unsaved edits stay in the dialog when switching Settings tabs. Closing it asks before discarding them. If another client changes the active cluster while you are editing, Radar freezes the old draft; explicitly discard and reload to edit the new cluster. A stale save is rejected rather than applied to a different context or merged with a concurrent file edit. #### File editing and refresh Let Settings create context keys and accepted target fingerprints. Each entry stores its own configuration directly under `profiles[context-key].integrations`, keyed by `metrics`, `argocd` and `cost`. For example, within an existing context's `integrations.metrics` settings (keep the actual `target` and `identity` generated by Radar): ```json theme={null} { "target": "", "prometheus": { "url": "https://metrics.example.net/prometheus", "headersFromEnv": { "Authorization": "PROMETHEUS_TOKEN", "X-Scope-OrgID": "PROMETHEUS_TENANT" } } } ``` Environment variables must exist in the Radar process; Desktop does not necessarily inherit your terminal environment. Missing variables pause only the affected connection. Environment-backed header references are file-edited, not editable in Settings. Radar rejects unknown fields and validates accepted cluster targets, URLs, HTTP headers and integration-specific rules when it reads the file; an invalid connection pauses only that integration. CLI and Desktop reread bounded file contents on the next integration operation, including background consumers, not only when Settings opens. A content hash avoids reparsing unchanged data. Concurrent saves use a file lock and revision checks. Draft revisions cover the selected context and integration, including redacted credentials: saving Argo CD or another cluster does not invalidate an unfinished Metrics draft. The exception: importing previous settings with an explicit endpoint on any context requires open drafts for that integration on other contexts to reload. A concurrent edit to the same settings requires reloading. Copy also checks the source revision, so an endpoint or secret cannot change unnoticed between selection and applying the copy. The final write also checks the entire file, so overlapping saves during a connection test can still conflict rather than overwrite another process. Malformed JSON, unrecognized fields or an unsupported version block the file without overwriting it. A recognized field used with the wrong integration, or another semantically invalid connection setting, blocks only that integration; unrelated edits preserve the invalid connection. Some empty JSON values accepted by the reader are normalized when written; the schema describes the written form. The stored format is versioned independently of Radar releases. Future format changes must retain support for existing v1 files or provide an explicit migration. Older readers reject unfamiliar fields and versions rather than silently dropping data. A newer format produces upgrade guidance, not an instruction to repair or delete the file. #### Cluster identity and recovery The context key includes the kubeconfig source path and in-file context name. Same-named contexts in different files do not share credentials. If a context is renamed or its file moved, copy its saved connection to the new context, then remove the old connection from **Settings → Connection → Integration settings by cluster**. Tools that write a new temporary kubeconfig for each shell produce a new key each time; launch Radar with `--kubeconfig` pointing at the stable file instead. That section groups saved integrations by kubeconfig entry; removal is offered for entries no longer loaded, with a confirmation for the selected integration. Each integration reads as saved, or as auto-discovery (Cost: Automatic) when it was switched back and no endpoint, credential or cluster mapping is left. Overview's collapsed **Configuration files** section explains the three local files and credential storage; the info button beside the cluster name in each integration tab identifies its kubeconfig entry. In-cluster installations show operator configuration guidance instead of local-file information. Discovery-only credentials may require reentry. An unavailable kubeconfig is distinguished from a context confirmed removed from a readable file. CAPI profiles use management-source/namespace/name identity, not temporary kubeconfig paths. Changes to the Kubernetes server, CA trust, TLS settings, proxy or user reference pause each affected integration, because a saved endpoint may now serve a different cluster's data. **Review changes** asks whether the settings still apply and lets you keep selected integrations together. To decline instead, **Use auto-discovery instead** (Cost: **Reset to Automatic**) removes that integration's saved endpoint and credentials after a confirmation, including a saved token or key with no URL. Anonymous discovery without a saved mapping needs no confirmation. Token rotation alone does not prompt. Replacing a cluster behind identical endpoint, trust and user reference is not detectable by this fingerprint. #### Startup overrides and older settings An explicit local `--prometheus-url` is a complete, read-only **Set for this launch** override bound to the initial context and target. It never inherits saved headers; local header flags require the URL flag in the same launch. Argo CD and cost environment overrides similarly form complete, context-bound launch configurations, not layers over saved credentials. Switching away disables the override; returning to that target restores it. Restart without the override to edit its saved settings. Workload-metrics scope assertions remain process-local and clear on a context switch attempt, even an unsuccessful one. Saved settings never restore them. Repairing a connection that was unusable at startup resumes automatic matching, not its startup assertion. Older global `config.json` integration settings are not applied to every cluster in local mode. When the loaded kubeconfig has exactly one context, Radar assumes they were meant for it and imports them at startup. This happens once, when `clusters.json` is first created: the first such launch imports, settings removed later are not restored, and other single-context kubeconfig files launched afterwards use the picker below. Integrations set by startup flags or environment are left alone. With several contexts, **Copy settings from… → Previous global settings** in each integration tab fills an editable draft; **Save changes** imports it for the current cluster, while **Discard** leaves its connection unchanged. Picker entries describe URL-less sources (for example, auto-discovery with a saved API key), and invalid previous settings stay listed with their error. Saved credentials remain hidden. An explicit endpoint is imported once per integration, then reused by copying from that context; URL-less previous settings stay available to every context. Discovery-bound credentials and Kubecost mappings respect their original context binding, and the draft names anything left behind. Settings → Overview explains the change and links to the affected tabs until you choose **Don’t show again**, which silences these reminders everywhere without removing the picker entry. The old file stays as a recovery copy, never a fallback; removal does not resurrect it. Older Radar versions still read that global file, so rolling back does not preserve the new context-scoped behavior. Missing-connection hints in Metrics, Rightsizing, PVC usage, Cost (including workload and application tabs), and Argo CD diff/health views keep their usual **Configure** action and add a note when previous global settings can be copied for this cluster. Metrics and Cost also note when another context's saved settings are available to copy if that backend serves this cluster; Argo CD does not, because the server that matters is the one owning the Applications in this cluster. The action opens the relevant Settings tab and never imports or saves anything. Existing errors remain visible, and a previous connection is not a guarantee that its backend is reachable. Working views are unchanged: if auto-discovery finds a working backend, Settings → Overview still explains the change. These recovery hints are for local CLI/Desktop configuration, not operator-managed or embedded Cloud installations. In-cluster OSS settings remain Helm/operator-controlled and read-only in the UI. Cloud remains installation-scoped; `clusters.json` is not a Hub configuration transport. The profile file and lock are created with Unix mode `0600`; a newly created directory uses `0700`. Existing directory permissions are not changed. Headers are plaintext, not encrypted: protect backups and your OS account. When creating or replacing the file manually on Unix, set `chmod 600 ~/.radar/clusters.json`; Radar does not change existing file permissions merely by reading it. Atomic replacement prevents partial writes but does not guarantee the latest save survives sudden power loss. On Windows, protect access with user-directory ACLs; Unix modes are not an ACL guarantee. Locking is intended for local filesystems, not validated for network homes. On Windows, an open reader or antivirus scanner may briefly prevent atomic replacement. A failed save leaves the previous file and running connection unchanged; reload and retry after that reader finishes. Shared OSS/Cloud configuration remains installation-scoped. A shared deployment's header-only configuration may still pair with its URL flag; changing a saved server requires replacing every inherited header source. These installations do not read local profiles. Cross-origin HTTP redirects are refused, including for custom auth and tenant headers. Headers require an explicit URL and are not sent during auto-discovery. [Workload matching](/docs/features/workload-metrics#automatic-attribution) and [PVC scope](/docs/features/workload-metrics#pvc-usage-scope) describe the cluster-identity checks and their limits. Other name-based metrics do not have those checks. In-cluster Radar has no context switcher. Configure the integration for that installation, using Secret-backed environment variables for credentials where supported; local config persistence is not a replacement for deployment configuration. ## Workload metrics overrides by run mode Workload metrics and PVC usage normally use automatic identity matching. `--prometheus-single-cluster` and repeatable `--prometheus-cluster-label` replace it for workload request/resource charts, history, Pod comparison and PVC usage. They do not scope rightsizing, node/HPA or the workload network/filesystem charts. Leave both unset for automatic matching. Configured labels must exist on each affected metric source, including kubelet volume stats. See [scope and remedies](/docs/features/workload-metrics#pvc-usage-scope). | Run mode | How to configure an explicit override | | - | - | | Local CLI / `kubectl radar` | Supply the flag on each launch that needs it. These overrides have no `config.json` key or environment-variable equivalent. | | Desktop | Automatic matching is supported; these overrides are not exposed in Desktop's flag parser or Settings. Use standalone CLI Radar if an explicit override is required. | | In-cluster OSS | The operator supplies `traffic.prometheusSingleCluster` or `traffic.prometheusClusterLabels` through Helm/GitOps, which applies them at process startup. | | Radar Cloud | The installation's operator configures the same agent Helm values; this is not a viewer preference or a Hub authentication setting. | Changing the Kubernetes connection, logical metrics backend or headers clears an active override and resumes automatic matching. A new local port-forward to the same discovered Service is not a different backend. Only reapply an override after verifying the new connection's scope. See [operator override details](/docs/features/workload-metrics#optional-operator-override). ## Startup Context Which cluster Radar comes up on depends on how you launched it. **The Desktop app reopens where you left off.** The context selected at startup and every successful context switch are recorded as `lastDesktopContext` in `~/.radar/settings.json`, and the next launch reconnects to it - the natural behaviour for a window you closed and reopened. **`kubectl radar`, `radar`, and `radar diagnose --standalone` start on the kubeconfig's `current-context`**, as `kubectl` would. A command typed right after `kubectl config use-context staging` runs against staging, and a cluster picked in the Desktop app days ago never redirects it. Terminal runs don't record switches either, so nothing you do in one moves where the Desktop app reopens. The separation is not a preference: the remembered cluster is written under a Desktop-scoped key that the CLI never reads, and there is no setting that opts the CLI in. To stop the Desktop app reopening on the last cluster, turn off **Reopen on the last used cluster** in Settings → Connection, or set in `~/.radar/config.json`: ```json theme={null} { "restoreLastDesktopContext": false } ``` Details worth knowing: * The remembered context records the kubeconfig file it came from, not just its name - the name alone is not a stable handle. With several kubeconfigs loaded, two files can define the same context name, and which one keeps the unqualified name depends on the order the files are read, so adding a file can hand that name to a different cluster. * Radar reopens only on an exact match: the same context, in the same file. Anything else - the context renamed or deleted, the file moved or no longer loaded - opens the kubeconfig's `current-context` instead, and says so in Diagnostics. A same-named context in another file is not treated as evidence that it is the same cluster; losing the convenience costs a click, landing on the wrong cluster costs more. * If the remembered cluster is unreachable (VPN down, for instance), Radar reports the connection failure rather than silently connecting to a different cluster. Pick another cluster from the header. * Clusters connected through CAPI are never remembered: their kubeconfig is a temporary file that no longer exists on the next run. * Turning the memory off takes effect on the next Desktop start: it clears the remembered cluster as well as stopping new recording, so turning it back on later starts fresh rather than reopening a cluster you stopped using months ago. ## Local terminal context Each local terminal tab shows the context Radar put in its temporary kubeconfig, with **Opened for** in the terminal toolbar. Hover over the label to see the full context name. This records the terminal's startup configuration; shell settings, `KUBECONFIG` changes, and explicit command flags can override it. Switching clusters in Radar leaves existing local shells running. A terminal opened for another context shows **Different context** and a **New terminal** action in the existing toolbar. Hover over the notice to see both full context names. The action opens a shell for the context Radar is now showing. Each tab retains the full context selected when it was opened. Reconnecting starts a new shell only while Radar has that context selected; otherwise it asks you to switch back or open a new terminal for the current context. The tab keeps its previous context label during reconnect attempts. Reconnect and Retry are disabled while Radar is showing a different context. When a context is selected, the toolbar says **Requested** before the first shell starts. The server checks the requested context against the same snapshot it exports, so switching contexts while a tab is opening cannot start that shell for a different context. Commands supplied by actions such as **Authenticate in terminal** are sent once per tab. Reconnecting does not repeat a command that was already sent. If the connection closes before it is sent, the command remains pending for the next connection. If Radar cannot create a temporary kubeconfig, the existing original/inherited kubeconfig fallback remains available and the tab says **Context not confirmed**. That shell's Kubernetes target has not been established by Radar. If the selected context differs from the tab's requested context, its notice says **Requested context differs**, and the tooltip keeps that request distinct from a confirmed kubeconfig. The Desktop app can also open an unconfirmed recovery shell when no kubeconfig is available. Its explicit empty-context intent must match the server's empty active context; once a context is selected, open a new terminal for it. These checks preserve context selection across opens and reconnects; they do not enforce a shell's live command target or detect a kubeconfig context being repointed to a different physical cluster under the same name. If a context switch fails before Radar changes its active client, a terminal requested for the new context is refused. Finish recovering that connection, switch back to the previous context, or copy the recovery command into an external terminal. Radar will not substitute a shell for the previous context. ## Namespace Picker The header has a namespace picker on the right. Pick a single namespace to focus the view, or **All namespaces** to see everything you have access to. Cluster-scoped resources (Nodes, Namespaces, PVs, StorageClasses) appear regardless of the pick if your RBAC permits them - they have no namespace to filter on. Namespace-restricted users without their own cluster-scoped RBAC won't see cluster-scoped sections at all. The pick is a per-user view filter - it doesn't change anything for other users sharing the same Radar instance. Locally, your pick is remembered per kubeconfig context across restarts. In shared (auth-enabled) deployments the pick lives for the session. The pick narrows Radar's own views only, and only because the UI sends it with each request. An API read that names no namespace (`?namespace=` / `?namespaces=`) never inherits a remembered pick: it returns everything you're allowed to see, within the `--namespace-scope` pin when one is set. So the same user reading through another client - AI tools over MCP, Radar Cloud's fleet-wide views and background pollers, or `curl` - is never quietly narrowed to a namespace picked while browsing. A few UI surfaces deliberately cover all your namespaces regardless of the pick: the namespace list in the ⌘K palette, the **My permissions** dialog, and the standalone Cost view (which disables the picker). Home's Cost Insights card follows the pick. Until you make a pick, local sessions default to the namespace set on the kubeconfig context (kubectl parity - the same namespace `kubectl` would use, including one set via `kubectl config set-context` or `kubens`). An explicit `--namespace` / `--namespaces` flag outranks the kubeconfig value, and contexts without either default to **All namespaces**. Once you pick namespaces or explicitly choose **All namespaces**, that choice sticks for the context and the kubeconfig value is no longer consulted. When Radar starts with `--namespace-scope`, the picker controls the process-wide cache scope instead of just a view filter. Namespaced informer caches are pinned to one namespace while cluster-scoped resources remain cluster-wide. Local/no-auth sessions can switch the scoped namespace, which rebuilds the cache in place. Auth-enabled and Radar Cloud sessions lock the picker to the startup namespace so one user cannot reshape the shared backend cache for everyone. **Single namespace only.** `--namespace-scope` pins the cache to exactly one namespace; scoping to several namespaces at once is not supported yet. Passing more than one (e.g. `--namespace=a,b`) fails at startup with a clear error rather than silently caching nothing. When scoped, the namespace picker becomes single-select, and a switch re-points the whole cache to the new namespace rather than adding to it. ### Namespaces missing from the picker If your account can use resources in some namespaces but isn't allowed to list namespaces cluster-wide, Radar can't discover which namespaces exist. The picker then shows only the namespaces Radar has been given: the kubeconfig context's namespace and any you configure. Add every namespace you use, then restart Radar: ```bash theme={null} kubectl radar --namespaces ns1,ns2,ns3 ``` Radar Desktop doesn't take command-line flags, so set the same list in `~/.radar/config.json` (this works for the CLI too): ```json theme={null} { "namespaces": ["ns1", "ns2", "ns3"] } ``` Radar probes each listed namespace for access and watches every namespace where access is granted - resource views then cover all of them, not just the first. The list is also each user's initial picker selection: locally via the launch URL, and in shared (auth-enabled) deployments as a per-session default the UI picks up when it loads. Like any pick, it narrows only the UI's views, not API reads that name no namespace. Clearing the picker back to **All namespaces** sticks for the rest of the session. The picker can switch between those namespaces or keep several selected at once. This covers built-in resource types and custom resources alike: CRDs (GitOps, Gateway API, etc.) are probed per-kind across the same list and watched in every granted namespace. The list is capped by `--max-scope-candidates` (default 20) - startup fails with a clear error rather than silently probing a subset. ## Audit evidence under partial access The unused ConfigMap/Secret check distinguishes observed use from evidence of no use. A visible workload or supported controller reference establishes use. An unused finding requires the relevant consumer inventories to be readable under Radar's audit scope and initially synced for that namespace. Unknown subjects contribute neither findings nor passing/evaluated counts. Scan-level `missingInputs` reports `configmap-references` or `secret-references` when this prevents an evaluation; zero findings alone do not establish a complete scan. The namespace picker selects audit subjects. For Reflector-annotated subjects, authorized visible consumers of remote mirrors can still establish source use. Unreadable cross-namespace consumers can prevent proving a Secret unused even when all local workloads are visible. ClusterIssuer credential namespaces are resolved from an observed cert-manager controller's explicit flag, including its literal or downward-API namespace environment value; an unresolved namespace is unknown. Explicit flags take precedence over configuration files; no controller default or unread file value is assumed. These checks cover Radar's supported reference fields, not arbitrary controller behavior. Cache readiness records initial synchronization, not continuous watch freshness; the existing scan memo can lag evidence changes by up to five seconds. The per-resource audit endpoint remains a findings array and has no completeness metadata. Use the scan response or AI resource context when that distinction matters. ### Resource budget checks Missing CPU/memory request and limit checks recognize both container resources and shared Pod budgets in `spec.resources` (or the workload's Pod template). Regular containers and restartable init sidecars are checked; one-shot init containers are excluded from these efficiency checks. For standalone Pods with no owner references, Radar reads the stored, admitted specification. Owned Pods are not checked directly by these efficiency checks. For workload templates, it considers request-from-limit defaulting, namespace LimitRanges, and possible shared-budget admission behavior. When those possibilities disagree, Radar leaves the affected evaluation unknown rather than reporting a certain pass or failure. Scan-level `missingInputs` includes `pod-resource-defaulting` when this uncertainty prevents an evaluation. These are configuration checks, not measurements of actual consumption. If LimitRanges cannot be listed, workload-template efficiency checks are skipped and `missingInputs` includes `limitranges`. ### Per-resource check suppression Add `radarhq.io/ignore-checks` with comma-separated check IDs to the resource named in a finding, for example `singleReplica,hostNetwork`. For workload Pod-spec findings, put the annotation on the Deployment or other owning workload, not only on its generated Pods. Unknown check IDs have no effect. The annotation travels with the manifest and suppresses the matching findings for every Radar reading it; it does not disable those checks for other resources. ### Storage review checks Three snapshot checks in **Efficiency**, at **Medium** posture priority (`warning` in the raw scan): | Check ID | Flags | Threshold | | - | - | - | | `pvcNoConsumer` | Bound PVC with no consumer observed | More than 24 h since `Unused=True` when its transition time is known; otherwise PVC older than 24 h | | `pvcLongPending` | Pending PVC with no consumer observed, including WaitForFirstConsumer claims | PVC older than 24 h | | `releasedPV` | Released PV kept with Retain, or not yet deleted with Delete; Failed PV (reclaim failed after its claim was deleted) | Retain: 24 h after release, or release time unknown. Delete: 1 h grace when release time is known, or immediately on a `VolumeFailedDelete` warning from the last 24 h. Failed: immediately | A consumer is any Pod (completed ones included) or a Deployment, ReplicaSet, StatefulSet, DaemonSet, Job or CronJob template. Templates count at zero replicas, and every StatefulSet claim-template ordinal counts, including those left after a scale-down. Generic ephemeral Pod volumes also count. A Pending PVC clone counts as a consumer of its source claim when `dataSource` or `dataSourceRef` names a core PersistentVolumeClaim. `dataSource` uses the destination's namespace; `dataSourceRef` uses its explicit namespace when set, otherwise the destination's namespace. A Bound clone does not count. Clone destinations count even if owned, young or deleting. Claims with a controller owner reference are excluded from audit subjects. CRD consumers (such as virtual machines), snapshot operations and external consumers may not be seen: **no consumer observed is not a statement that storage is safe to delete**. On Kubernetes 1.37+, the PVC protection controller sets an `Unused` condition on Bound claims (feature gate `PersistentVolumeClaimUnusedSinceTime`, beta, on by default). Radar uses it when present: * `Unused=False` (a running Pod uses the claim) counts as a consumer, even if Radar did not observe the Pod. * With `Unused=True`, the 24 h floor counts from when the claim became unused rather than from its creation, and the finding reports the condition's time. That time is approximate: Kubernetes notes it can be off in either direction because of controller delays. Right after the feature is enabled (for example, after an upgrade to 1.37), every unused claim's timestamp is the enable time, so long-unused claims are held back for 24 h and then reported with that shorter age. * Observed Pods and templates still count as consumers, including a zero-replica StatefulSet. * No condition, or status `Unknown`, keeps the creation-age behavior. Absence never means a claim is unused. `pvcLongPending` doesn't use the condition. The namespace picker selects PVC subjects. PVs and StorageClasses need the caller's cluster-scoped list grant; without the PV grant, PVs are not subjects of that caller's scan. `pvc-consumers`, `persistentvolumeclaims`, `persistentvolumes`, `pv-deletion-events` and `pv-release-time` appear in `missingInputs` only when that input's absence left a subject unevaluated. A gap that still lets a finding fire is stated in the finding instead, such as **reclaim policy not visible**, **StorageClass binding mode not visible** or **Deletion warning events not fully visible**. * Bound PVs count as passing `releasedPV` subjects; Available and Pending PVs are not counted. * Deleting claims and volumes (`metadata.deletionTimestamp` set) are `stuckTerminating` subjects, not storage-check subjects: that check flags them after 5 minutes. When the blocking finalizer is `kubernetes.io/pvc-protection` or `kubernetes.io/pv-protection`, the finding explains what holds the deletion and what happens when it is released, and warns against removing the finalizer by hand. A deletion can't be cancelled, so it doesn't tell you to complete it either. * In a namespace whose consumer inventories are unreadable or unsynced, an eligible claim with an observed consumer or a Bound claim with `Unused=False` still passes; any other eligible claim is neither a finding nor a pass (`pvc-consumers`). * A Released Delete PV within its 1 h grace, with incomplete event coverage (complete only when the caller can see all namespaces) and no observed current warning, is unknown: neither evaluated nor passed (`pv-deletion-events`). * PV thresholds use time since `status.lastPhaseTransitionTime`, which also excludes warnings from before the current phase. Without a nonzero transition time, release duration is unknown: a Released Delete PV without a recent deletion warning is neither evaluated nor passed (`pv-release-time`); a recent warning still establishes failure. A Released Retain PV is flagged with **release time unknown** and **PV age**, without implying the 24 h grace elapsed. ## Radar Cloud Radar is free and fully functional without an account. A Cloud button in the header offers to connect the cluster to [Radar Cloud](https://app.radarhq.io) - optional, and nothing else depends on it. | Variable | Effect | | - | - | | `RADAR_CLOUD_FUNNEL=off` | Removes the Cloud button entirely. `on` forces it on. | | `RADAR_HUB_URL` | Connect to your own Radar Cloud Self-Managed control plane instead of the hosted service. | | `RADAR_HUB_APP_URL` | That control plane's web origin, when it differs from `RADAR_HUB_URL`. | To connect a cluster from the command line, use `radar cloud install` (`--hub-url` for a Self-Managed control plane). `radar cloud install` and `radar cloud status` target one cluster, so they use the configured primary kubeconfig and report configured directories they ignore. With no configured source, they use the normal `KUBECONFIG` / default kubeconfig loading rules. Directory-only configuration must add a primary kubeconfig before these commands can run. ### What Radar sends Until you connect a cluster to Cloud, Radar makes three kinds of outbound request, all to Skyhook, none containing your resources, logs or events: * **Update check** - to `releases.skyhook.io`, with the Radar version, OS/arch, install method, whether it is running locally or in-cluster, and the installation timestamp when Radar can determine it. Radar caches the release result for one hour. Development builds are excluded. * **[Anonymous usage stats](/docs/configuration/usage-stats)**: once a day, strictly opt-in. * **Cloud dialog copy** - only when you *open* the Cloud dialog, to fetch the current terms shown in it. No identifiers are sent. `RADAR_CLOUD_FUNNEL=off` stops this request from ever happening. A standalone Radar sends your cluster's data nowhere: it talks to your Kubernetes API directly and keeps everything it reads on your machine. Connecting a cluster to Cloud is what changes that, and it is the point of connecting - the cluster's agent opens an outbound tunnel to the Hub so the team can reach the same views without each person holding kubeconfig access. Deciding whether to connect is a separate question from the requests above. ## Related Documentation * [CLI reference](/docs/configuration/cli) - Commands and operator-facing flags * [In-Cluster Deployment](/docs/configuration/in-cluster) - Deploy Radar inside your cluster with Helm * [Authentication & Authorization](/docs/configuration/authentication) - Proxy and OIDC auth for shared deployments # In-cluster deployment Source: https://radarhq.io/docs/configuration/in-cluster Run Radar as a pod in your cluster for shared team access. Helm chart, ingress, RBAC opt-ins, and graceful permission degradation. 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 `KUBECONFIG` to override in-cluster detection), see the [Configuration Guide](/docs/configuration/files). Choose the path that matches what you are trying to do: * **New installation:** follow [Quick Start](#quick-start), then configure how your team will reach and authenticate to Radar. * **Existing installation:** start with [Upgrading](#upgrading). The correct workflow depends on whether Helm, Argo CD, Flux, or another system owns the deployment. ## Quick Start ```bash theme={null} helm repo add skyhook https://skyhook-io.github.io/helm-charts helm repo update skyhook helm upgrade --install radar skyhook/radar -n radar --create-namespace ``` Access via port-forward: ```bash theme={null} kubectl port-forward svc/radar 9280:9280 -n radar open http://localhost:9280 ``` ## Installation settings Shared OSS Radar is configured by the installation operator, not by visitors to its web UI. Settings shows effective configuration and connection status; it does not save installation changes. Use Helm values (or flags/environment for a non-Helm deployment), then restart or roll out Radar. Kubernetes actions such as scaling workloads still follow their existing permissions. | Setting | Helm values | | - | - | | Prometheus-compatible backend | `traffic.prometheusUrl`, `traffic.prometheusHeadersFromEnv` plus Secret-backed `env` entries | | Argo CD API | `argocd.url`, `argocd.existingSecret`, `argocd.insecureTls` | | Cost data | `cost.source`, `cost.kubecost.url`, `cost.kubecost.clusterId`, `cost.kubecost.existingSecret`, `cost.currency` | | Audit policy | `audit.ignoredNamespaces`, `audit.disabledChecks` | | Helm OCI chart sources | `helm.ociSources` | | Timeline / MCP | `timeline`, `persistence`, `mcp.enabled` | For example, add this to your existing values file: ```yaml theme={null} audit: ignoredNamespaces: [kube-system, kube-node-lease, kube-public, "*-system"] disabledChecks: [] helm: ociSources: - oci://ghcr.io/myorg/charts ``` `audit: null` uses Radar's default system-namespace exclusions. Explicit empty lists include every namespace and enable all checks. Check IDs are listed in the `checks` object returned by `GET /api/audit`. Namespace exclusions accept exact names or one leading/trailing `*`. OCI entries are prefixes, not full chart references; private-registry credentials are not managed by these values. The chart renders audit/OCI settings into a read-only ConfigMap, separate from Radar's writable local files. Its checksum triggers a rollout when values change. Radar reads it at startup and rejects unknown fields, unsupported schema versions, invalid patterns, and unknown check IDs. It never writes ConfigMaps or Secrets to save UI edits. Rotating an externally managed integration Secret also requires restarting Radar; that Secret's contents are not part of the ConfigMap checksum. A timeline PVC does not persist installation settings. When upgrading from an installation where Settings allowed edits, copy the intended configuration into Helm values first. Old Pod-local edits are not automatically adopted, and generally did not survive Pod replacement anyway. Use a chart and binary from the same release: pinning an older `image.tag` does not add this configuration behavior to the older binary. Future document keys unsupported by a pinned binary fail startup rather than being silently ignored. Theme and sidebar pins stay in each browser; they do not change another visitor's preferences. Namespace browsing remains available. Authenticated users have their existing per-user namespace filters; without authentication, visitors still share the anonymous namespace selection. A forced `--namespace-scope` cache cannot be changed by shared-OSS visitors. Context switching and CAPI “Connect to Cluster” are also installation-owned. Live Traffic source selection remains an existing runtime control, not a persisted setting. This is configuration ownership, **not authentication**. An unauthenticated installation still gives visitors its configured Kubernetes access; configure [authentication](/docs/configuration/authentication) before sharing it outside a trusted network. The same read-only policy applies to authenticated OSS on a laptop, and to a Pod using an explicit kubeconfig with a non-loopback listener. A personal loopback CLI in a Kubernetes-hosted development workspace remains editable unless an operator file is supplied. Helm installations always supply that file, regardless of listener or kubeconfig overrides. Local unauthenticated CLI/Desktop settings remain editable. Radar Cloud retains its existing role-managed settings and personal preferences; the chart rejects non-default `audit` or `helm.ociSources` values in Cloud mode rather than silently ignoring them. For non-Helm shared OSS, use the existing flags/env for integrations. Structured audit/OCI settings can be supplied through a read-only JSON file selected by `RADAR_OPERATOR_SETTINGS_FILE`: Without that file, managed OSS uses the built-in audit policy and an empty OCI source list, not old values from `settings.json`. If `audit` is supplied, both arrays are required; use `[]` deliberately to remove exclusions or disabled checks. ```json theme={null} { "version": 1, "audit": { "ignoredNamespaces": ["kube-system", "*-system"], "disabledChecks": [] }, "helmOciSources": ["oci://ghcr.io/myorg/charts"] } ``` This file contains no credentials, is read once at startup, and opts the process into operator-owned settings. In Cloud it is ignored with a startup warning. ## Upgrading Upgrade Radar through the same source that manages the current installation. If Argo CD or Flux owns Radar, change the version in Git and let the controller reconcile it. Running `helm upgrade` directly against a GitOps-managed release creates drift and the controller may revert it. Before upgrading, review the [release notes](https://github.com/skyhook-io/radar/releases) between your current and target versions, especially for a major-version update. The Radar version shown on the Home page matches the published Helm chart version. ### Helm If Radar links you to a Helm release, use that release name and namespace. You can also find likely releases with: ```bash theme={null} helm list --all-namespaces --filter radar ``` Update the chart repository, then upgrade while preserving the values already set on the release: ```bash theme={null} helm repo update skyhook helm upgrade radar skyhook/radar \ --namespace radar \ --reset-then-reuse-values \ --wait ``` Replace `radar` with the release name and namespace used by your installation. `--reset-then-reuse-values` requires Helm 3.14 or newer. It starts from the target chart's defaults, then reapplies the release's existing values, so keys introduced by the new chart are not omitted. With an older Helm client, upgrade Helm or use the installation's maintained values file; plain `--reuse-values` is not equivalent and can omit new defaults. If the deployment is managed from a values file, use that same file with `-f values.yaml` instead of `--reset-then-reuse-values` so Git remains the reproducible source of configuration. The command installs the latest published chart; add `--version X.Y.Z` to pin the exact version shown in Radar (without the leading `v`). ### Argo CD or Flux Do not upgrade the live Helm release or Deployment directly. Update the version in the repository that owns the installation, then reconcile its controller: * **Argo CD Application:** if the Application directly references the Radar chart, update its Helm chart `targetRevision`. Otherwise update the chart or image version in the Git source referenced by the Application, then sync it. * **Flux HelmRelease:** update the Radar chart version in the HelmRelease source, usually `spec.chart.spec.version`, then reconcile the HelmRelease. * **Flux Kustomization:** update the Radar chart or image version in the Git source referenced by the Kustomization, then reconcile it. Radar links directly to a verified owning Application, HelmRelease, or Kustomization when it can. Open that object to confirm its source and health before changing Git. ### If the manager could not be identified Inspect the Radar Deployment before choosing an upgrade method: ```bash theme={null} kubectl get deployment --all-namespaces \ --selector app.kubernetes.io/name=radar \ --output yaml ``` Look at the Deployment's labels and annotations: * `meta.helm.sh/release-name` and `meta.helm.sh/release-namespace` identify the underlying Helm release. * `argocd.argoproj.io/tracking-id` or `argocd.argoproj.io/instance` points to Argo CD ownership. * `helm.toolkit.fluxcd.io/*` and `kustomize.toolkit.fluxcd.io/*` point to Flux ownership. A GitOps-managed chart normally has both Helm and GitOps metadata. In that case, GitOps is the source of truth: update Git rather than running Helm directly. If none of these markers exist, update the manifests or image tag in the system that originally deployed Radar. ### Verify or roll back Wait for the Radar Deployment to finish rolling out, then refresh Radar and confirm the new version on the Home page: ```bash theme={null} kubectl rollout status deployment/radar --namespace radar --timeout 5m ``` Use the actual Deployment name and namespace if they differ. For a Helm-managed installation, inspect `helm history` and use `helm rollback` if the rollout fails. For a GitOps-managed installation, revert the version change in Git and reconcile the owning object. ## Exposing with Gateway API If your cluster uses Gateway API, the chart can create an `HTTPRoute`: ```yaml theme={null} # values.yaml httpRoute: enabled: true parentRefs: - name: public-gateway hostnames: - radar.your-domain.com ``` With no custom `rules`, the chart routes `/` to Radar. `httpRoute` and `ingress` are mutually exclusive, and at least one `parentRefs` entry is required. See the [chart reference](https://github.com/skyhook-io/radar/blob/main/deploy/helm/radar/README.md#with-gateway-api-httproute) for custom rules and timeouts. ## Exposing with Ingress ### Ingress without authentication > **Warning:** Only use this behind a trusted private network boundary. Anyone > who can reach Radar can use the permissions granted to its ServiceAccount. > For shared or externally reachable installations, configure authentication > before exposing the ingress. ```yaml theme={null} # values.yaml ingress: enabled: true className: nginx hosts: - host: radar.your-domain.com paths: - path: / pathType: Prefix ``` ```bash theme={null} helm upgrade --install radar skyhook/radar \ -n radar -f values.yaml ``` ### Subpath Ingress (No Strip-Prefix) If your ingress forwards `/radar/...` to the Radar service as `/radar/...`, set `basePath` to the same prefix: ```yaml theme={null} # values.yaml basePath: /radar ingress: enabled: true className: nginx hosts: - host: tools.your-domain.com paths: - path: /radar pathType: Prefix ``` Then 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: ```yaml theme={null} 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-logout ``` Radar'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](https://radarhq.io) is for. You still run Radar in each cluster, but it dials out to the control plane, so there is no per-cluster ingress to wire up. ### With ingress basic authentication 1. **Create the auth secret:** ```bash theme={null} # Install htpasswd if needed: brew install httpd (macOS) or apt install apache2-utils (Linux) # Create the file and enter the password when prompted htpasswd -cB auth admin # Create the secret kubectl create secret generic radar-basic-auth \ --from-file=auth \ -n radar rm auth # Clean up local file ``` 2. **Configure ingress:** ```yaml theme={null} # 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 ``` 3. **Deploy:** ```bash theme={null} helm upgrade --install radar skyhook/radar \ -n radar -f values.yaml ``` ### With TLS (HTTPS) Requires [cert-manager](https://cert-manager.io/) installed in your cluster. ```yaml theme={null} # 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 ``` ## DNS Setup 1. **Get your ingress IP:** ```bash theme={null} kubectl get ingress -n radar ``` 2. **Create a DNS A record** pointing your domain to the ingress IP. **Multi-cluster naming convention:** ``` radar.. ``` Example: `radar.prod-us-east1.example.com` ## RBAC 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 * Kubernetes 1.37 Workloads, PodGroups, CompositePodGroups, PodCertificateRequests, and ClusterTrustBundles when the API server advertises them; the scheduling APIs are feature-gated * Pod logs (enabled by default) ### Opt-in Permissions 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. Also the traffic-source fallback (Hubble/Caretta): Radar dials the relay/metrics Service directly first, so this is only needed when a NetworkPolicy or routing blocks that direct path | | 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](/docs/configuration/authentication#cloud-mode-helm-bindings) | | 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 | | Webhooks view | `rbac.viewWebhooks: true` | `false` | Show admission webhook configurations. Off by default because they expose the cluster's admission-control posture; auto-enabled when auth or cloud mode enforces each user's own RBAC | | Node runtime evidence | `rbac.viewNodeRuntime: true` | `false` | Let upgrade-impact checks inspect kubelet metrics and effective configuration through `nodes/proxy`. Enable only for a trusted no-auth audience; authenticated users need this permission on their own Kubernetes identity | | 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 `patch` on nodes, `list` on pods, and `create` on `pods/eviction`, which are not included in the default ClusterRole. The read-only drain plan (`POST /api/nodes/{name}/drain-plan`) needs `get` on nodes, `list` on pods and `list` on `poddisruptionbudgets`; without the last one the plan is still returned, marked as not having evaluated PodDisruptionBudgets. Note the defaults differ when `deleteEmptyDirData` is omitted from the request body: the drain plan assumes `false`, while the drain itself (and the MCP `manage_node` drain) assumes `true` - API callers should always send the field explicitly. Add the grants via `rbac.additionalRules` or use [per-user authentication](/docs/configuration/authentication) so each user's own RBAC governs node operations. Enable features as needed: ```yaml theme={null} # 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: ```yaml theme={null} # values.yaml debug: image: my-registry.internal/busybox:1.36 ``` Radar 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. ### CRD Permissions Radar reads CRDs from many popular tools. Common CRD groups can be toggled individually: ```yaml theme={null} rbac: crdGroups: all: false # get/list/watch on every API group and resource, including Secrets # Individual groups (default true unless noted): 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 rules ``` Radar's NetworkPolicy check (an issue when a policy leaves every pod behind a Service with no allowed inbound traffic) also reads the policies of other engines that can allow what a core NetworkPolicy denies: Cilium, Calico, Antrea (`rbac.crdGroups.antrea`), EKS VPC CNI (`rbac.crdGroups.awsNetworkPolicy`) and `policy.networking.k8s.io`. If one of those APIs is served but Radar can't read it, the check stays silent for the Services it might cover, so keep these groups enabled. Calico 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. The chart grants cluster-wide `get/list/watch` on VictoriaMetrics VMAgent, VMCluster and VMSingle whenever it grants cluster-wide Secrets reads: `rbac.secrets: true`, `rbac.helm: true`, auth enabled (`auth.mode != none`), or cloud mode. These objects can carry inline credentials, so they share the same grant decision as Secrets; there is no separate VictoriaMetrics setting. All other VictoriaMetrics kinds are excluded from this rule. See the [chart reference](https://github.com/skyhook-io/radar/blob/main/deploy/helm/radar/README.md#crd-access) for exclusions and wildcard grants. Radar decides what it can read at startup, so RBAC changes take effect when the Radar pod restarts. `helm upgrade` rolls the pod whenever `rbac` values change; restart Radar yourself after changing its RBAC outside Helm. ### Graceful RBAC Degradation 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. **Stalled API requests.** If a read from the Kubernetes API stalls without the connection resetting (for example, a proxy whose upstream dropped silently), Radar ends it after 60 seconds with no data and retries, instead of freezing that resource's data until a restart. Slow transfers that keep making progress are never cut, and log streams, exec and port-forward are unaffected. **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** ```yaml theme={null} # 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.io ``` Set `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. ## Authentication For shared team access, enable authentication so each user gets per-user permissions via Kubernetes RBAC. See the **[Authentication & Authorization Guide](/docs/configuration/authentication)** for full setup instructions. **Quick start with proxy auth:** ```yaml theme={null} # values.yaml auth: mode: proxy ``` Then 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:** ```yaml theme={null} # 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/callback ``` ## Security Considerations When 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. 1. **Authentication**: Always enable authentication when exposing via ingress. Use [built-in auth](/docs/configuration/authentication) (proxy or OIDC mode) or basic auth (shown above) at minimum. 2. **RBAC scope**: The default ClusterRole grants cluster-wide read access. For namespace-restricted access, set `rbac.create: false` and create a custom Role/RoleBinding. Radar will gracefully adapt to the available permissions. 3. **Privileged features**: Terminal (`podExec`) and port forwarding grant significant access. Only enable these in trusted environments or when using [per-user authentication](/docs/configuration/authentication). 4. **Network access**: Consider using NetworkPolicies to restrict which pods can reach Radar. ## Timeline Storage: memory vs sqlite vs postgres Radar's timeline records every cluster change. Three 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. Requires `persistence.enabled=true`. * **`postgres`**: events persist in an externally managed PostgreSQL database. Pick this for durability across restarts and rolling updates, or when you want to avoid the single-PVC constraint. Use a dedicated database per Radar deployment. The DSN must be provided via an existing Kubernetes Secret (`timeline.postgres.existingSecret`); Helm never touches the credential. For SQLite, 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. `timeline.maxSize` is ignored for postgres and memory. 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`. ### PostgreSQL requirements * PostgreSQL 14+ (tested against 17). * A dedicated database and a user with DDL permissions so Radar can create/update the `radar_timeline_*` tables, indexes, and migrations on first startup. * `sslmode` and other TLS settings are controlled through the DSN string. * The DSN is read from a Secret; never put it in Helm values or `config.json`. * Restart the Radar Deployment after rotating the Secret because Kubernetes does not refresh environment variables in running containers. * There is no built-in import path from SQLite to PostgreSQL. Migrating historical events is not supported. * An unreachable PostgreSQL costs you the timeline, not the rest of Radar. Cluster views keep working, the Timeline page says the database can't be reached and points to the logs, which have the connection error (so does the diagnostics overlay), and Radar reconnects on its own once the database answers. Radar logs when the database becomes unreachable, how soon it tries again, and when it answers again, naming the database's host, port, name and user (never the password) with the driver's error; search the logs for `[timeline]`. Events that occur while it is down are not recorded and are not recovered later; the dropped count in diagnostics shows how many. Radar never falls back to an in-memory timeline, because that looks healthy and then loses everything on the next restart. * Because the rest of Radar keeps working and `/api/health` stays healthy, an outage is easy to miss. Alert on Radar's `/metrics` instead: `radar_timeline_available` is `0` while the database can't be reached (checked at most every 5s, with a 2s limit on each check), and `radar_timeline_dropped_events_total{reason="store_unavailable"}` and `{reason="store_failed"}` count the changes lost to it. For example, `radar_timeline_available == 0` held for 5 minutes. The reason itself is not in the metrics, because it names the database host; it is in the Timeline page and the diagnostics overlay. * Verified TLS modes: `require` and `verify-full`. ## Configuration Reference See [Helm Chart README](https://github.com/skyhook-io/radar/blob/main/deploy/helm/radar/README.md) 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/postgres) | `memory` | | `timeline.dbPath` | SQLite database path | `/data/timeline.db` | | `timeline.historyLimit` | Max events to retain (memory only) | `10000` | | `timeline.retention` | Retention (Go duration; `0` disables). Applies to sqlite and postgres. | `168h` | | `timeline.maxSize` | SQLite max DB + WAL size before oldest events are pruned (`0` disables). Not used for postgres. | `800Mi` | | `timeline.postgres.existingSecret` | Name of a Secret holding the PostgreSQL DSN (required when `storage=postgres`) | `""` | | `timeline.postgres.secretKey` | Key within the Secret holding the DSN | `dsn` | | `cost.source` | Cost source: `auto`, `prometheus`, or `kubecost`; controls stay editable only when this and the Kubecost URL, cluster ID, and Secret are empty | `""` (Auto) | | `cost.kubecost.url` | Kubecost 3 Aggregator URL; blank discovers local port 9004 and may fall back to the named SAML/OIDC bypass port 9008 without a key; required for agent-only clusters | `""` (discover local) | | `cost.kubecost.clusterId` | Cluster ID filter for a central Aggregator | `""` (detect literal `CLUSTER_ID`) | | `cost.kubecost.existingSecret` | Secret containing an optional Kubecost API key; setting it disables automatic port-9008 auth bypass | `""` | | `traffic.prometheusUrl` | Manual PromQL-compatible query URL (Prometheus, VictoriaMetrics, Thanos, Mimir). Required whenever headers are set | `""` (auto-discover) | | `traffic.prometheusHeadersFromEnv` | Prometheus headers sourced from environment variables, for secret-backed auth headers. Requires `traffic.prometheusUrl` - credentials are never sent to auto-discovered endpoints | `{}` | | `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.viewWebhooks` | Show admission webhook configurations | `false` | | `rbac.viewNodeRuntime` | Inspect kubelet metrics and effective configuration for upgrade impact | `false` | | `rbac.traffic` | Read Hubble TLS certs | `true` | | `rbac.crdGroups.all` | `get/list/watch` on every API group and resource, including core Secrets; overrides per-group flags | `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. ## Troubleshooting ### Pod not starting ```bash theme={null} kubectl logs -n radar -l app.kubernetes.io/name=radar kubectl describe pod -n radar -l app.kubernetes.io/name=radar ``` ### Ingress not working ```bash theme={null} kubectl get ingress -n radar -o yaml kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx ``` If the UI loads through an ingress but pod exec does not connect, ensure the ingress forwards WebSocket upgrades. If it does and exec still fails, see the next section: the terminal uses the same address check as changes. ### Node drain requests time out Drain waits for pod deletion by default. The default deadline is two minutes for eviction retries and deletion waiting together, followed by a final verification read of up to five seconds. Configure every proxy and client on this path to allow at least 150 seconds for the default. If you send an explicit drain timeout, allow more than that value plus five seconds and network latency. For ingress-nginx, add these annotations to your Radar Ingress, or set them under the Helm chart's `ingress.annotations` values: ```yaml theme={null} nginx.ingress.kubernetes.io/proxy-read-timeout: "150" nginx.ingress.kubernetes.io/proxy-send-timeout: "150" ``` These are the ingress-nginx controller's [timeout annotations](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#custom-timeouts). Other ingress controllers and load balancers have their own request or upstream response timeout settings. Set each layer explicitly; extending Radar's deadline cannot override a shorter limit in front of it. A browser, proxy or MCP timeout does not prove the drain failed or that nothing changed. Evictions may have been accepted already. Check the node's pods before retrying or shutting it down. API callers can use `waitForDeletion: false` and MCP callers `wait_for_deletion: false` for admission-only behavior, but must then verify deletion separately. See [MCP drain timeouts](/docs/features/mcp#node-drain-timeouts). ### Changes fail with "Radar refused this request" If you can browse, but every action that changes the cluster (apply, delete, scale, restart, drain, Argo CD or Flux sync) fails with *Radar refused this request because it came from ...*, the proxy in front of Radar is rewriting the `Host` header. The pod terminal does not connect, and a few views that load data with POST, such as application cost, fail the same way. Radar accepts these only from a page served at its own address, so that another website you have open cannot act through your browser, and a rewritten `Host` hides that address. The message names both addresses: where the request came from, and the `Host` Radar received. Fix it either way: * Configure the proxy to keep the original `Host` header. * Tell Radar the address you open it at. In the Helm chart: ```yaml theme={null} trustedOrigins: - http://radar.internal ``` Outside Helm, set `RADAR_TRUSTED_ORIGINS` or pass `--trusted-origins`. Use the scheme and host exactly as they appear in your browser's address bar, with the port if there is one. Separate several with commas. A trusted origin can do everything Radar's own page can: read, make changes and open pod terminals (and, on a local install, the local terminal). List only addresses that serve Radar. This setting does not cover the MCP endpoint at `/mcp`, which has its own `RADAR_MCP_TRUSTED_ORIGINS`. A page served from any other address cannot call Radar's API at all, reads included. That covers another port on `localhost`, because browsers treat all ports on a host as one site. If you run your own browser tool that calls Radar's API, for example a dashboard on `http://localhost:3000`, add its address to trusted origins the same way. curl, scripts and MCP clients are not affected. This read check relies on the `Sec-Fetch-Site` header, which browsers send only to HTTPS addresses and to `localhost`. If you open Radar over plain HTTP at any other address, a page on another site can still make Radar answer a read, though it cannot see the answer. Changes stay protected either way. Serve Radar over HTTPS to close that gap. Radar logs each refusal with the headers it received: ```bash theme={null} kubectl logs -n radar -l app.kubernetes.io/name=radar | grep '\[origin\] refused' ``` ### Metrics charts show "Prometheus not connected" Radar discovers Prometheus in the background as soon as it connects to the cluster (and again after every context switch), by listing Services and probing the ones that look like a PromQL backend at their in-cluster address. When it can watch Services in every namespace it reads them from its own cache instead of listing them from the API. Discovery also runs again in the background, without a timer, when Radar has no connection and a Prometheus-like Service is created or one of its Pods becomes Ready (so a Prometheus installed after Radar connects on its own), and when the connected endpoint stops answering queries or its port-forward closes. While that runs the charts show "Discovering Prometheus…"; once it ends without a connection, the charts and **Settings → Overview → Metrics** show why: * *no Prometheus service found in cluster* - nothing matched the well-known names or labels. Set `traffic.prometheusUrl`. * *No working Prometheus endpoint found. Candidate services could not be reached* - candidates exist but their Service addresses did not answer from Radar's pod. In-cluster Radar does not fall back to port-forwarding. Check endpoint health and network access, or configure a reachable `traffic.prometheusUrl`. This does not mean Prometheus is absent. * *Prometheus candidate monitoring/prometheus-server:9090 was unreachable, and NetworkPolicy monitoring/… isolates ingress to its pods with no rule admitting radar/…* - Radar found the reason. When a probe fails at the network level, Radar evaluates the core `NetworkPolicy` objects it can see in its own namespace and the candidate's against its own pod and the candidate's published endpoints, and names the policies only when they deny the connection outright (an egress denial on Radar's side is reported the same way). It stays silent whenever the answer is uncertain: `MY_POD_NAME`/`MY_POD_NAMESPACE` unset, a namespace-scoped install that cannot list policies in the candidate's namespace, an endpoint it cannot resolve, a `hostNetwork` pod, or a probe that was refused rather than unreachable. Two limits: this is the policy's declared intent - a CNI that does not enforce NetworkPolicy (kindnet) will still pass the traffic - and Cilium/Calico CRD policies are not evaluated. * *Prometheus headers are configured but no Prometheus URL is set* - see `traffic.prometheusHeadersFromEnv` above. For CLI/Desktop connection failures, see [local integration connections](/docs/configuration/files#local-integration-connections). ### Basic auth prompt not appearing Verify the secret format: ```bash theme={null} kubectl get secret radar-basic-auth -n radar -o jsonpath='{.data.auth}' | base64 -d # Should show: username: ``` ## Uninstalling ```bash theme={null} helm uninstall radar -n radar ``` If `radar` is a dedicated namespace and contains nothing you need to retain, delete it separately with `kubectl delete namespace radar`. # Usage stats Source: https://radarhq.io/docs/configuration/usage-stats Radar's opt-in, anonymous usage stats: what's sent, what never is, where it goes, and how to turn it off. Anonymous and strictly opt-in: usage stats show us which features get used, so we know what to build next. They go straight to us, with no third-party trackers, and never include names, contents, logs or the resources in your clusters. Settings > Privacy shows the exact report before anything is sent. ## When Radar asks * **A local Radar** asks once, in a small card the first time it runs, and in the What's New dialog after an upgrade (it opens by itself only for releases with enough to show; otherwise from the nav rail). "No thanks" is final. If you close the question without answering, Radar asks again after three months. * **A shared Radar** never asks; its operator decides. See [Shared Radar](#shared-radar). You can change your answer any time in Settings > Privacy. ## What's sent Once a day, and only if Radar was used: * Radar's version, OS, architecture, install method, and whether it runs locally or in a cluster. * Radar's setup: auth mode, timeline storage, whether MCP and Prometheus are on, cost source, and browser family. * How often each view, action and MCP tool was used, how often the command palette and search were opened, which requests failed, and which screens crashed. Actions are named by Radar's own route, never by what they acted on, for example `POST /api/helm/releases/{namespace}/{name}/rollback`. * Sessions, and active time as a range. * How many kubeconfig contexts you have, as a range, and for each cluster used: Kubernetes minor version, platform, node count as a range, and known integrations from a fixed list. It answers three questions: which features get used, how teams running Radar in a cluster use it compared with people running it locally, and which integrations matter most. ## What isn't No install ID or cluster ID. No timestamps of when you do anything, only daily counts. No names of resources, namespaces, clusters, contexts or hosts. No manifests, logs, events, metric values, URLs or anything you type. No API groups Radar doesn't recognize, since they may be your own. ## Where it goes Straight to `usage.radarhq.io`. Radar has no third-party trackers. The receiver rejects anything that isn't a report like the one above, and stores each report in our product analytics (PostHog) under a new random ID, without your IP address or its location. It is not used for sales. ## Turning it off | Where | How | | - | - | | Settings > Privacy | Turn the switch off. This also deletes the pending report, `~/.radar/usage-report.json`. | | Helm | `usageReporting.enabled: false` (or `true`) decides for everyone using that Radar. | ## Shared Radar A shared Radar (in a cluster, behind sign-in, or on an address others can reach) reports how the team uses it, so its configuration decides for everyone and nobody is asked: `usageReporting.enabled: true` in the Helm chart. Settings > Privacy shows the current state read-only. In a cluster, Radar also sends what it has when its pod stops, since the pending report doesn't outlive the pod. # Access control (RBAC visibility) Source: https://radarhq.io/docs/features/access-control Inspect what any ServiceAccount, User, or Group can actually do - without three kubectl describe calls. Pod and Workload detail pages surface blast radius so you can answer 'what happens if this is compromised?' in one click. Kubernetes RBAC is fragmented across `Role`, `ClusterRole`, `RoleBinding`, `ClusterRoleBinding`, plus implicit group memberships and the rule expansion behind every binding. Figuring out the effective permissions of a single ServiceAccount usually means three or four `kubectl describe` invocations and mental rule-set merging. Radar inverts the model: every RBAC-relevant detail page (ServiceAccount, Pod, Workload, Role, RoleBinding, Namespace) shows the reverse-lookup inline, framed as the question you're actually asking. ## What you can answer * **What can this ServiceAccount do?** Open the SA detail page. You see direct bindings, the deduplicated flat rule set, and inherited grants from implicit groups (`system:authenticated`, `system:serviceaccounts`, `system:serviceaccounts:`) bucketed into a collapsed section so direct grants stay front-and-centre. * **Who is actually running as this SA?** A "Used by Pods" section on the SA page lists every Pod whose `spec.serviceAccountName` matches. Empty state for unused SAs. * **What's the blast radius if this Pod is compromised?** Pod detail gets a Permissions section listing the most-permissive rules the Pod's SA grants. An alert banner fires when the SA has verb wildcards, cluster-admin bindings, escalation verbs (`escalate` / `bind` / `impersonate`), or cluster-wide `create pods`. * **What about every Pod this workload spawns?** Deployment / StatefulSet / DaemonSet detail pages carry the same Permissions section, framed at the workload level - every Pod the workload spawns inherits these grants. * **Who is bound to this role?** Open any Role or ClusterRole. The reverse-lookup section lists every binding that references it, with subjects expanded inline. * **What does this binding actually grant?** RoleBinding and ClusterRoleBinding pages show an inline rules preview from the linked role, plus a warning callout when subjects include wide groups (`system:authenticated`, `system:unauthenticated`, `system:masters`). * **What's the RBAC posture of this namespace?** The Namespace detail page is no longer a generic fallback - it shows RoleBindings configured in the namespace plus ClusterRoleBindings whose subjects reference it. Group-only cluster bindings are excluded from the cluster binding list since they touch every namespace and would be noise. * **Why can't I do X?** The header toolbar has a "My Permissions" panel - a live `SelfSubjectRulesReview` for your current identity in the active namespace. Faster than `kubectl auth can-i --list`. ## Severity cues The Permissions section auto-expands only when the host's SA has a real blast-radius hit (any of the trigger conditions above). Otherwise it stays collapsed beneath Conditions so day-to-day debugging isn't dominated by RBAC noise. Resource-only wildcards (e.g. an inherited `view` listing every `fluxcd.io` CRD) deliberately do **not** trigger the alert - they fire on every authenticated SA and would train operators to ignore the banner. Triggers require verb wildcards, cluster-admin, escalation verbs, or cluster-wide `create pods`. ## Permissions required Radar's own ServiceAccount needs `list` on `rolebindings` and `clusterrolebindings` in `rbac.authorization.k8s.io` for the reverse-lookup to work. With the Helm chart, enable `rbac.viewRBAC=true` to get these plus `list` on roles / clusterroles for rule expansion. When user authentication is enabled, Radar also gates every reverse-lookup read on the calling user's own permissions: * The endpoint returns 403 if the caller cannot list both `rolebindings` and `clusterrolebindings`. * "Used by Pods" on the SA page is additionally gated on the user's `list pods` permission in the SA's namespace, so a user with binding-list access but no pod-list access cannot enumerate pod names by querying SAs they shouldn't see. Without the binding permissions Radar degrades gracefully: the reverse-lookup sections hide and the rest of the detail page renders normally. See [Authentication](/docs/configuration/authentication) for the auth-enabled setup. ## AI integration The [MCP server](/docs/features/mcp) exposes a `get_subject_permissions` tool with the same data shape. Use it to answer "is this SA over-privileged?" or "blast radius if this Pod is compromised?" from your MCP client. Returns subject identity, bindings (each with `inheritedFromGroup` attribution and rule count), the deduplicated flat rule list, and for ServiceAccounts the list of consuming Pods. ## What's deferred Visibility ships first. The audit / linting layer is the natural follow-up but isn't in this release: * RBAC audit checks (wildcard grants, cluster-admin assignments, orphan bindings, unused roles) aligned to Kubescape control IDs. * A verb x resource matrix view on the SA page (rakkess-style) as a third tab. * A "Subject Explorer" top-level page for browsing Users / Groups that have no detail page today. * A graph topology view of Subject -> Binding -> Role -> Rule (`rbac-tool viz` style). * In-UI binding edits and a "can-i" free-form query UI. The current shape covers the reverse-lookup question, which is the one operators hit most often during incidents and access reviews. # Applications view - browse a cluster by app Source: https://radarhq.io/docs/features/applications Group a cluster's workloads into the application they belong to - resolved from ownership, GitOps, Helm, and labels - with a workload rail, topology, and search. Open source and Cloud. Available in Radar v1.8.0+ A deployable application isn't a Kubernetes object - there's no `App` kind. What you call "the billing app" is scattered across Deployments, a worker, a CronJob, a Service, maybe an Ingress, possibly across namespaces. The **Applications** view (`/applications`, or `g a`) reconstructs that: it groups a cluster's workloads into the app they belong to, so you browse by application instead of by raw kind. ![Radar's Applications view - a cluster's apps grouped from workload evidence, with Source, Class, and Environment filters and per-app version, readiness, and workload counts](https://mintcdn.com/skyhook/NUBrQ7AHIoh8MNYG/images/applications-list.png?fit=max\&auto=format\&n=NUBrQ7AHIoh8MNYG\&q=85\&s=770c2edf0a6faab9e9ea3c3ae369c931) ## How apps are resolved Radar resolves app membership from real evidence rather than forcing you to label everything. Each app row carries a **Source** facet so you can filter by where the grouping came from: * **Argo CD** / **Flux** - the GitOps Application, ApplicationSet, or Flux source that manages the workloads. * **Helm** - the release the workloads belong to. * **Label** - common app labels (`app.kubernetes.io/name`, `app.kubernetes.io/instance`, `app.kubernetes.io/part-of`, `app`). * **Ungrouped** - workloads Radar couldn't confidently attribute. The workload is always the addressable thing; app identity is grouping evidence layered on top, never a replacement for it. ### When inference isn't enough Grouping is heuristic by nature, and heuristics occasionally guess wrong. When they do, you don't fight the inference - you declare the answer. Set the `app.skyhook.io/app` annotation on the workloads that make up the app, using the **same value** on each, and Radar groups by that explicit identity: ```yaml theme={null} metadata: annotations: app.skyhook.io/app: billing ``` An **identity chip** on each row shows how confident Radar is in the grouping - **emerald** when the identity is *declared* (the annotation, or a GitOps source path), **neutral** when it's a *heuristic* match - with an evidence tooltip explaining the call. A declared identity is also what lets the [fleet view](/docs/cloud/applications) fold the same app across clusters; a per-cluster name alone can't. ## Per-app detail Open an app and you get an application shell - identity, a context strip, and a **workload rail** listing the app's members - wrapped around the same runtime tabs you get on any resource: **Overview, Topology, Timeline, Logs, Metrics, YAML**. The topology is scoped to the app, so you see how its workloads wire together without the rest of the cluster's noise. Hovering a workload highlights it and its resources across the graph while the rest dim; related resources open in place in a drawer. ![The argocd application in Radar - the workload rail on the left and the app-scoped topology graph, with argocd-server highlighted and the other workloads dimmed](https://mintcdn.com/skyhook/NUBrQ7AHIoh8MNYG/images/applications-topology.png?fit=max\&auto=format\&n=NUBrQ7AHIoh8MNYG\&q=85\&s=5a7cdd5dc7f6a2506bdb1d942363f3ad) ## Workload metrics Select a Deployment, StatefulSet or DaemonSet in the workload rail, then open **Metrics** to inspect CPU, memory and throttling alongside HTTP request rate, errors and latency when supported telemetry is available. These charts describe the selected workload, not the sum of every member of the app. Compare current Pods to find outliers, or use workload history to follow resource and request metrics across rollouts when ownership data is retained. See [Workload metrics](/docs/features/workload-metrics) for screenshots, data prerequisites, automatic discovery and how to interpret coverage. ## Rollouts and serving readiness Applications can contain several workloads at different points in a rollout. Radar keeps two signals separate so transient controller work is not mistaken for an outage: * **Ready** reports the target capacity currently available to serve. Surge Pods never inflate the count above the target. * **Runtime** reports applying, scaling, waiting, progressing, paused, or failed rollout activity. While a rollout is in progress or stuck, the Applications list shows a compact runtime label beside readiness. Application detail expands that into separate Ready and Runtime facts, and the Workloads table shows each workload's rollout status and progress. This makes it possible to see that an application is still serving from its stable revision while identifying the Deployment, StatefulSet, DaemonSet, or Argo Rollout that is changing or stuck. Open a workload from the rail to reach the same actions and live rollout detail available in the [Resource browser](/docs/features/resources), including **Set image**, restart, rollback, logs, and YAML. ## Finding apps * Search filters apps by name; keyboard navigation (`j` / `k`, `Enter`) moves through the list and opens an app without leaving the keyboard. * The view sits in the left nav rail alongside every other view - jump to it with `g a`. ## Across clusters The single-cluster view above is the open-source and per-cluster Cloud surface. To fold the *same* app across the clusters and environments it runs in - one row per app, with version skew and promotion lag - see [Fleet Applications](/docs/cloud/applications) in Radar Cloud. ## See also * [Jobs and workflows](/docs/features/jobs-and-workflows) - inspect retained runs, execution steps, configuration, activity, and logs for batch workloads. * [Topology](/docs/features/topology) - the graph the per-app view scopes down. * [GitOps](/docs/features/gitops) - the Argo CD / Flux applications that feed app grouping. * [Fleet Applications](/docs/cloud/applications) - the same view folded across your fleet (Cloud). # Capacity Source: https://radarhq.io/docs/features/capacity Read-only capacity diagnosis for any cluster: node groups across every manager, the cluster scheduling ledger, and deep Karpenter answers - why a pod is pending, which NodePool could take it, why nodes are not joining. Available in Radar v1.9.0+ **Capacity** is Radar's read-only view of what your cluster can schedule and what manages that ability. On any cluster it shows the logical node groups (GKE node pools, EKS managed node groups, AKS agent pools, Karpenter NodePools), the detected capacity managers, and the cluster scheduling ledger - no cloud APIs involved. If [Karpenter](https://karpenter.sh/) manages your nodes, Capacity goes much deeper: it answers the questions you otherwise stitch together from `kubectl get nodeclaims`, controller logs, and scheduler events. It never mutates anything. ## When it appears Capacity is always in the sidebar (next to Cost, shortcut `g p`), and the Overview renders on any cluster. The page requires cluster-level node visibility; Karpenter access adds the deep screens. The Karpenter-specific screens (NodePool detail, Demand, Activity) and every Karpenter bridge require the integration: NodePools discovered **and** listable by your identity. When NodePools exist but your identity is denied them, the Overview still renders its cluster-only shape with an honest coverage banner, while the Karpenter screens fail closed - and the 403 is actionable: it names what remains visible without the grant and carries a copy-pasteable ClusterRole with exactly the permissions to request. Both `karpenter.sh/v1` and `v1beta1` work, including provider NodeClasses (EC2NodeClass, AKSNodeClass, and friends). ## The four screens ### Overview The recurring posture check: * **KPI tiles** - Capacity managers (each detected manager with a worst-of health rollup), Node groups (the unified inventory count, with Karpenter readiness as the detail), Nodes (total observed, with a pooled/unattributed breakdown), NodeClaims with a lifecycle rollup ("8 ready · 1 launched · 1 failed · +1 orphaned") on Karpenter clusters, and Pending pods. * **Cluster scheduling capacity bar** - all observed nodes, per resource: scheduled requests fill the allocatable track to scale; Karpenter in-flight claim capacity extends *beyond* the allocatable edge (capacity that will exist once claims register, never capacity the scheduler can use today); pending demand is a count chip after a `//` break, deliberately not to scale - demand can exceed the whole fleet. A resource the fleet does not have reads "0 requested of 0 allocatable · N pending" - the GPU-pool-missing story in one line. * **Operational signals** - prioritized findings (limit pressure, registration health, blocked disruption, pending demand), each linking to its diagnosis. A cascade collapses to its root cause: a broken NodeClass reads as one incident, not as a peer warning on every pool that references it. * **Node groups inventory** - every logical group across every manager, with nodes, allocatable, scheduled requests, and a typed scaling story ("5-11 nodes · target 9", "bounds not published in-cluster" - never a bare dash). Each row carries its platform identity (the label that makes the row exist), kept distinct from its autoscaler: "None detected" appears only when the autoscaler status was actually observed and named no manager; an unreadable or denied source reads "Detection unavailable" instead. Karpenter rows carry readiness and link into the NodePool detail; autoscaler-managed rows expand into the per-zone groups (MIGs / VMSS) as the autoscaler itself reports them: min-max, target, health, backoff, and the autoscaler's own probe time. * **Known to the autoscaler, unattributed** - groups the autoscaler reports that no observed node joins. Scale-to-zero groups live here until nodes appear; they are never counted as logical groups. ![Capacity overview](https://mintcdn.com/skyhook/I_xZiVKBf9GHKY3o/images/oss/capacity-overview.png?fit=max\&auto=format\&n=I_xZiVKBf9GHKY3o\&q=85\&s=564ad2da3856afa2b7fdc9c79b7f1d94) Capacity overview - managers, the scheduling bar, prioritized signals, and the cross-manager inventory ### NodePool detail The **capacity ledger** per resource: configured limit, provisioned (`status.resources` - Karpenter's actual provisioning gate), limit headroom, node allocatable, scheduled requests, unallocated, and metrics-sampled actual usage. A scaled-to-zero pool's ledger opens by narrating its true zeros instead of an empty table. Tabs cover claim lifecycle, fleet composition (capacity type / instance type / zone / architecture), workload attribution, and the full configuration. A broken pool leads with its diagnosis: the Issues card - cause, next step, an Inspect link into the NodeClass, and a deep link into the pool's provisioning episodes - renders before the ledger, with the raw controller conditions folded behind a toggle as receipts. Healthy pools keep the ledger first. Pools that declare accelerator requirements say so: a "Declared accelerator pool" fact echoes the declaration - well-known GPU requirement keys, or instance families recognized by each cloud's naming convention - never a parsed catalogue. ![NodePool detail with diagnosis-first ordering](https://mintcdn.com/skyhook/I_xZiVKBf9GHKY3o/images/oss/capacity-nodepool-diagnosis.png?fit=max\&auto=format\&n=I_xZiVKBf9GHKY3o\&q=85\&s=64e0f33787bb3c716cb617ffdbdf8df6) A broken NodePool leads with its diagnosis - cause, next step, and the accelerator fact - with the raw conditions folded behind a toggle ### Demand Pending pods grouped by **scheduling signature** - canonicalized selectors, affinity, tolerations, and requests - so five hundred pending replicas read as one group. Each group carries a conservative state (`awaiting_capacity`, `blocked`, `held`, `waiting_for_scheduler`, `unknown`), the scheduler's own reasons, and **pool evaluations**: the group checked against every NodePool's declared constraints with per-predicate evidence. When every evaluated pool rules a group out, the card headlines the dominant reason ("No pool can take this group - requires a label no pool declares") with the per-predicate tables as evidence beneath. Filter by state, by pool, by workload owner, or by pod (resolved server-side to the same owner the grouping uses) - the forms the Issues and Pod-drawer bridges use, filtered server-side so an empty result is a true zero. Empty states name their truth: nothing is pending (the success state), visibility is namespace-bounded, or no groups match the named filter. Evaluations cover Karpenter NodePools only: `blocked` means no NodePool can take the demand, not that no node can - on mixed clusters, capacity Karpenter does not manage may still satisfy it. ![Demand view with a blocked group](https://mintcdn.com/skyhook/I_xZiVKBf9GHKY3o/images/oss/capacity-demand-blocked.png?fit=max\&auto=format\&n=I_xZiVKBf9GHKY3o\&q=85\&s=61bc3ac5c3440810bead2fd764ab2c91) A blocked demand group headlines its dominant reason; per-pool predicate evaluations are the evidence beneath ### Activity A bounded evidence timeline of provisioning, disruption, interruption, termination, and config-change **episodes**, classified from Karpenter's exact event vocabulary. A **type rollup strip** summarizes the whole retained window ("Provision · 28 · 3 failed") as filter pills that stay stable while the type filter narrows the list; counts render as lower bounds whenever coverage is partial. Evidence tables lead with when/source/raw/references, with pipeline provenance behind a per-episode toggle. `DisruptionBlocked` and `Unconsolidatable` read as disruption being *blocked* - the opposite of disruption happening. A claim that records a failure signal closes its provisioning episode as **failed** the moment the signal lands; a claim deleted before ever reaching Ready with no recorded cause closes as **ended** - the episode never reads as still in progress, and Radar never asserts a failure it did not observe. Heuristic matches are labeled `inferred`, never presented as fact. ![Activity timeline episode](https://mintcdn.com/skyhook/I_xZiVKBf9GHKY3o/images/oss/capacity-activity-episode.png?fit=max\&auto=format\&n=I_xZiVKBf9GHKY3o\&q=85\&s=dfa071919d5a7e245088d9617d359176) A disruption-blocked episode with its raw evidence - when, source, and references ## Node groups without Karpenter The group inventory works on every cluster, built entirely in-cluster: * **Identity comes from node labels** (`cloud.google.com/gke-nodepool`, `eks.amazonaws.com/nodegroup`, `kubernetes.azure.com/agentpool`, `karpenter.sh/nodepool`) - never from parsing provider group names, which truncate. Nodes with no identity evidence are counted as **unattributed**, never guessed into a group. * **Autoscaler bounds** come from the `cluster-autoscaler-status` ConfigMap that the Cluster Autoscaler - including the GKE and AKS managed autoscalers - publishes in `kube-system`. Radar parses both the structured format (autoscaler 1.30+) and the legacy text format, and joins per-zone groups to logical groups by node-name evidence. * The ConfigMap's own timestamp renders as "as of T": healthy, quiet clusters publish hours-old payloads, so age is context, not breakage. ## Entry points You rarely start at the nav: * **Home** shows a capacity card on every cluster with a capacity story: full Karpenter posture when available, softened rows with an honest "Karpenter view unavailable" header when NodePools are denied, manager and group counts on Karpenter-less clusters - and no card at all when there is nothing to say. * **Issues** adds "View in Capacity" on capacity-relevant scheduling issues - it lands on Demand filtered to the affected workload. The flag fires for any pod evaluated against NodePools, compatible or not: a rejected pod is exactly the one the diagnosis serves. * **Pending pods** in the resource drawer offer "Evaluate against Karpenter NodePools", landing on Demand filtered to that pod's workload. The workload page offers the same bridge whenever its pods await scheduling. * **NodePool drawer** offers "Open in Capacity". * On clusters without Karpenter, the pending-pods tile hands off to Issues instead of dead-ending. ## Reading the numbers Every quantity carries its own certainty. Exact is the quiet default - glyphs render only on deviation, so `≥` and `?` stand out exactly where they matter: | Glyph | Meaning | | - | - | | (none) | Exact - the source was fully observed | | `≥` | Lower bound - partial coverage (namespace-scoped pods, sampled metrics) | | `≤` | Upper bound - a difference computed from a lower-bound input | | `?` | Unknown - the source was not observed; absence of data is never zero | The invariants behind the glyphs: **unavailable is never rendered as zero** (a denied or unobserved source says "Unavailable", not `0`), **partial is never rendered as exact**, and **scheduling capacity is kept structurally apart from actual usage** - Karpenter schedules on pod requests, so usage is an efficiency signal, never scheduler headroom. The bar never acquires health colors: high utilization is a bin-packing goal, not an incident. Two measured facts about pending demand are surfaced without changing a group's state. Requests from pods with `spec.priority < 0` are reported separately as `negativePriorityRequests` - a measured priority fact about potential preemption victims, not an overprovisioning claim, since actual preemption depends on scheduler policy, placement, and disruption constraints. Pods holding a scheduler node nomination (`status.nominatedNodeName`) are annotated per demand group; nomination is best-effort and can go stale, so it never changes the group's state or a pool's eligibility. ## What "declared compatible" means Pool evaluations check whether the pod's declared scheduling contract intersects the NodePool's declared provisioning contract: readiness, permanent taints vs tolerations, selector feasibility, configured limits, `minValues`, and observed member shapes. The boundaries are deliberate: * A required label the pool never declares is **incompatible** - Karpenter only applies labels from pool requirements and template labels. This is the classic misconfiguration. * Provider labels (zone, instance-type, capacity-type) are **unknown** when undeclared, but **compatible** when the pool's own `In` requirement bounds the values and the pod's need intersects them. * Requests that fit no observed member shape degrade to **unknown** - shapes are compared as whole vectors, so a pool cannot fabricate a composite machine from its biggest CPU and biggest memory on different instance types. Radar does not simulate provider offerings or bin-packing, and never claims a pod *will* schedule - declared compatibility is exactly the boundary where Karpenter's own provisioning and the cloud's actual capacity take over. ## Scope and RBAC Capacity is deliberately **cluster-wide**: supply (NodePools, Nodes, NodeClaims) is cluster-scoped, so the namespace view filter does not apply - scoping only the pod-derived numbers would show "my namespace's demand" against "everyone's supply". RBAC and the `--namespaces` deployment flag remain the only scopers, and coverage badges label both. ## Limitations * **No scheduling simulation.** Declared contracts only - no provider offering catalogues, spot availability, or bin-packing. * **DRA demand is invisible to the requests ledger.** Classic Dynamic Resource Allocation `ResourceClaims` (GA since Kubernetes 1.34) express accelerator demand outside container requests, so DRA-based accelerator demand never appears in the requests ledger - demand evaluation already degrades such pods to a labeled `unknown` rather than guessing. * **No trends yet.** Current state plus the bounded activity window. * **Single cluster.** Like the rest of Radar OSS, Capacity describes the connected cluster. ## See also * [Karpenter integration](/docs/features/integrations/karpenter) - topology chain and CRD detail views. * [Issues](/docs/features/issues) - where capacity-relevant scheduling problems surface first. * [Cost insights](/docs/features/cost) - the spend side of the same fleet. # CloudNativePG Source: https://radarhq.io/docs/features/cloudnative-pg PostgreSQL clusters run by CloudNativePG: which cluster needs attention and why, what each instance is doing, whether you could actually restore, and day-2 operations that are reviewed before they run and followed until the outcome is observed. Available in Radar v1.17.0+ When the [CloudNativePG](https://cloudnative-pg.io/) operator is installed, Radar adds views for your PostgreSQL clusters: which cluster needs attention and why, what each instance is doing right now, whether its backups would actually let you restore, and the day-2 operations - switchover, restart, fencing, hibernation, backups, restore - behind a review that shows exactly what will be written. It replaces stitching the story together from ten CRD lists, `kubectl cnpg status`, operator logs and a Grafana dashboard. ## Where it lives CloudNativePG stays under **Resources**; there is no new top-level navigation item. When the `postgresql.cnpg.io` CRDs are discovered, the CloudNativePG group in the Resources sidebar gains a **Views** block above its resource kinds: | View | What it answers | | - | - | | **Clusters** | Every PostgreSQL cluster with readiness, replication, storage, backups and its top problem, ordered by urgency. | | **Backups** | Recovery evidence per cluster, recent backup runs, schedules with their last outcome, destinations. | | **Declarations** | Databases, roles, publications and subscriptions by cluster - declared versus reconciled. | | **Pooling** | PgBouncer Poolers and the clusters they front. | | **Operator** | Whether the operator is leading, watching and reachable, and its plugins and image catalogs. | Sidebar badges count affected clusters, not findings, and follow the namespace filter. Opening the Cluster kind from anywhere - search, a favorite, a link from Issues - lands on the same Clusters view. ## Clusters The fleet: every cluster you can see, worst first. **Needs attention** narrows the list to clusters with a warning-or-worse problem on the Cluster, its instance Pods, the Jobs it runs, or an object that references it. Each row names its top problem in words, so you can tell an unschedulable first instance from a failing backup schedule without opening anything. ![CloudNativePG Clusters view with three PostgreSQL clusters](https://mintcdn.com/skyhook/dfXlS0BPTX1npIV3/images/oss/cnpg-fleet.png?fit=max\&auto=format\&n=dfXlS0BPTX1npIV3\&q=85\&s=a9a55b0a28bd380455c2d722c6f45ae9) The fleet - two of three clusters need attention, each with its top problem ## The Cluster page Each cluster's page is organized by task, and each tab is the one home of its facts: | Tab | What it holds | | - | - | | **Overview** | What needs attention, then one line per health dimension, PostgreSQL version and every operator condition. | | **Replication** | One card per instance - role, timeline, streaming state, how far behind each standby is - plus the high-availability configuration. Instance actions live here. | | **Storage** | Volumes and their usage, what is holding WAL, and resizing. | | **Performance** | Sessions and connection headroom, the lock-blocking tree, per-database health, and history charts. | | **Backups** | Recovery evidence, backup runs, schedules, and restore. | | **Activity, Logs** | Events and changes; logs merged across instances. | | **Configuration** | How to connect, PostgreSQL parameters as declared and as each instance reports them, and certificates. | Four health dimensions follow you on every tab: **Serving** sits on the title line, and **Replication**, **Storage** and **Backups** mark their tabs - a dot when something needs a look, a hollow ring when it could not be assessed, nothing when it is fine. A broken cluster leads with its cause. ![Cluster overview showing an unschedulable first instance](https://mintcdn.com/skyhook/dfXlS0BPTX1npIV3/images/oss/cnpg-cluster-overview.png?fit=max\&auto=format\&n=dfXlS0BPTX1npIV3\&q=85\&s=6b56eeaccc0d968641b4aa3cd21b2f28) A cluster that cannot start leads with the scheduler's reason, then each health dimension at a glance Replication measures each standby's catch-up as **replay backlog in bytes** - the primary's WAL position minus what the standby has replayed. PostgreSQL's `replay_lag` is an acknowledgement delay that reads zero for a standby that receives nothing, so Radar never presents it as catch-up time. ![Replication tab with instance cards](https://mintcdn.com/skyhook/5DH1vpXinq8pa2i5/images/oss/cnpg-replication.png?fit=max\&auto=format\&n=5DH1vpXinq8pa2i5\&q=85\&s=e802a6cc51f8241f349a0652e55a1cd7) Replication - the primary and its streaming standby, the backlog, and the slot the primary keeps for it ## Operations that are reviewed and followed through Day-2 operations run with your own Kubernetes identity, never Radar's: * **Back up now**, **switchover**, rolling or single-instance **restart**, **reload**, **fence** and lift fence, **hibernate** and rehydrate, node **maintenance**, ScheduledBackup suspend, resume, edit and run now, Pooler pause and resume, cancelling or terminating a blocking session, and destroying a standby. * Every dialog leads with the effect, keeps the literal API write one click away, and asks you to type the cluster name for disruptive actions. * The request carries the facts you reviewed - the context, the cluster's identity, the current primary, the fence. The server re-reads them and refuses if anything changed in between, and nothing is retried automatically. * Writes to objects a GitOps tool manages show whether the next sync is likely to revert them. ![Switchover confirmation dialog](https://mintcdn.com/skyhook/5DH1vpXinq8pa2i5/images/oss/cnpg-switchover-dialog.png?fit=max\&auto=format\&n=5DH1vpXinq8pa2i5\&q=85\&s=818c16cb99084a6e536180c4a2ffc0be) A switchover review - the candidate's replication state, the cluster's sync and quorum settings, and the exact status patch An accepted request is not a finished operation. The cluster header follows each operation until Radar observes the outcome, and completion is judged step by step on evidence. A step Radar cannot see keeps the operation **unobservable**, never completed. ![Operation tracker showing a completed switchover](https://mintcdn.com/skyhook/5DH1vpXinq8pa2i5/images/oss/cnpg-switchover-tracked.png?fit=max\&auto=format\&n=5DH1vpXinq8pa2i5\&q=85\&s=3c80e16eec380b1d779affe8c142357b) A switchover followed to completion - each step confirmed from what the cluster reports ## Backups and recovery The Backups tab answers the question that matters in an incident: could you restore, and to when? It shows the schedule, the destination, the last successful backup and where that fact came from, WAL archiving, the recovery window, and whether anyone has validated a restore. It catches the setup that looks fine and is not: with no destination configured, CloudNativePG still reports WAL archiving as working, because it accepts each WAL file without keeping it. ![Backups tab with recovery evidence and failed backup runs](https://mintcdn.com/skyhook/5DH1vpXinq8pa2i5/images/oss/cnpg-backups-evidence.png?fit=max\&auto=format\&n=5DH1vpXinq8pa2i5\&q=85\&s=aaa5d5e02c16e76c312d37f0eb17850d) A cluster that looks protected but is not - a blocked schedule, seven failed runs, and WAL that is accepted but never kept **Set up backups** guides the three steps: attach an existing Barman ObjectStore with an archive identity reserved for this cluster, create or repair a matching schedule, then verify that uploads resumed and a new base backup completed. Each change has its own review; saving configuration alone is never shown as protection. Radar does not create object storage or credentials. ![Set up backups guide](https://mintcdn.com/skyhook/5DH1vpXinq8pa2i5/images/oss/cnpg-backup-setup-guide.png?fit=max\&auto=format\&n=5DH1vpXinq8pa2i5\&q=85\&s=aa60168042e9edfa35c582e2d13ffe0d) The setup guide - storage, schedule, then verification from observed evidence **Restore to a new cluster** starts from a cluster, a Backup or an ObjectStore. You choose the latest point, a point in time or the end of a backup, and see what the source actually holds. A target outside the available evidence is warned about, never blocked. The restore is followed until the new cluster has a ready instance, and someone can then record what they checked as a restore-validation note on the cluster. ## Connect and create **Connect** lists every address for the cluster - read-write, read-only, any instance, and each Pooler - with whether the cluster's own Services currently have ready endpoints, plus copy-ready connection strings and `psql` commands. Credentials are named, never read: the password stays a placeholder. ![Connect dialog](https://mintcdn.com/skyhook/5DH1vpXinq8pa2i5/images/oss/cnpg-connect.png?fit=max\&auto=format\&n=5DH1vpXinq8pa2i5\&q=85\&s=9c42e823028b4368cd8e85dd6b805398) Connect - addresses with their availability, and commands to use them **Create** guides a new cluster's name, namespace, instance count and volume size, then opens the exact manifest for review. It can never update an existing cluster. ![Create Cluster dialog](https://mintcdn.com/skyhook/5DH1vpXinq8pa2i5/images/oss/cnpg-create.png?fit=max\&auto=format\&n=5DH1vpXinq8pa2i5\&q=85\&s=afb8c2dfc6bd644c6aabe4a3c7414294) Create a cluster, then review the manifest before anything is written ## Operator The Operator view answers whether CloudNativePG itself is healthy: whether the operator holds its leader lease, which namespaces it watches, whether its admission webhooks have ready endpoints, and reconcile errors per controller. When the operator is not reconciling, every cluster page says so before any status it shows. While its webhook is rejecting writes, actions that go through it are refused up front. ![CloudNativePG Operator view](https://mintcdn.com/skyhook/5DH1vpXinq8pa2i5/images/oss/cnpg-operator.png?fit=max\&auto=format\&n=5DH1vpXinq8pa2i5\&q=85\&s=eb92bb00e8ef2f5a09e7df23896a4be9) Operator diagnosis - leader lease, watched namespaces, webhooks and reconcile errors ## Reading the values Every value is something the cluster reports, labelled with where it came from. When something cannot be read, the page says so - it never shows zero, "none" or green in its place: * **Unknown is never healthy.** A standby whose lag cannot be read is "lag unknown", not caught up. * **Partial is never exact.** When some namespaces could not be read, counts read as a lower bound (`≥N`). * **No access names the permission.** A section you cannot read says which grant it needs, instead of looking empty. ## Access and data sources Radar uses your own Kubernetes permissions throughout, and each part of a page is authorized separately: reading a cluster never implies reading its Secrets, Jobs or Pods. * Live instance data - replication, slots, WAL, sessions - comes from each instance manager and PostgreSQL exporter through `pods/proxy`. * The blocking view and `psql` use `pods/exec`; without it, those parts say so and the rest of the page still works. * History charts come from Prometheus when it scrapes the CloudNativePG exporter. Without Prometheus, Radar samples while the page is open. * Secret values are never read, including in the downloadable report bundle. ## Limitations * **Restore validation is a human note.** Kubernetes records no restore tests, so a cluster is never shown as tested - only as validated by someone, with who and when. * **Third-party backup plugins.** Destinations for plugins other than Barman Cloud are not modeled. * **Single cluster.** Like the rest of Radar OSS, these views describe the connected cluster. ## See also * [CloudNativePG integration](/docs/features/integrations/cloudnative-pg) - the per-kind detail views these screens build on. * [Issues](/docs/features/issues) - where CloudNativePG problems also surface. * [GitOps](/docs/features/gitops) - the write guard behind every action. # Cluster audit Source: https://radarhq.io/docs/features/cluster-audit Built-in best-practices scan covering security, reliability, and efficiency. Inspired by Polaris and the NSA / CISA Kubernetes hardening guide. Configurable policy. No extra installs. Cluster audit runs a set of static checks against your live K8s state and surfaces failures inline on each affected resource. No CRDs to install, no extra controllers - it's part of the Radar binary. In the nav rail this view is labelled **Checks** (`g u`) and appears under the **Best practices** tab. For version-targeted compatibility analysis, use [Upgrade impact](/docs/features/upgrade-impact). ## What it checks The check set covers three categories. Defaults skip stylistic checks; they flag misconfigurations only. ### Security * Containers running as root, running privileged, allowing privilege escalation, or using a writable root filesystem. * Dangerous capabilities such as `SYS_ADMIN`, plus capabilities that are usually unnecessary and should be dropped. * `hostNetwork`, `hostPID`, and `hostIPC` access. * Container-runtime socket mounts and sensitive host paths. * Values that look like credentials or tokens stored in ConfigMaps instead of Secrets. * Automatically mounted ServiceAccount tokens on workloads that may not need Kubernetes API access. ### Reliability * Missing liveness or readiness probes. * Image tags that are `latest` or omitted, and mutable tags used without `imagePullPolicy: Always`. * TLS certificates in `kubernetes.io/tls` Secrets that expire within 30 days (danger within 7 days or once expired). This reads the stored certificate, not whether a service is serving it or whether renewal is failing. * Single-replica Deployments, and multi-replica Deployments or StatefulSets without a PodDisruptionBudget or topology-spread constraints. * Deployments whose rolling update allows every replica to be unavailable with no surge, so a rollout can remove all old Pods before replacements are ready. * Replicas that are all scheduled on one node, leaving no real node-level redundancy. * Services without matching Pods, Ingresses without their referenced Service, and supported Traefik routes or middleware with missing backends. * Deprecated Kubernetes API versions. * **Stuck terminating** - resources with `metadata.deletionTimestamp` set past the cleanup window (warning at 5min, danger at 30min). Most controllers complete cleanup within seconds; minutes-long delays usually mean a finalizer's owning controller is unhealthy or unable to reach a dependency. Findings name the blocking finalizers and point at the controller (Argo CD's `argocd-application-controller`, Flux's `kustomize-controller` / `helm-controller` / `source-controller`, etc.) to investigate. Same warning / danger thresholds as the per-resource [GitOps lifecycle Issue](/docs/features/gitops#lifecycle-awareness-terminating-doesnt-lie-about-state) so the two surfaces agree on severity. * **Stuck Crossplane resources** - managed resources, composite resources, and claims reporting `Ready=False` or `Synced=False` past the same window (warning at 5min, danger at 30min). Findings include the condition's reason and message, which usually carries the provider's upstream error. Paused resources are skipped. * CloudNativePG clusters with no ScheduledBackup declaring when backups run. This reports what is declared in the cluster, not whether backups exist. * When the cluster runs GitOps (at least one Argo CD Application, Flux Kustomization, or Flux HelmRelease exists): top-level workloads that no GitOps controller or Helm manages, and workloads installed with Helm directly instead of through a GitOps controller. ### Efficiency * Missing CPU or memory requests or limits, accounting for shared Pod budgets, restartable sidecars and admission defaults. See [Resource budget checks](/docs/configuration/files#resource-budget-checks). * ConfigMaps and Secrets that are not referenced by a supported workload, Ingress, or controller configuration. * PersistentVolumeClaims with no consumer observed - Bound claims older than 24 hours that no readable Pod or built-in workload template references. Claims with a controller owner are skipped, since hibernated databases and stopped virtual machines can intentionally keep claims without Pods. * Long-Pending PersistentVolumeClaims - claims without a controller owner, Pending since they were created more than 24 hours ago, that no Pod or built-in workload template references. `WaitForFirstConsumer` claims with no consumer are included. * Released or Failed PersistentVolumes - Released volumes kept for more than 24 hours with reclaim policy `Retain`, Released volumes still not deleted after 1 hour with `Delete` (sooner when a recent `VolumeFailedDelete` warning shows deletion failing), and Failed volumes immediately. No consumer observed is not a statement that storage is safe to delete: a CRD consumer such as a virtual machine, or a consumer outside the cluster, can still be using the claim. Consumer evidence, what happens when an input cannot be read, and phase-transition timing are in [Storage review checks](/docs/configuration/files#storage-review-checks). ## How to read it Open **Checks → Best practices** in the nav rail. You see: * **Summary and filters** - failing checks and individual findings broken down by severity, with category, framework, and search filters. * **Remediation queue** - one row per failing check, ordered by severity and affected-resource count. Expand a row for the rationale, remediation guidance, upstream references, and affected resources. The resource browser also shows an audit badge on rows with findings, and workload details include the resource's audit findings. These use the same scan results as the Best practices queue. ## Configure the checks policy Some findings are intentional in a particular environment - for example, a sandbox namespace where `latest` tags are acceptable or a system DaemonSet that legitimately needs host access. Use the settings button on **Checks → Best practices** to: * hide findings from an entire namespace; * enable or disable individual checks globally. Each check row also lets you disable a check or category. These instance-wide settings apply to everyone; in Radar Cloud, only owners can change them. For a per-resource exception, add `radarhq.io/ignore-checks` with comma-separated check IDs to the resource named in the finding. For Pod-spec findings, annotate the owning workload. See [Per-resource check suppression](/docs/configuration/files#per-resource-check-suppression). Local settings persist in `~/.radar/settings.json`. Radar ignores `kube-system`, `kube-node-lease`, `kube-public`, and namespaces matching `*-system` by default. ## Remediation guidance Every check explains why the configuration matters and how to fix it, with upstream references where available. Radar does not apply those changes automatically; make the change through your normal manifest or GitOps workflow, then refresh the scan. ## API + MCP Audit findings are available programmatically: * `GET /api/audit` - cluster-wide * `GET /api/audit/resource/{kind}/{namespace}/{name}` - single resource * MCP tool `get_cluster_audit` - filterable by category / severity / namespace That makes it easy to wire findings into CI, dashboards, or AI tools. ## See also * [Kubernetes upgrade impact](/docs/features/upgrade-impact) - blockers, review items, and incomplete evidence for a target Kubernetes minor. * [Resource browser](/docs/features/resources) - the inline audit chip per row. * [AI via MCP](/docs/features/mcp) - have an AI explain or remediate findings. * [Fleet checks](/docs/cloud/fleet-views) - audit findings rolled up across all clusters in your org (Cloud). # Compare resources Source: https://radarhq.io/docs/features/compare Diff two Kubernetes resources of the same kind side-by-side - YAML diff with field-level highlighting, swap, spec-only, hide-unchanged. Compare any two Kubernetes resources of the same kind side-by-side. The classic use case: "is this staging Deployment actually identical to prod?" or "why is `api-v2` flaking when `api-v1` is healthy?" - questions you'd otherwise answer by exporting two YAMLs and running `diff` by hand. ![Radar compare view - side-by-side YAML diff of two Deployments with red/green field-level highlighting, A/B pills, and view-mode toggles](https://mintcdn.com/skyhook/AQYQtvzB89gFEBOf/images/compare-view.png?fit=max\&auto=format\&n=AQYQtvzB89gFEBOf\&q=85\&s=0aa23f0d8f77dfcca8b124c37c033206) ## Two ways to enter **From a resource detail page.** Click the **Compare** button in the action bar. A picker opens listing every other resource of the same kind. Same-namespace candidates are surfaced first (usually the one you want), with `↑↓` arrow keys and `Enter` for fast keyboard picking. **From the resource table.** Hit the **Compare ⇄** toggle in the table header. The table flips into compare mode - a leading A/B-badge column appears and a sticky tray slides up at the bottom. Click two rows, then hit **Compare** in the tray. ![Radar resource table in compare mode - leading A/B column, sticky bottom tray with two pick slots and a Compare button](https://mintcdn.com/skyhook/AQYQtvzB89gFEBOf/images/compare-mode-tray.png?fit=max\&auto=format\&n=AQYQtvzB89gFEBOf\&q=85\&s=a9479f34638b3e90e4b5e47f7223d415) Both flows land on the same shareable URL: `/compare?kind=&apiGroup=&a=ns/name&b=ns/name` (the `apiGroup` param is only required for CRDs that collide with core kinds). ## What the diff shows Side-by-side YAML with Monaco's diff editor. Red lines on the left are present in A but missing or different in B; green lines on the right are present in B but missing or different in A. The overview ruler on the right edge gives you a bird's-eye map of where the changes are. Four header toggles, each constant-label with a highlight indicating the active state: * **Raw metadata** - off by default. Keep it off and Radar strips the server-assigned noise listed below (`uid`, `resourceVersion`, `managedFields`, `last-applied-configuration`, etc.) before computing the diff. Flip it on if you're debugging API-level oddities and actually want to see those fields. * **Spec only** - drops `status` from both sides so the diff focuses on intent, not the controller's view of current state. Useful when one resource is mid-rollout. * **Diff only** - collapses unchanged regions to a single line. The default; flip it off to see the full YAML for context. * **Unified** - switches between side-by-side (default) and a single-column layout with `+` / `-` lines. Unified packs more content per line height; side-by-side reads more like a code review. A small swap button between the pills flips A ↔ B, and the pencil icon on each pill re-opens the picker so you can change one side without losing the other. ## What's stripped before diffing K8s API responses carry a lot of server-assigned noise that always differs between two resources but doesn't reflect any meaningful difference. By default - with **Raw metadata** off - Radar strips it before computing the diff: * `metadata.managedFields` (server-side apply tracking - huge and never interesting in a compare) * `metadata.resourceVersion`, `metadata.uid`, `metadata.generation`, `metadata.creationTimestamp`, `metadata.selfLink`, `metadata.deletionGracePeriodSeconds` * `metadata.annotations.kubectl.kubernetes.io/last-applied-configuration` (massive duplicated JSON blob) * `metadata.annotations.deployment.kubernetes.io/revision` * `metadata.labels.pod-template-hash`, `metadata.labels.controller-revision-hash` User-defined labels, annotations, spec, and (unless you toggle Spec only) status all stay. ## Errors render per side If A loads but B 404s (a stale share-link is the typical cause), the working side still renders. The failed side's pill turns red, and a banner names exactly which side failed and what went wrong - click the pencil icon to pick a replacement without rebuilding the URL. ## Scope of v1 * **Same kind only.** Comparing a Deployment to a StatefulSet would be more confusing than useful. * **Two-way.** Cap is hard-set at 2 picks; selecting a third in the table replaces the oldest so the click always has a visible effect. * **Single cluster.** Compare works within one cluster. In [Radar Cloud](/docs/quickstart-cloud), the fleet **Compare** page diffs the same kind across two clusters. ## Keyboard | Action | Shortcut | | - | - | | Compare-mode toggle in table (header button) | *click* | | Pick / unpick a row in compare mode | Enter / d (on highlighted row) | | Exit compare mode | Esc | | In the picker: navigate candidates | ↑ / ↓ | | In the picker: pick highlighted candidate | Enter | | In the picker: jump to first / last | Home / End | # Cost insights Source: https://radarhq.io/docs/features/cost Kubernetes cost visibility from OpenCost-compatible Prometheus metrics or Kubecost 3. Available in Radar v1.8.4+ Radar shows Kubernetes allocation cost from either OpenCost-compatible metrics in your existing Prometheus or the APIs of a Kubecost 3 Aggregator. Both integrations are passive and read-only. ## What you see * **Cluster cost** - current hourly rate, monthly projection, and CPU, memory, storage, and network breakdowns when available. * **Namespaces** - sortable cost rows with workload details on demand. * **Workloads and applications** - focused CPU and memory allocation cost for Deployments, StatefulSets, and DaemonSets. * **Nodes** - instance, region, and hourly or monthly pricing where provider data is available. * **Trends** - historical allocation cost over 6h, 24h, or 7d with hover details. * **Allocation use** - CPU and memory shown separately when complete usage evidence is available. Radar presents monthly projections from the current hourly rate. Historical spend uses the selected range. OpenCost-compatible Prometheus data supports history at cluster, namespace, application, and workload level. Kubecost supplies the retained cluster and namespace trend from its Aggregator; workload and application history are not available through that path. ## Request guidance The workload Cost tab can also show [Rightsizing](/docs/features/rightsizing). Rightsizing uses Prometheus demand evidence to evaluate one workload's configured requests. It is separate from OpenCost allocation accounting and does not claim guaranteed savings. ## Choose a cost source **Auto** is the default. Radar keeps working OpenCost-compatible data from its discovered Prometheus. If representative cost metrics are absent, it looks for a Kubecost 3 Aggregator in the connected cluster. The Cost view appears only after a source returns usable current allocation data. You can pin the source to **Prometheus** or **Kubecost** under **Settings → Cost**. Auto and Kubecost are tested before Radar saves the selection. Prometheus can be saved before its cost metrics are installed, which supports staged installations. ### OpenCost-compatible Prometheus Radar uses its automatically discovered Prometheus or the endpoint configured with `--prometheus-url`. OpenCost or Kubecost must expose allocation metrics to that Prometheus. ### Kubecost in the connected cluster Radar discovers an active Kubecost 3 Aggregator StatefulSet and its matching Service. It tries the Service's named `tcp-api` port 9004 first. If that endpoint rejects unauthenticated access and no API key is configured, Radar can retry the same Service's exact `tcp-api-rbac` port 9008, which Kubecost exposes for internal clients when SAML or OIDC is enabled. A Service exposing only port 9008 is not auto-discovered; configure that endpoint explicitly. Local Radar opens a scoped port-forward to the selected port. In-cluster Radar uses Service DNS. A configured API key is never bypassed through port 9008: if the key is rejected, Radar reports the authentication failure. ### Central or federated Kubecost A cluster running only the Kubecost FinOps Agent has no local Aggregator for Radar to discover. Configure the central Aggregator URL and the exact cluster ID in **Settings → Cost** or through Helm. Radar accepts a root API URL or one ending in `/model`. Add a Kubecost service-account API key when the endpoint requires one. ## In-cluster Helm controls Setting `cost.source`, `cost.kubecost.url`, `cost.kubecost.clusterId`, or `cost.kubecost.existingSecret` through Helm makes the source, URL, cluster ID, and API-key controls environment-managed and read-only in Settings. `cost.kubecost.existingSecretKey` only selects the key within a configured Secret; its `api-key` default does not lock those controls. `cost.currency` is managed independently and makes only the display-currency control read-only when configured through Helm. | Value | Purpose | Default | | - | - | - | | `cost.source` | `auto`, `prometheus`, or `kubecost`. Empty selects Auto. | `""` | | `cost.currency` | Optional ISO 4217 display-currency override. Radar detects the cost tool's configured currency when possible, then uses USD. It labels values but does not convert them. | `""` | | `cost.kubecost.url` | Kubecost 3 Aggregator URL. Empty discovers a local Aggregator on 9004 and may use the controlled 9008 fallback described above. Required for agent-only clusters. | `""` | | `cost.kubecost.clusterId` | Exact cluster ID used to filter a central Aggregator. Empty detects one literal `CLUSTER_ID` from an active local FinOps Agent or Aggregator. | `""` | | `cost.kubecost.existingSecret` | Secret containing an optional Kubecost service-account API key. Setting it disables the automatic 9008 bypass. | `""` | | `cost.kubecost.existingSecretKey` | Key within that Secret. Radar sends its value as `X-API-KEY`. | `api-key` | ## Common gaps Some cost categories require additional provider or cost-tool configuration: * **GPU pricing** - requires GPU pricing data. * **Cloud egress** - requires cloud-provider integration. * **Persistent volumes** - requires storage-class pricing. * **Spot vs. on-demand** - requires provider and node metadata. ## Limitations * Radar does not change requests, instance types, or running workloads. * Workload and application cost currently focus on steady-state Deployment, StatefulSet, and DaemonSet workloads. * Batch workload, storage, and network attribution may remain at namespace or cluster level. * OSS cost data is per cluster. [Radar Cloud](/docs/quickstart-cloud) provides fleet views. ## See also * [Rightsizing](/docs/features/rightsizing) - workload-level request guidance and methodology. * [OpenCost and Kubecost integration](/docs/features/integrations/opencost) - prerequisites, discovery, and source behavior. * [Prometheus integration](/docs/features/integrations/prometheus) - metrics discovery and supported monitoring resources. # AI investigations Source: https://radarhq.io/docs/features/diagnose AI investigations for failing resources. Locally with your own agent CLI - Claude Code, Codex, Cursor, or OpenCode - or hosted in Radar Cloud with no CLI at all. Read-only until you approve a fix. Available in Radar v1.8.3+ Investigate a Kubernetes resource with AI to understand what is wrong, inspect the evidence, and choose your next steps. It comes in two forms that share the same investigation UI: * **Local (open source)** - Radar drives the agent CLI you already have - **Claude Code**, **Codex**, **Cursor**, or **OpenCode** - against a read-only tool mount, on your machine, under your existing subscription. No API key, no account. * **Hosted (Radar Cloud)** - the investigation runs in Radar Cloud with a Radar-managed agent. Nothing to install, no key to bring; see [investigations in Radar Cloud](#investigations-in-radar-cloud). For the walkthrough of a real investigation, see the [product page](https://radarhq.io/product/diagnose). For the security reasoning behind the design, see [Read-Only Is Not a Safety Boundary](https://radarhq.io/blog/read-only-is-not-a-safety-boundary). ## Requirements (local) * **Standalone Radar** (local, no-auth mode) - the BYO-agent form is not available when Radar runs with authentication enabled; in Radar Cloud you get the hosted form instead * **A supported agent CLI on your `PATH`**: Claude Code (`claude`), Codex (`codex`), Cursor (`cursor-agent`), or OpenCode (`opencode`, Radar v1.15.0+). Radar detects installed CLIs automatically; if none is present, the feature stays out of your way ## Starting an investigation From the UI: * The **Investigate** button on a resource's detail header - prominent when the resource has a problem, a quiet icon when it's healthy * Per-row on the **Issues** queue * The top-bar entry, which also shows a live count of running investigations From the terminal: ```bash theme={null} radar diagnose deploy/checkout-worker -n prod ``` The CLI starts the same durable server-side run the panel uses and prints a watch URL, so you can follow the transcript in the browser or stay in the terminal. Before your first run, a consent card explains where data goes (see [Privacy](#privacy-and-consent)). Consent is per machine and shared between the panel and the CLI. ![Issues queue with AI investigation actions](https://mintcdn.com/skyhook/I_xZiVKBf9GHKY3o/images/oss/diagnose-issue.png?fit=max\&auto=format\&n=I_xZiVKBf9GHKY3o\&q=85\&s=c69c362cb6ef3c860a032786f0f58622) Start an investigation from an issue ## During a run The investigation streams live: the agent's plan, each tool call with its timing, and a stall hint when the model is thinking. Runs are owned by the server, not the browser tab - close the panel, navigate away, or refresh and the run keeps going. Up to three investigations run concurrently. The tool surface the agent drives is **read-only by construction**: write tools are not present on the mount, and a regression test fails the build if one leaks in. The agent's built-in web search is disabled during a run, so what it reads stays between your machine and your model provider. Switching kubeconfig context marks in-flight runs as belonging to a different cluster - they refuse follow-ups and apply rather than answering about the wrong cluster. ![A live investigation with streaming tool calls](https://mintcdn.com/skyhook/I_xZiVKBf9GHKY3o/images/oss/diagnose-investigating.png?fit=max\&auto=format\&n=I_xZiVKBf9GHKY3o\&q=85\&s=9487c0390989d9d8c5583ace6be210ae) The investigation streams live - every tool call visible as it runs ## Findings: the evidence first As the investigation runs, verified results from the Radar tools the evidence pane understands are collected as **Findings** - the pane beside the transcript. Depending on which tools the agent calls, that includes the workload's logs, the alert rules that watch it, the Helm release it came from, the permissions its ServiceAccount holds, and the workload's vitals - CPU, memory and restarts over the pod set, captured by the `diagnose` MCP tool when it runs and Prometheus is reachable. The agent is instructed to argue its case over that evidence and cite the findings its claims rest on. A Prometheus range query about the target renders as a chart card with change markers, so "memory doubled at 14:02" is something you can see. Investigations can use spec edits, NetworkPolicy changes, Secret rotations and stale environment evidence. Results identify coverage limits; Secret write times from before Radar started may not establish a data change. A workload's HorizontalPodAutoscaler is diagnosed on the workload itself. Evidence that does not cover the HPA is never treated as conclusive about it. ## Results An investigation ends in one of three states: * **Root cause** - an explanation of what broke, its supporting evidence, and next steps with their rationale * **All-clear** - the agent verified the resource is healthy, not merely found nothing; absence of evidence is not treated as health * **Inconclusive** - RBAC-denied reads, missing data, or genuinely ambiguous state, stated plainly instead of dressed up as an answer An **Explain simply** chip rephrases the verdict in plain language, a follow-up composer continues the conversation, and a hand-off action copies the command to resume the session in your own agent CLI (Claude Code and Codex; Cursor and OpenCode sessions can't be handed off). ![Investigation results with root cause and next steps](https://mintcdn.com/skyhook/I_xZiVKBf9GHKY3o/images/oss/diagnose-verdict.png?fit=max\&auto=format\&n=I_xZiVKBf9GHKY3o\&q=85\&s=f38addbfce3c0f99bf0df190e5645390) Investigation results with evidence and recommended next steps ## Applying a fix Apply is deliberately separate from investigating: * The confirmation dialog shows the **exact change** that will run and states that the agent will modify your cluster using your kubeconfig credentials * An approved fix runs in a **fresh write-enabled session bound to that one confirmed change** - content the agent read during the investigation cannot steer what gets written. Apply without a confirmed fix is rejected * Changes to **GitOps- or Helm-managed resources** require an extra acknowledgment: a direct change is reverted on the next sync, so the durable fix belongs in the Git source * After an apply, Radar **re-checks the resource's health** rather than declaring success * If the agent nominates no safe step, there is no Apply button at all - remediation stays copy-only ![The apply confirmation dialog](https://mintcdn.com/skyhook/I_xZiVKBf9GHKY3o/images/oss/diagnose-apply.png?fit=max\&auto=format\&n=I_xZiVKBf9GHKY3o\&q=85\&s=b6096b0e248f84db3e8d134049f3c1e1) Apply shows the exact change before anything runs - in a separate write-enabled session ## Agents and isolation | Agent | Isolation during a run | | - | - | | Claude Code | All built-in tools disabled - cluster access is Radar's tools only. Your Claude settings, hooks, and CLAUDE.md still apply | | Codex | Isolated mode by default (ignores your user config and MCP servers); your full setup is available as an explicit trusted option. Built-in web search disabled | | Cursor | Sandboxed shell and file tools, but Cursor cannot suppress your globally configured MCP servers - it gets its own consent card saying exactly that | | OpenCode | Runs with your OpenCode configuration and provider setup, with `--auto`: permission requests from built-in tools and configured MCP servers are approved automatically, while explicit denials still apply. Radar does not enforce a CLI sandbox | Configure it under **Settings → AI investigations** (preferences are per-browser; consent is per-machine, stored server-side): * **Agent** - shown when more than one supported CLI is installed. Gemini CLI is detected and listed, but can't be selected yet. Reload the page after installing another CLI, or use the recheck in the setup notice * **How Radar runs it** - the execution profile, safeguarded vs full local, where the agent supports both (Cursor and OpenCode are full-local only); each choice spells out what is and isn't constrained for that agent * **Model** - Claude offers version-stable aliases (`opus`, `sonnet`, `haiku`); Codex and Cursor take a free-text model slug; OpenCode takes a `provider/model` name from `opencode models` * **Reasoning effort** - Codex only * **Clear history** - wipes finished runs; live runs survive Runtime bounds are env-overridable: `RADAR_AI_TURN_TIMEOUT` (default `15m` per turn), `RADAR_AI_MAX_TURNS` (default 15 model turns), `RADAR_AI_CLI_BIN` (force a specific agent binary). ## Privacy and consent Nothing about an investigation touches Radar's servers - runs execute on your machine, and Radar has no key, account, or telemetry in the loop. What the agent reads **does** go to your model provider (Anthropic, OpenAI, Cursor's backend, or the provider configured in OpenCode) under your own account; the consent card states this before the first run instead of burying it. Consent is acknowledged per machine, enforced server-side, and re-requested if the disclosure ever changes. ## History (local) Runs persist to `~/.radar/ai-runs.db` (SQLite, file mode 0600) so transcripts and verdicts survive restarts - retention is 100 runs or 30 days, whichever comes first. Runs interrupted by a restart are marked as errored rather than resumed silently; Cursor sessions cannot be resumed across restarts. Disable persistence with `--ai-history=false`, and clear it any time from **Settings → AI investigations → Clear history**. ## Investigations in Radar Cloud In Radar Cloud, investigations run **hosted**: open a cluster and start an investigation from the same UI - no agent CLI, no API key, nothing installed in your browser or on your machine. The agent appears as **Radar Hub (cloud)** in the agent picker, and its consent card describes the hosted data flow before the first run. How the hosted form differs from local: * **Where it runs** - each investigation turn runs as a sandboxed, single-purpose job inside Radar Cloud's infrastructure, using a Radar-managed model. Your cluster is read over the same tunnel your dashboard already uses * **Read-only, enforced outside the agent** - the job reaches your cluster through a short-lived, org- and cluster-scoped, read-only credential held by a separate gatekeeper process; the agent container itself holds no credentials, and write tools are refused fail-closed at that gate * **Requests you make use your access** - starting an investigation or asking a follow-up uses your Kubernetes permissions, including when you follow up on an alert-triggered investigation * **Radar 1.16.0+ required** - both user-started and alert-triggered investigations need this version on the cluster * **Alert-triggered turns use separate access** - they read as `radar:ai:reader`. See [AI investigation access](/docs/cloud/rbac#ai-investigation-access) for default permissions and how to restrict them * **No Apply** - hosted investigations never write to your cluster. The verdict gives you the remediation; you run it through your normal change process * **Transcripts persist in Radar Cloud** - runs are durable and multi-turn: follow-ups resume the same investigation, and history survives restarts and browser sessions * **A workspace, not a panel** - every investigation has a stable link and lives in the organization's investigations workspace with its history; Findings and charts are the same as in OSS * **Private until you share** - investigations you start are visible only to you until you share them with the organization. Organization members can then read the whole transcript and continue or stop the investigation. Only the starter can change its sharing. Owners can disable sharing of user-started investigations; existing shares become private and are not restored when sharing is re-enabled * **Shared results can exceed your access** - the transcript contains what each asker's permissions allowed them to read. Alert-triggered investigations are organization-visible from the start; human follow-ups use the asker's access * **A budget you can see** - **Settings → Organization → AI & MCP** shows the month's usage as a percentage of the monthly investigation budget. When automatic analysis is enabled for the organization, a second meter shows its usage against its own share. Automatic analysis pauses when its share is spent while manual investigations continue; when the whole budget is spent, both wait for the month to reset The privacy trade differs from local mode and is worth stating plainly: locally, cluster reads go to *your* model provider under *your* account; hosted, they are processed by a model managed by Radar Cloud - the same trust boundary as the rest of your Radar Cloud dashboard, which already serves your cluster data through the hub. ### On a self-managed Hub With [Radar Cloud Self-Managed](/docs/cloud/self-hosted), you run the control plane on your infrastructure. Here, the hosted engine is strictly opt-in (off unless you enable it in the chart) and calls the model through **your own provider account** - Anthropic, Amazon Bedrock, or Google Vertex AI. Each turn runs as a sandboxed Job in your cluster, and the model credential lives only in a sidecar broker for the provider you select. The chart's NetworkPolicy allows outbound HTTPS on port 443 to any destination, not only your provider; restrict it further with an egress proxy or a CNI policy that filters by destination. Investigations stay within your boundary: your Hub, your cluster tunnel, your model account. Setup, per-provider values, and adding it to an existing install are in [Self-Managed AI investigations](/docs/cloud/self-hosted/ai-agent). ## Troubleshooting * **No Investigate button (local)** - Radar is running with auth enabled (the BYO form is standalone-only), or no supported agent CLI was found on `PATH` * **Agent CLI errors** - failures from the underlying CLI (login expired, version too old) surface in the panel with the action to take * **403 when starting a run** - the consent card for the resolved agent hasn't been acknowledged on this machine yet # Exec & port-forward Source: https://radarhq.io/docs/features/exec-and-portforward Pod terminal, debug containers, port forwarding, and a local terminal pane - all gated by RBAC and disable-able per install. Radar can drop you into a pod, attach a debug container, forward a port to your laptop, or open a local terminal. Each is gated by RBAC and can be disabled per-install. ## Pod terminal Click **Exec** on any pod (or a container row). Radar opens a WebSocket-backed terminal in the side panel, falling back to SPDY automatically - so exec, attach, and copy work behind gateways (like the GKE Connect Gateway) that block SPDY. * Default shell cascade: `bash -il` → `ash` → `sh`. Override with `--pod-shell-default 'bash -l'`. * Multiple terminals can be open simultaneously; they're persisted in the URL so you can share or reload. * Explicit copy and platform-aware paste keep the terminal usable across browsers and operating systems. * Closing the panel terminates the exec session immediately. **RBAC:** needs `pods/exec` on the namespace. Disabled by default in the chart - opt in with `rbac.podExec: true`. Per-user installs (proxy / OIDC auth) inherit each user's exec permissions. **Disable cluster-wide** with `--disable-exec`. Running native sidecars (`initContainers` with `restartPolicy: Always`) support terminals and live-file browsing. Container pickers include regular containers and native sidecars; one-shot init containers are excluded. ## Debug containers For pods running minimal images (distroless, scratch), Radar can inject an [ephemeral debug container](https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/#ephemeral-container) with a full shell. Click **Debug** instead of **Exec** on the pod. The debug container shares the target pod's PID and network namespace by default - so `ps`, `netstat`, and friends see what the target sees. **Image:** defaults to `busybox:latest`, and the same image is used for node debug pods. In air-gapped or private-registry clusters where that can't be pulled, point it at a mirror with `--debug-image` (standalone) or the `debug.image` Helm value. Radar 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. **RBAC:** needs `pods/ephemeralcontainers` patch. Your identity must have this permission separately from Pod exec. ## Port forwarding Forward any pod or service port to your local machine. Click the port number in the resource row → **Forward**. * The list of active forwards is in the bottom-right tray. Click any one to copy the local URL. * SSE-pushed status keeps the tray live across pages. * Forwards survive page navigation but stop when you close Radar. * If a forward fails (for example, the target pod was replaced), a **Retry** button re-resolves to a running pod in one click. **RBAC:** needs `pods/portforward` on the namespace. Disabled by default in the chart - opt in with `rbac.portForward: true`. > [!NOTE] > > **Live forwarding only works when Radar runs on your machine.** When Radar runs in-cluster or in Radar Cloud, the forward's listener would bind on the Radar pod - unreachable from your browser - so live port-forward is gated off. Instead, every port shows a copy-paste **`kubectl port-forward`** command you can run with your own `kubectl` and cluster credentials, and HTTP ports get a [Probe](#probe-a-service-endpoint) button that works without any local setup. API: ```http theme={null} GET /api/portforwards # list POST /api/portforwards # create DELETE /api/portforwards/{id} # stop GET /api/portforwards/available/{type}/{namespace}/{name} # which ports a target exposes ``` ## Probe a Service endpoint When a live port-forward isn't possible (Radar Cloud / in-cluster), you can still answer "is this endpoint alive, and what does it return?" without leaving the browser. On an HTTP-ish Service port, click **Probe** (or **Curl**): Radar issues a single `GET` to the Service **from inside the cluster** and shows you the response - status, latency, byte count, headers, and a capped body. ![Radar's Probe response dialog - a GET to kube-dns:9153/metrics returning 200 OK with latency, byte count, headers, and the response body](https://mintcdn.com/skyhook/NUBrQ7AHIoh8MNYG/images/probe-service-response.png?fit=max\&auto=format\&n=NUBrQ7AHIoh8MNYG\&q=85\&s=ba0c88210a8b3cba55862057a9437a28) * **GET-only**, 10s timeout, 512 KiB body cap, and redirects are **not** followed (a probed workload can't bounce the request elsewhere or be used to leak credentials). * Only HTTP-ish TCP ports get the button (a heuristic over `appProtocol`, port name, and common HTTP port numbers); UDP and non-HTTP ports don't. * Edit the scheme and path before sending; the full response opens in its own dialog for large bodies like `/metrics`. **RBAC:** authorized by a SubjectAccessReview for `get` on the Service's `services/proxy` subresource, using your identity - so Probe never reaches an endpoint you couldn't reach yourself. No extra capability flag; a `403` surfaces in the response. (Local, no-auth installs are always allowed.) API: ```http theme={null} POST /api/probe/service # GET a Service via the apiserver services/proxy subresource POST /api/curl/service # in-cluster direct dial to a Service (no credentials sent to the workload) ``` ## Local terminal Radar can open a terminal on the **machine running Radar** (your laptop for local installs, or the Radar pod for in-cluster installs). Useful for ad-hoc `kubectl` / `helm` commands without leaving the UI. * Local installs: full shell access on your laptop. Honors your \$SHELL and login config. * In-cluster installs: access to the Radar pod's shell. Combined with the pod's ServiceAccount permissions, that's effectively a `kubectl` session. **Disable** with `--disable-local-terminal`. For in-cluster shared deployments where you don't want operators having a shell on the Radar pod, set this and `rbac.podExec: false`. ## Restricted-permission preview If you want to *preview* what a restricted operator would see, run with `--disable-helm-write` / `--disable-exec` / `--disable-local-terminal` to mask those features in the UI without changing your real RBAC. ## See also * [Authentication](/docs/configuration/authentication) - per-user RBAC via impersonation. * [In-cluster deployment](/docs/configuration/in-cluster) - the chart's RBAC opt-ins. * [CLI flags](/docs/configuration/cli) - the `--disable-*` switches and `--pod-shell-default`. # GitOps workspace Source: https://radarhq.io/docs/features/gitops Operate Argo CD Applications and Flux Kustomizations and HelmReleases from one fleet view, with topology, diagnosis, activity, and reconciliation controls. Radar gives Argo CD and Flux a shared operational workspace. Instead of treating GitOps resources as generic CRDs, it resolves their sources, maps the resources they manage, explains unhealthy reconciliation state, and puts the appropriate controls next to the problem. ![Radar GitOps fleet view with Argo CD Applications and Flux Kustomizations and HelmReleases](https://radarhq.io/images/radar/screenshot-gitops.png) ## Start with the fleet Open **GitOps** in the sidebar to see Argo CD Applications alongside Flux Kustomizations and HelmReleases. Each row shows the controller, source, destination, sync and health state, automation mode, and lifecycle state. Use search and filters to narrow the fleet by sync, health, project, namespace, labels, automation, or lifecycle. Radar resolves Flux source references to their repository URL, so both ecosystems are recognizable without opening the underlying CRD. Click a row to open its detail page. Every detail page has three top-level tabs: * **Topology** maps the GitOps resource to its sources, dependencies, and managed Kubernetes resources. * **Resources** lists the managed objects. The additional diagnosis available here depends on the controller and apply strategy. * **Activity** shows the reconciliation signal Radar can read from the controller resource. ## Argo CD and Flux capabilities The workspace is shared, but Radar does not pretend the two controllers expose identical data. | Capability | Argo CD | Flux | | - | - | - | | Fleet resources | Applications | Kustomizations and HelmReleases | | Source context | Repository, path, revision, destination, and project | Git, OCI, Helm, or bucket source, path or chart, revision, and target namespace | | Topology | Application-managed resources and nested Applications | Source, `dependsOn`, inventory or Helm-managed resources, and nested Flux resources | | Diagnosis | Application conditions, operation failures, drift state, child health, and lifecycle issues | Failing readiness and Helm conditions, stalled or reconciling state, source failures, and lifecycle issues | | Resources | Managed resources, recent Kubernetes events, and field-level drift when desired state is available | Managed-resource inventory and live resource state | | Activity | Current operation and deployment history, including initiator and revision | Current condition transitions and applied or attempted revision | | Operations | Sync, refresh, terminate, auto-sync, rollback, and selective sync | Reconcile, sync with source, suspend, and resume | For a screenshot-led Flux walkthrough, setup guidance, and Flux-specific limitations, see [FluxCD UI and Kubernetes integration](/docs/features/integrations/flux). For exact CRD and API coverage, see the [GitOps coverage matrix](/docs/features/integrations/gitops-flux-argo). ## Understand what an application manages The **Topology** tab is the common visual model for both controllers. Switch between graph and table views, search the resource tree, or filter by kind, sync, health, role, and namespace. Radar follows Kubernetes ownership from the GitOps root through workloads, ReplicaSets, and Pods. It also understands controller-specific relationships: * Argo CD Applications connect to the resources reported in their managed-resource tree. Child Applications open as their own GitOps detail pages. * Flux Kustomizations and HelmReleases connect to their source, `dependsOn` dependencies, and managed resources discovered from Flux inventory or helm-controller labels. Nested GitOps resources carry an **Argo** or **Flux** badge and open as their own GitOps detail pages rather than generic resource drawers. A lineage breadcrumb keeps the parent reachable as you move through nested Applications or a Flux dependency chain. ## Investigate the current state The issue band at the top of a detail page translates controller state into an operator-facing explanation. It separates controller-specific diagnosis from shared lifecycle problems: * **Argo CD** - operation failures, Application conditions, unhealthy or missing children, drift with auto-sync disabled, and drift that persists after a successful automatic sync. * **Flux** - failed `Ready`, `Healthy`, `Released`, or `TestSuccess` conditions, plus stalled or actively reconciling state, with the controller's message and affected source or dependency context preserved. * **Both** - pending deletion, finalizer ownership, deletion age, and controller health when Radar can read the controller Pods. ### Per-resource health Each managed resource shows a health value in the topology, the Resources tab, and the issue band. Radar labels where that value comes from: * **Argo CD's own verdict.** Argo evaluates every managed resource it has a health check for and rolls the results into the Application's health. Through Argo CD 2.x that per-resource result is also written into the Application object, and Radar shows it as is. * **Argo CD's verdict, read from its API server.** Argo CD 3.0 changed the default so the controller no longer records per-resource health on the Application (`controller.resource.health.persist` in `argocd-cmd-params-cm`, now `false`). The Application still reports its overall health, but every managed resource is left without one. When Radar can reach argocd-server it asks for the same per-resource verdicts and shows those: with the Argo CD integration connected (Settings → Argo CD), or with no setup at all on installs that allow anonymous read (`users.anonymous.enabled`). Once an anonymous read has succeeded, that connection is the integration: Settings shows it as connected with no token needed, and the Git-rendered diff works over it too; a server that refuses the read stays "Not connected". Radar accepts the answer only for the same Application (matching UID) and only when the Application deploys to the connected cluster. If the integration is set up and Radar still gets no usable answer — token rejected, server unreachable, a different install answering — the notice says so and why. * **Radar's own read.** When Argo's verdicts are neither on the Application nor reachable, Radar reads the live resources itself and reports what it finds: crash loops, image pull failures, failed `Ready` conditions, and the other problems its issues engine classifies. Those rows carry a **Found by Radar** marker, and a line above the issue band says the problems listed are Radar's findings. Radar only reports problems this way. It never marks a resource Healthy on Argo's behalf, and a resource it has nothing on is shown without a health value. A resource Argo has no health check for can still show a Radar finding when it is unhealthy; the marker makes clear that this is Radar's read, not a change in Argo's verdict. An Application that deploys to another cluster gets neither: Radar reads only the cluster it is connected to, so it derives nothing for that Application and says so. Radar Cloud can connect both clusters and show the Application's resources together. To see Argo's per-resource verdicts instead of Radar's read, connect Radar to your Argo CD server in Settings, or set `controller.resource.health.persist: "true"` in `argocd-cmd-params-cm` and restart the application controller. Radar switches to Argo's values automatically either way. The **Resources** tab provides deeper per-resource investigation for Argo CD Applications. Radar shows recent Kubernetes events and computes field-level drift when desired state is available from the Argo CD API or a resource's `kubectl.kubernetes.io/last-applied-configuration` annotation. Flux currently provides managed-resource inventory and live state, but not Git-to-live field diffs or the same per-resource event enrichment. ## Review activity Activity reflects the history each controller exposes: * **Argo CD** shows the current operation and deployment history with timestamps, revisions, initiators, and rollback actions. * **Flux** shows the current condition transitions and the applied or attempted revision. It is not a complete historical operation log. Reconciliation-related Kubernetes events also appear in the [Timeline](/docs/features/timeline), where clicking an Application, Kustomization, or HelmRelease opens its GitOps detail page. ## Operate the controller Radar uses the operation appropriate to the selected resource. Keyboard shortcut `s` opens Argo CD Sync or triggers Flux Reconcile. ### Argo CD | Operation | What it does | | - | - | | **Sync** | Applies the desired state with options for prune, dry run, force, replace, server-side apply, and selective resources. | | **Refresh / Hard refresh** | Re-fetches application state, optionally bypassing repository cache. | | **Terminate** | Cancels an operation in progress. | | **Suspend / Enable auto-sync** | Disables or restores automated sync while preserving its prior prune and self-heal settings. | | **Rollback** | Syncs to a selected deployment-history entry. | ### Flux | Operation | What it does | | - | - | | **Reconcile** | Requests an immediate reconciliation outside the configured interval. | | **Sync with source** | Reconciles the referenced source before the Kustomization or HelmRelease. | | **Suspend / Resume** | Toggles `spec.suspend`. | Mutating operations are disabled while a resource is terminating. The same operations are available to AI clients through Radar's [MCP server](/docs/features/mcp), with Kubernetes RBAC as the enforcement boundary. ## GitOps context across Radar GitOps ownership remains visible outside the workspace: * Resource drawers link tracked Kubernetes objects back to their owning Application, Kustomization, or HelmRelease. * Topology and Timeline open GitOps resources in their dedicated detail pages. * Helm releases installed by Flux show their owning HelmRelease and warn that direct Helm changes may be reconciled back. * Cluster Audit flags resources stuck terminating using the same lifecycle thresholds as GitOps detail pages. ## Permissions and scope Read-only visibility requires access to the Argo CD or Flux CRDs. Operations require the corresponding `patch` permissions. Canonical Git-rendered Argo CD diffs require a configured Argo CD API integration and access to its managed-resource data. See [in-cluster configuration](/docs/configuration/in-cluster) for Helm values and custom RBAC rules. Radar builds GitOps relationships from the cluster it is connected to. Argo CD hub-and-spoke installations cannot show Application-to-workload edges when the controller and workloads live in different clusters. Flux normally reconciles its local cluster, so its relationships are usually available. ## Go deeper * [FluxCD UI and Kubernetes integration](/docs/features/integrations/flux) - Flux workflows, screenshots, supported resources, setup, and limitations. * [GitOps coverage matrix](/docs/features/integrations/gitops-flux-argo) - exact Argo CD and Flux CRD and feature coverage. * [Topology view](/docs/features/topology) - how GitOps groupings fit into the wider cluster graph. * [Cluster audit](/docs/features/cluster-audit) - lifecycle and reliability checks across the cluster. * [MCP server](/docs/features/mcp) - GitOps operations and diagnosis for AI clients. # Helm management Source: https://radarhq.io/docs/features/helm View installed Helm releases, inspect values and rendered manifests, track available upgrades, choose a version, rollback, and uninstall - all without leaving Radar. Radar reads the Helm release secrets directly from K8s (no `helm` binary needed), so every release shows up automatically. From there you can see what's running, understand failed deployments, compare revisions, upgrade, rollback, or uninstall. ![Radar Helm view - releases with status, revision history, and a per-revision values diff](https://radarhq.io/images/radar/screenshot-helm.png) ## What you see The Helm view lists every release, scoped to namespace or cluster-wide: * Release name, namespace, chart, chart version, app version. * Status (`deployed`, `pending`, `failed`). * Revision number and last deploy time. * Owning resources (Deployments / StatefulSets / etc. that the release manages). * Operation signals for failed installs or upgrades, stuck pending operations, and rollback-after-failure sequences. * Source and ownership signals, including Flux-managed releases and releases whose upgrade source cannot be resolved. Click a release for the detail view: * **Overview** - chart metadata, release description, source state, current operation insight, and the best live resource to inspect when Helm recorded a rollout failure. * **Resources** - every K8s resource in this release, linked to its row in the resource browser. * **Values** - the user-supplied values, side by side with the chart defaults. * **Manifest** - the fully rendered manifest as Helm produced it. * **Notes** - the chart's `NOTES.txt`. * **History** - all revisions with timestamps, descriptions, and a per-revision diff. ## Compare revisions Each revision shows a structured diff against the previous one, and you can pick **any two revisions** to compare across values, rendered manifest, notes, hooks, and resources - not just adjacent ones. Compare opens a full-page workspace so the rendered output has enough room to be useful: * **Manifest diff** - line-by-line YAML diff of the rendered manifests. This is the source of truth when the chart changed names, labels, templates, or object identity. * **Rendered resource inventory** - a compact index of which Kubernetes objects were added, removed, or changed in place. When object identities do not overlap, Radar says so and points you back to the manifest diff instead of pretending it can pair unrelated objects. * **Field-level resource changes** - for objects that can be paired by kind, namespace, and name, Radar highlights meaningful spec changes such as image, probes, replicas, selectors, and ports. * **Values diff** - which user-supplied values changed. * **Notes diff** - changes to the chart's `NOTES.txt`. * **Hook evidence** - hook metadata and any retained Job / Pod / Event / log evidence associated with failed or running hooks. ## Upgrades, rollbacks, uninstalls | Action | What Radar runs | RBAC needed | | - | - | - | | **Upgrade / Downgrade** to a chosen version | Helm release upgrade to the selected chart version | `rbac.helm: true` (broad write) | | **Rollback** | Helm release rollback to a chosen revision | `rbac.helm: true` | | **Uninstall** | Helm release delete | `rbac.helm: true` | | **Inspect** | Read the release secret | Default RBAC | Read-only mode: pass `--disable-helm-write` (or run without `rbac.helm`) and the action buttons disappear; you still see everything. ## Diagnosing failed upgrades and hooks When an upgrade goes wrong, Radar reconstructs what happened from release history instead of leaving you to read raw revision rows: * **Failed upgrades and atomic rollback.** Helm doesn't persist whether `--atomic` was set, so Radar infers an atomic-style rollback from the revision sequence - a failed upgrade revision followed by a deployed rollback that points back to the previous release - and surfaces it as one operation (failed revision, rollback revision, target revision, and the failure description Helm recorded) rather than an ordinary healthy rollout. * **Stuck pending operations.** Pending installs, upgrades, and rollbacks get called out as active Helm operations, not just generic Kubernetes readiness problems. * **Likely resource to inspect.** When Helm's failure description names a live Kubernetes object, Radar links that object and shows its readiness/status signal so you can move from release history to the resource that blocked the rollout. * **Hook forensics.** For a failed or running hook, Radar shows the hook's identity, path, weight, lifecycle events, and last-run phase, and correlates the live Job / Pod / Event evidence plus short, secret-redacted log snippets when your identity can read them. * **Best-effort evidence.** Hook delete policies, `ttlSecondsAfterFinished`, garbage collection, or RBAC can remove the Job / Pod / Event / log data after Helm records the phase. Radar says when evidence is unavailable instead of pretending the hook didn't fail. Native Helm install / upgrade failures also surface in [Issues](/docs/features/issues) and reach AI agents over [MCP](/docs/features/mcp). ## Version-aware upgrades When a newer chart version is available, the release shows an **update badge**. The upgrade dialog lets you pick a specific **target version** - not just the latest - from the newest 50 versions (type to filter). Selecting a version older than what's installed is labelled as a **downgrade**. Upgrades run with Helm's `--reset-then-reuse-values`, so your previously-supplied values are preserved while the new chart's new default keys are picked up (plain `--reuse-values` can render `nil` for keys a newer chart added). ![Radar Helm upgrade dialog - a Target version dropdown set to an older version, with a 'This is a downgrade from 6.3.0' warning and a Downgrade button](https://mintcdn.com/skyhook/InHD1Kfb6VV2Mr3R/images/oss/helm-version-picker.png?fit=max\&auto=format\&n=InHD1Kfb6VV2Mr3R\&q=85\&s=6a2c9481b32bdc94f7953947ef151530) ## Chart sources and upgrades Upgrades require a confirmed chart source: its repository URL and chart name, or full OCI path. Open **Choose chart source**, select a configured source and use **Check and record source**. A name match or update badge does not verify where the installed chart came from; older source records may need confirmation. For public charts, choose **ArtifactHub → Search ArtifactHub**. This sends only the installed chart name, after your click. Review the publisher and address, then add the repository or OCI prefix. Use **Add manually** for private sources. Adding a source saves configuration without changing the release. Sources are shared; only owners can add or remove them in Radar Cloud. Configure repositories where Radar runs; your laptop's Helm cache may differ. For OCI charts, register a prefix such as `oci://ghcr.io/myorg/charts`. Private OCI sources need credentials from `helm registry login` available to Radar, including when it runs in-cluster. Review the version, settings and rendered changes before upgrading. Changing the source or settings requires a fresh preview. Values-only edits can use the stored chart unless dependencies are missing. Previews cannot guarantee hook, data or immutable-field compatibility; Helm does not upgrade CRDs from `crds/`. ## GitOps-managed releases If a release was installed by Flux's helm-controller, a direct `helm upgrade` would be reverted at the next reconcile. Radar detects this, shows a **Managed by Flux** chip, and routes you to the [GitOps view](/docs/features/gitops) to bump the version in Git - rather than offering an in-place upgrade that wouldn't stick. ## AI and MCP Radar exposes the same Helm context to AI agents through MCP: * `list_helm_releases` returns release status, resource health, Flux ownership, current operation insight, and a capped operation trail. * `get_helm_release` returns detailed release data, owned resources, history, operations, hooks, and hook diagnostics. Include diff options to compare revisions. * `issues` includes active native Helm failures and stuck pending operations as `kind=HelmRelease`, `group=helm.sh`. * `get_changes` includes native Helm release deployment history with `source: helm`, so agents can answer "what changed before this broke?" without scraping the UI. ## Concurrent edits If two operators try to upgrade the same release at the same time, Helm's own lock (the release `pending-upgrade` state) wins - Radar surfaces the conflict instead of silently overwriting. Failed upgrades show the failure reason and offer a one-click rollback. ## See also * [Resource browser](/docs/features/resources) - every Helm-managed resource shows its release. * [GitOps (Flux & Argo)](/docs/features/gitops) - if you'd rather manage Helm declaratively. * [In-cluster deployment](/docs/configuration/in-cluster) - `rbac.helm` opt-in. # Image filesystem Source: https://radarhq.io/docs/features/image-filesystem Inspect a container image's filesystem directly from its registry. Browse layers, search, and download files, with Pod-bound authorization in shared deployments. The image filesystem viewer lets you inspect container images referenced in the cluster and walk its files - same UX as a file tree in your editor. No Docker daemon is required. Radar reads the registry directly using the OCI distribution spec. Access depends on Radar's network path, registry authentication and, in shared deployments, your Pod permissions. ![Radar image filesystem viewer - layer picker, file tree, inline file preview](https://radarhq.io/images/radar/screenshot-image-fs.png) ## When you'd reach for it * "Did our build actually include this binary?" * "Which version of OpenSSL is in this base image?" * "What's in `/etc/ld.so.conf.d/` for this distro?" * "Is the JVM truststore baked in or expected to be mounted?" * "Why is this image 1.2 GB?" ## How to open it Click an image reference in resource details. With authentication enabled, open it from a Pod you can read; workload views direct you to a Pod. ## What's in there * **Layer picker** - pick a layer to scope the file tree, or "All layers" to see the merged final filesystem. * **File tree** - browse top-down. Click a file to open it. * **Search** - find files by name across the whole image. * **Inline view** - text files render as text; binaries get hex; symlinks resolve to their target. * **Download** - any file can be downloaded individually. * **Image metadata** - manifest digest, config blob, exposed ports, env, entrypoint, cmd, working dir, OCI labels. * **Layer details** - per-layer size, instructions (when the image still carries history), and "what this layer added / changed." ## Auth & networking Radar needs registry access from where it runs. Without authentication, it tries supplied pull secrets, the Google keychain for Google registries, then its Docker configuration (`~/.docker/config.json`). With authentication, inspection is tied to a readable Pod and its image. Using pull-secret credentials requires permission to read that Secret or exec in its namespace; Radar's own credentials require exec permission and the pulled digest. Credentials require HTTPS. Private registries are supported, subject to network checks. See [Image inspection authorization](/docs/configuration/authentication#image-inspection). ## Performance Layers are cached for a few minutes. Authenticated requests recheck registry access before serving cached data. Registry latency and image size affect load time. ## Limits * **No write.** This is a read-only viewer; you can't push back to the registry. * **No exec.** Use [pod exec](/docs/features/exec-and-portforward) if you need a shell instead of a file viewer. * **Compressed-only deltas.** Layer-level diffs work on what the registry stores; if a layer rewrites a whole large file, that whole file appears as "changed". ## See also * [Resource browser](/docs/features/resources) - inspect images from resource details. * [Cluster audit](/docs/features/cluster-audit) - "image is `latest`", "image not pinned to digest", and friends. # Integrations Source: https://radarhq.io/docs/features/integrations Argo, Flux, Cilium, Istio, Karpenter, KEDA, Crossplane, cert-manager, OpenCost, Kubecost, Prometheus, Trivy, Velero, Knative, and more - all detected at runtime, zero plugins required. Radar detects ecosystem tools at runtime. For CRD-backed tools, installing the CRD activates the matching renderer - typed detail views, smart columns in the resource browser, topology edges, AI-summary support, and the right action buttons (sync, suspend, rollout, restore). Data-source integrations use their own discovery, such as Prometheus Services or a Kubecost Aggregator. Every integration on this page is compiled into the binary; no plugins are required. Some remote or authenticated data sources require an endpoint or credentials. (New to Radar? Start with the [product tour](https://radarhq.io/product).) * **Graceful degradation.** Missing RBAC for a CRD: the panel shows a "needs read on `argoproj.io/applications`" hint, not a blank page. Missing CRDs entirely: the integration is silently skipped. * **No version handshake.** Radar reads the schema, not a hand-written client. New CRD versions land in upstream releases without a Radar update. ## GitOps & rollouts > **[GitOps (Flux + Argo CD)](/docs/features/integrations/gitops-flux-argo)** > > Argo Applications and Flux Kustomizations / HelmReleases / Git & OCI repositories. Sync, suspend, refresh, terminate; managed-resource inventory; topology grouping per app. > **[Argo Rollouts](/docs/features/integrations/argo-rollouts)** > > Rollout strategy, current revision, traffic split, pause / resume. > **[Argo Workflows](/docs/features/integrations/argo-workflows)** > > Workflow status, step graph, retry / suspend / resume. ## Networking & service mesh > **[Gateway API](/docs/features/integrations/gateway-api)** > > Gateways, HTTPRoutes, TLSRoutes, GRPCRoutes, TCPRoutes - typed views and topology edges. > **[Istio](/docs/features/integrations/istio)** > > VirtualServices, DestinationRules, traffic edges, mTLS scope. > **[Traefik](/docs/features/integrations/traefik)** > > IngressRoutes, Middlewares, TLSStores. > **[Contour](/docs/features/integrations/contour)** > > HTTPProxies and policy attachments. > **[Network Policies](/docs/features/integrations/network-policies)** > > Native Kubernetes, Cilium, and Calico policies, including staged-policy previews and workload coverage. With Hubble, why a flow was dropped and which policies apply. > **[Live traffic providers](/docs/features/traffic)** > > Hubble (Cilium), Istio, Caretta, and Beyla auto-detected for live request flows. Opens the Traffic view. ## Capacity & autoscaling > **[Karpenter](/docs/features/integrations/karpenter)** > > NodePools, NodeClaims, EC2NodeClass / AKSNodeClass / GCPNodeClass, full provisioning chain in topology. > **[Cluster API (CAPI)](/docs/features/integrations/cluster-api)** > > Declarative cluster lifecycle. Cluster, ClusterClass, KubeadmControlPlane, MachineDeployment, MachineSet, Machine - with infrastructure-provider renderers. > **[KEDA](/docs/features/integrations/keda)** > > ScaledObjects + ScaledJobs, current scale, triggers. > **[Vertical Pod Autoscaler](/docs/features/integrations/vpa)** > > VPA recommendations vs. live requests/limits, mode (off/initial/auto/recreate). ## GPU & AI/ML > **[Dynamic Resource Allocation](/docs/features/integrations/dra)** > > ResourceClaims with allocation lifecycle (incl. leaked-device warnings), DeviceClasses, ResourceSlice device inventory, Pod-to-claim relationships. > **[NVIDIA GPU Operator](/docs/features/integrations/nvidia-gpu-operator)** > > ClusterPolicy operator state and component rollout, NVIDIADriver lifecycle, MIG strategy. > **[AI/ML Ecosystem](/docs/features/integrations/gpu-ml-ecosystem)** > > Kueue admission and queue quota detail, JobSet member investigation, and resource visibility across KubeRay, KServe, Volcano, Kubeflow training and the wider AI/ML ecosystem. ## Cost & observability > **[OpenCost + Kubecost](/docs/features/integrations/opencost)** > > Powers the Cost view from OpenCost-compatible Prometheus metrics or a Kubecost 3 Aggregator. > **[Prometheus Operator](/docs/features/integrations/prometheus)** > > Prometheus, ServiceMonitor, PodMonitor, AlertmanagerConfig. Auto-discovered cluster Prometheus powers latency percentiles in the Traffic view and OpenCost-compatible data in the Cost view. ## Security & policy > **[Trivy Operator](/docs/features/integrations/trivy)** > > VulnerabilityReports + ConfigAuditReports inline on every workload. > **[Kyverno](/docs/features/integrations/kyverno)** > > Policies, ClusterPolicies, PolicyReports - violations attached to the resources they're failing on. > **[cert-manager](/docs/features/integrations/cert-manager)** > > Certificates, Issuers, ClusterIssuers, expiry tracking. > **[Sealed Secrets](/docs/features/integrations/sealed-secrets)** > > SealedSecret browse, decrypted-Secret link, status conditions. > **[External Secrets Operator](/docs/features/integrations/external-secrets)** > > ExternalSecrets, SecretStores, ClusterSecretStores - sync state and provider config. > **[Reflector](/docs/features/integrations/reflector)** > > Which Secrets and ConfigMaps are mirrors, where each came from, and how current its copy is. ## Infrastructure & data > **[Crossplane](/docs/features/integrations/crossplane)** > > Managed resources for every provider (Upbound AWS/GCP/Azure, provider-kubernetes, provider-helm, any community provider); Composite / Claim views with a per-resource live status badge on every composed resource; Compositions, CompositionRevisions, XRDs; upstream cloud errors surfaced verbatim. > **[Velero](/docs/features/integrations/velero)** > > Backups, Restores, Schedules - with restore actions in-line. > **[CloudNativePG](/docs/features/integrations/cloudnative-pg)** > > Clusters, Poolers, Backups; primary / replica visibility. > **[Strimzi connectors](/docs/features/integrations/strimzi)** > > Connector and task failures from KafkaConnector status in Issues, API and MCP, with operator-snapshot limits. > **[Knative](/docs/features/integrations/knative)** > > Serving (Service / Route / Configuration / Revision) and Eventing (Broker / Trigger). ## Unrecognized CRDs Any CRD not listed above still appears in the resource browser under its generic API group, with full YAML in the detail drawer. You don't get a typed renderer or topology edges; you keep search, filter, and edit. To add a typed renderer for a CRD family Radar doesn't yet know, see the [contributor guide](https://github.com/skyhook-io/radar/blob/main/CONTRIBUTING.md). ## Compared to plugin-based UIs | | **Radar** | **Plugin-based UIs** | | - | - | - | | AI / MCP context | Every integration in the same context window | Each plugin emits its own (or doesn't) | | Update cycle | One binary version pin | N plugins, N pinned versions, N upgrade paths | | Argo, Flux, Helm | Built-in | Install plugin | | Live traffic, cost, Trivy, Kyverno | Built-in | Install plugin per provider | ## See also > **[Topology](/docs/features/topology)** > > Where most integrations express themselves visually - typed nodes and edges per CRD family. > **[Traffic view](/docs/features/traffic)** > > Hubble / Istio / Caretta / Beyla auto-detection and the live edge overlay. > **[Cost view](/docs/features/cost)** > > OpenCost- or Kubecost-powered spend by namespace, workload, and node. > **[In-cluster RBAC opt-ins](/docs/configuration/in-cluster)** > > The Helm `rbac.crdGroups` block - what each integration needs read access to. # Argo Rollouts progressive delivery visibility Source: https://radarhq.io/docs/features/integrations/argo-rollouts Visualize Argo Rollouts in Radar: live Rollout status in topology, typed detail views, and AI summaries for canary and blue-green progressive delivery. [Argo Rollouts](https://argoproj.github.io/rollouts/) provides progressive delivery strategies including blue-green and canary deployments. | CRD | Group | Topology | Detail View | AI Summary | | - | - | - | - | - | | Rollout | `argoproj.io/v1alpha1` | Yes | Yes | Yes | | AnalysisRun | `argoproj.io/v1alpha1` | Yes | Yes | Yes | | AnalysisTemplate | `argoproj.io/v1alpha1` | - | Yes | - | | ClusterAnalysisTemplate | `argoproj.io/v1alpha1` | - | Yes | - | | Experiment | `argoproj.io/v1alpha1` | - | Yes | - | ### What Radar Shows **Control surface:** Set image updates one or more regular or init-container images on the Rollout's pod template or supported `workloadRef` target. Radar reads the current images before editing, rejects stale changes instead of overwriting them, and lets the Rollout controller start the resulting canary or blue-green rollout. A paused Rollout saves the template change but does not start rolling out until it is resumed. Promote, Promote full, Skip step, Retry, and Abort are also available on the Rollout detail page, each gated on a live capability probe (`patch rollouts` and `patch rollouts/status` are separate grants). Rollback goes through revision history, with an opt-in "skip canary steps" follow-up for hotfixes. **Rollout visibility:** Radar keeps serving readiness separate from transient rollout activity. Resource tables, drawers, full workload views, and Applications show the active step, progress, pause, or failure without marking capacity served by the stable revision as unavailable. **Canary/blueGreen progression:** The Rollout detail page renders a connected step timeline - every canary step in declaration order, with the current step highlighted and, for an analysis step, its live AnalysisRun status shown inline alongside a clickable link to the AnalysisTemplate/ClusterAnalysisTemplate it references. A blueGreen Rollout gets an equivalent derived phase list (preview scaled up → pre/post-promotion analysis → awaiting promotion → active cutover), since blueGreen has no `steps[]` array of its own to render directly. **ReplicaSet progression:** A ReplicaSets section lists every revision the Rollout owns (newest first), each with its Current/Stable/Rolling-out role badge, image, replica count, and - expandable per revision - its live pods with ready/phase status. Read-only; rollback stays on the existing revision-history dialog. Built entirely from the same `useWorkloadRevisions`/`useWorkloadPods` endpoints already used elsewhere for workload rollback, joined client-side by each pod's own pod-template-hash label - no new backend endpoint needed for this part. **AnalysisRun history:** Beyond the four "currently active" analysis slots (step / background / pre-promotion / post-promotion), an AnalysisRun History section lists the Rollout's full AnalysisRun history - phase, trigger, and metrics passing/total - via a dedicated `GET /api/rollouts/{ns}/{name}/analysisruns` endpoint gated on listing AnalysisRuns directly (a separate RBAC grant from the Rollout's own `patch` capability). **AnalysisTemplate / ClusterAnalysisTemplate / Experiment:** Full detail renderers - metric definitions (provider, interval, count, success/failure conditions) for the templates; templates, duration, and per-template replica/analysis status for Experiment. Reached both directly and via the step-timeline/Analysis-section links above. **Why it's stuck:** `InconclusiveAnalysisRun` names nothing on its own, so Radar resolves the AnalysisRun the controller recorded and surfaces the deciding metric - its success/failure condition, latest measured value, and message. The same verdict reaches AI agents through the Rollout's `issue` field. **Topology:** Rollout → active AnalysisRun (`uses`), labelled by trigger (step / background / pre-promotion / post-promotion). Only the runs the Rollout's status points at are graphed - historical runs would grow the graph without bound. (The full AnalysisRun history above is a detail-page list, not a topology change - the graph's "only active runs" bound is unchanged.) **Timeline:** Step index, traffic weights, pause conditions, abort/promote-full, and stable-ReplicaSet moves are all recorded as distinct events; a Rollout sits in `Progressing` for the whole canary, so phase alone would show nothing. # Argo Workflows visibility in Kubernetes Source: https://radarhq.io/docs/features/integrations/argo-workflows Inspect Argo Workflow runs, templates, CronWorkflows, DAG tasks, steps, retries, inputs, failures, and retained history in Radar. [Argo Workflows](https://argoproj.github.io/workflows/) is a container-native workflow engine for orchestrating parallel jobs on Kubernetes. Radar represents the stable workflow definition separately from its retained Workflow runs, then turns each selected run's controller graph into a readable execution hierarchy. Use the [Jobs and workflows guide](/docs/features/jobs-and-workflows) to learn how to select runs, compare captured configuration with the current definition, follow DAG tasks and Steps, inspect inputs, and diagnose failures. Radar understands Workflow, CronWorkflow, WorkflowTemplate, and ClusterWorkflowTemplate resources. It also follows template references attached to individual tasks and links them back to the referenced definition when the resource is visible. | CRD | Group | Topology | Detail View | AI Summary | | - | - | - | - | - | | Workflow | `argoproj.io/v1alpha1` | Yes | Yes | - | | WorkflowTemplate | `argoproj.io/v1alpha1` | Yes | Yes | - | | ClusterWorkflowTemplate | `argoproj.io/v1alpha1` | Yes | Yes | - | | CronWorkflow | `argoproj.io/v1alpha1` | Yes | Yes | - | > [!NOTE] > > Radar reads Workflow and template CRDs from the Kubernetes API using the current user's permissions. ClusterWorkflowTemplate names and metadata are not included when the user cannot list that cluster-scoped resource. # cert-manager TLS certificate visibility Source: https://radarhq.io/docs/features/integrations/cert-manager Trace cert-manager in Radar: Certificate-to-Issuer topology and the full provisioning chain - CertificateRequest, Order, Challenge - with renewal status at a glance. [cert-manager](https://cert-manager.io/) automates TLS certificate management - issuing, renewing, and revoking certificates from Let's Encrypt, Vault, Venafi, and other issuers. ### What Radar Shows **Topology:** Certificate → Issuer/ClusterIssuer edges connect each certificate to its observed built-in issuer, including ownerless issuers. Omitted `issuerRef.kind` and `issuerRef.group` use cert-manager defaults. Only referenced issuers are added; external issuer groups are not inferred. ClusterIssuer visibility uses its exact cluster-scoped read permission. Resource details and MCP context distinguish issuer dependencies from autoscaling. The full provisioning chain (Certificate → CertificateRequest → Order → Challenge) is connected via owner references. ![cert-manager Topology](https://mintcdn.com/skyhook/WMkyX9-pHguEy9dC/images/oss/certmanager-topology.png?fit=max\&auto=format\&n=WMkyX9-pHguEy9dC\&q=85\&s=1be1fefb83971b72342d904affecf3b7) cert-manager in Topology View - Certificate → CertificateRequest provisioning chain **Certificate Detail View:** * Status conditions (Ready) with color-coded expiry warnings * Validity period with progress bar (green → yellow → red as expiry approaches) * Subject, DNS names, issuer reference * Renewal time and last failure **Dashboard:** Certificate health card showing healthy/warning/critical/expired certificate counts across all namespaces. **TLS Secret Parsing:** Click any TLS Secret to see the X.509 certificate details - subject, issuer, validity dates, SANs - parsed directly from the secret data. ![Certificate Detail](https://mintcdn.com/skyhook/WMkyX9-pHguEy9dC/images/oss/certmanager-certificate-detail.png?fit=max\&auto=format\&n=WMkyX9-pHguEy9dC\&q=85\&s=ab3441844b2d6507df7f28c3d446e337) Certificate Detail View - Validity progress bar, DNS names, issuer reference, and status conditions ![Certificate List](https://mintcdn.com/skyhook/WMkyX9-pHguEy9dC/images/oss/certmanager-certificates-list.png?fit=max\&auto=format\&n=WMkyX9-pHguEy9dC\&q=85\&s=0f7df0d1d3f2dcb3e708e09054fa5669) Certificate Resource Browser - Ready status, domains, issuer, and expiry date at a glance ### Supported CRDs | CRD | Group | Topology | Detail View | AI Summary | | - | - | - | - | - | | Certificate | `cert-manager.io/v1` | Yes | Yes | - | | CertificateRequest | `cert-manager.io/v1` | Yes | Yes | - | | Issuer | `cert-manager.io/v1` | Yes | Yes | - | | ClusterIssuer | `cert-manager.io/v1` | Yes | Yes | - | | Order | `acme.cert-manager.io/v1` | Yes | Yes | - | | Challenge | `acme.cert-manager.io/v1` | Yes | Yes | - | # CloudNativePG PostgreSQL cluster visibility Source: https://radarhq.io/docs/features/integrations/cloudnative-pg Monitor CloudNativePG in Radar: PostgreSQL cluster phase, primary and replica node distribution, backup status, storage config, and failover health in one detail view. [CloudNativePG](https://cloudnative-pg.io/) (CNPG) is the Kubernetes operator for PostgreSQL, covering the full lifecycle from bootstrapping to monitoring, with high availability, automated failover, and backup management. Beyond the per-kind views below, CloudNativePG gets its own views under Resources (`/cnpg`): the fleet, backups and recovery evidence, declarations, pooling and the operator. Each Cluster has a page organized by task (what needs attention, replication, storage, performance, backups) and guarded actions such as switchover, fencing, backup setup and restore, each followed until Radar observes the outcome. Radar does not provision object storage or credentials. Design detail is in [cnpg.md](https://github.com/skyhook-io/radar/blob/main/docs/cnpg.md). **Fleet:** every Cluster ordered by urgency, each with its top problem. ![CloudNativePG Clusters view: three PostgreSQL clusters, two needing attention, each with its top problem](https://mintcdn.com/skyhook/dfXlS0BPTX1npIV3/images/oss/cnpg-fleet.png?fit=max\&auto=format\&n=dfXlS0BPTX1npIV3\&q=85\&s=a9a55b0a28bd380455c2d722c6f45ae9) **Cluster page:** the cause first, then one line per health dimension. ![A Cluster stuck because its first instance can't be scheduled: the scheduler's reason first, then Serving, Replication, Storage and Backups at a glance](https://mintcdn.com/skyhook/dfXlS0BPTX1npIV3/images/oss/cnpg-cluster-overview.png?fit=max\&auto=format\&n=dfXlS0BPTX1npIV3\&q=85\&s=6b56eeaccc0d968641b4aa3cd21b2f28) ### What Radar Shows **Cluster Detail View:** * Phase, instances ready/desired, primary instance, image version * Instance node distribution (which K8s nodes run each PostgreSQL instance) * Storage configuration: data size, storage class, WAL storage * Backup configuration: destination, retention policy, last successful backup, recovery point * Monitoring: PodMonitor integration, custom query ConfigMaps * Replication settings (for replica clusters), with per-instance role and timeline ID * PostgreSQL parameters * Health detection (AlertBanner for degraded clusters, failover/switchover in progress) * **WAL archiving failure** - a first-class AlertBanner driven by the `ContinuousArchiving` condition. This is the classic silent CNPG incident: the cluster keeps serving traffic normally while the recovery point stops advancing. The condition proves the last archive attempt failed, so Radar reports "archiving failing since \