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

56 lines
2.8 KiB
Markdown

# 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](./fleet-zitadel-faq.md)); the security rationale is in
[web-auth-security](./web-auth-security.md). 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` mismatch** — `zitadel_base` must equal the token issuer byte-for-byte, no
trailing slash.
- **`aud` mismatch** — `trusted_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 key** — `cookie_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](./web-auth-security.md) §3,
> [ROADMAP/09](../../ROADMAP/09-sso-config-hardening.md)).