Skip to main content
Self-hosted 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. Customer auth events stay inside your VPC.

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:
# Plaintext - hashed at boot, bcrypt cost 12, original discarded.
helm install ... \
  --set auth.breakGlass.email=admin@acme.example \
  --set auth.breakGlass.password='supersecret123'
# Pre-hashed - preferred for production. Generate the hash anywhere with bcrypt.
htpasswd -bnBC 12 "" 'supersecret123' | 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).

Signing in

POST to /api/auth/break-glass/login:
curl -i https://radar.acme.example/api/auth/break-glass/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@acme.example","password":"supersecret123"}'
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:
helm upgrade radar-hub skyhook/radar-hub --reuse-values \
  --set 'auth.breakGlass.passwordHash=<new bcrypt hash>'

kubectl -n radar-hub rollout restart deploy/radar-hub-hub
Sessions issued under the old password remain valid until cookie expiry - rotate cookies + bcrypt together if you suspect the old credential leaked.

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.
  5. (Optional) Add a Groups claim to the ID token; default name is groups.
auth:
  oidc:
    issuer: https://acme.okta.com
    clientID: 0oa<...>
    clientSecret: <from Okta>
    callbackURL: https://radar.acme.example/api/auth/oidc/callback
    groupClaim: groups
    adminGroups: [radar-admins]
    defaultRole: member

Role mapping

The control plane maps an OIDC ID-token’s group claim to one of three roles:
  • owner - full admin (mint cluster tokens, manage members, view audit log).
  • member - read + cluster operations (the default).
  • viewer - read-only.
Logic in plain English:
  1. Read the configured groupClaim value from the ID token. Leave it empty to disable group-based promotion.
  2. If any of those values is in adminGroups, the user is owner.
  3. Otherwise, the user is defaultRole (default member).
Role is computed only at first sign-in - subsequent logins keep whatever role the user currently has. This avoids accidentally yanking owner status from someone whose group membership got reorganized; promote / demote explicitly via the web app.

Session lifetime

The control plane mints a sealed session cookie scoped to its own lifetime - it does NOT silently refresh against the IdP. When the cookie expires (default 7 days), the web app shows the login page and the user re-auths through the IdP.

IdP offboarding behavior

Because the cookie is the freshness signal (not a per-request IdP probe), there is a window between an IdP-side change and the control plane catching up:
  • User removed from the IdP keeps a working session until the cookie expires (≤ 7 days) or they sign out. To force-revoke immediately: have an owner remove them from the org via Settings → Members. That cuts org-scoped access right away; the user’s stale cookie still authenticates them as a person but resolves to no org membership.
  • Group changes do NOT auto-propagate to existing users. First sign-in resolves a Radar role from adminGroups membership and writes it to org_members. Subsequent sign-ins do not re-resolve - ON CONFLICT DO NOTHING on the join row. Both directions are affected:
    • A new admin added to adminGroups after their first login stays at their original role until an owner promotes them via Settings → Members.
    • A demoted user removed from adminGroups retains their owner role in Radar until an owner adjusts it.
    For shops that need automatic group-driven role sync, write a small reconciler that calls the hub’s PATCH /api/orgs/{id}/members/{user_id} from your IdP webhook. Built-in two-way sync is on the GA roadmap.

Sign-out

The web app’s sign-out button calls POST /api/auth/signout, which clears the session cookie and (when the IdP advertises end_session_endpoint in discovery) returns a redirect URL to the IdP’s logout 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.