Files
harmony/docs/guides/operator-dashboard-sso.md
Jean-Gabriel Gill-Couture 1c0e9df682
All checks were successful
Run Check Script / check (pull_request) Successful in 2m19s
docs: trim operator SSO guide to a quickstart-first page
Was ~150 lines with the host-derivation repeated three times and
reference tables ahead of the happy path. Rewrite as a 4-step staging
quickstart (the main content), with the counterintuitive bits demoted to
short "when login fails" + "config reference" sections. ~55 lines.
2026-06-01 23:08:48 -04:00

2.8 KiB

Operator Dashboard SSO (Zitadel) — setup

Browser SSO for the fleet operator web UI (OIDC Authorization Code + PKCE, public client). Distinct from the agent/callout machine auth (fleet-zitadel-faq); the security rationale is in web-auth-security. Code: harmony_zitadel_auth/.

Quickstart (staging)

  1. Zitadel app — create a Web application, auth method PKCE (no client secret), redirect URI https://fleet-stg.<base>/auth/callback, post-logout URI https://fleet-stg.<base>/. Copy its Client ID.
  2. Seed config in OpenBao (namespace fleet-staging) — the deploy derives every host from base_domain, so you set only:
    • FleetDeployConfig.operator_oidc_client_id = the Client ID
    • FleetDeployConfig.operator_trusted_audiences = ["<Client ID>"]
    • FleetDeploySecrets.operator_cookie_key_b64 = openssl rand -base64 64
  3. Deploy: ./fleet/scripts/dev-deploy-operator.sh
  4. Open https://fleet-stg.<base>/ → Zitadel login → back to the dashboard.

fleet_staging_install already generates the cookie key, so a fresh install needs only the Client ID + audiences.

Local dev (serve-web)

fleet/harmony-fleet-operator/dev.sh sets the same config as two HARMONY_CONFIG_<TypeName> env JSON blobs (ConfigClient's env source). Point base_url at http://localhost:18080, register that callback in the app, and turn on the app's Development Mode (Zitadel rejects non-HTTPS redirects otherwise).

When login fails — check these first

  • iss mismatchzitadel_base must equal the token issuer byte-for-byte, no trailing slash.
  • aud mismatchtrusted_audiences must contain the token's aud; Zitadel puts the app's Client ID there by default.
  • Client secret — the app must be PKCE-only; the code never sends a secret.
  • Redirect URI — must be exactly {base_url}/auth/callback.
  • Cookie keycookie_key_b64 must decode to ≥64 bytes, else the dashboard refuses to start (cookie_key_b64 must decode to at least 64 bytes; reconcile keeps running).

Config reference

The operator reads ZitadelAuthConfig + OperatorCookieKey via ConfigClient. The deploy derives zitadel_base / base_url / logout_redirect_uri from base_domain (https://sso-stg.<base>, https://fleet-stg.<base>, …/) and fixes scope = openid profile email; you supply client_id, trusted_audiences, cookie_key_b64. All endpoints derive from zitadel_base: /.well-known/openid-configuration, /oauth/v2/authorize, /oauth/v2/token, /oidc/v1/end_session.

The dashboard only checks that the user authenticated — no role gate yet (web-auth-security §3, ROADMAP/09).