Skip to main content
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, domains and IdP groups describe the hosted service and do not apply here; Sign-in and access 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:
Or via an existing Secret (see Configuration → Skipping the chart’s Secret).

Signing in

POST to /api/auth/break-glass/login:
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:
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

  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).

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 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:<group> subject, so a RoleBinding you already maintain keeps working instead of being re-authored per user:
See IdP group-based access 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:
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: 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.
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, 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.
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, 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.
  • 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 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 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)
The user then sees the login page and signs in again. Both limits are chart values:
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:
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:
  • 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.

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.