Files
harmony/docs/guides/operator-dashboard-sso.md

53 lines
2.7 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/`.
## Deployment
`FleetApp` declares a dedicated dashboard Web PKCE application with the
dashboard callback and logout URLs. The operator's Device Code application is
separate. `ZitadelSetupScore` reconciles both and publishes the dashboard client
ID. `FleetOperatorScore` builds `ZitadelAuthConfig` from that output and the
dashboard Ingress, then generates and retains the session cookie key in the
operator Secret. No client ID or cookie key is entered by hand.
The dashboard requires the exact Zitadel project role `fleet-admin`. Create the
role on the fleet project and grant it to each operator. Login automatically
requests `urn:zitadel:iam:org:project:roles`, so the project-level **Assert Roles
on Authentication** setting is not required. Users without the role receive a
403 response that retains the session cookie and includes a sign-out link.
## 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).
- **403 after login** — confirm the user has the exact `fleet-admin` project role
and that the aggregate roles claim is present in the ID token.
## Config reference
The operator reads `ZitadelAuthConfig` and `OperatorCookieKey` through
ConfigClient. The deploy derives `zitadel_base`, `base_url`, client ID, trusted
audience, logout URI, and `scope = openid profile email`. The login flow adds the
aggregate roles scope once if it is absent. All endpoints derive from
`zitadel_base`:
`/.well-known/openid-configuration`, `/oauth/v2/authorize`, `/oauth/v2/token`,
`/oidc/v1/end_session`.