All checks were successful
Run Check Script / check (pull_request) Successful in 2m19s
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.
56 lines
2.8 KiB
Markdown
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)).
|