Files
harmony/docs/guides/operator-dashboard-sso.md
Jean-Gabriel Gill-Couture f316bd629b
All checks were successful
Run Check Script / check (pull_request) Successful in 2m31s
feat(fleet-auth): request Zitadel project roles in-band via OIDC scope (Ch1)
Role-gate follow-up from v0.3 plan Ch1:

- `build_login_attempt` appends the `urn:zitadel:iam:org:project:roles` scope,
  so the gate no longer depends on Zitadel's out-of-band "Assert Roles on
  Authentication" checkbox (which silently broke it once). Idempotent if the
  scope is already present.
- docs/guides/operator-dashboard-sso.md step 1b + config reference: drop the
  wrong checkbox instruction, document the in-band scope.

Role extraction stays local to each crate (dashboard object-map; callout
configurable claim path) — two small, genuinely-different parsers, not a
shared crate. Lifting `require_role` to a composable layer is skipped as
YAGNI — only `fleet-admin` exists; revisit at the second role.
2026-06-05 15:25:53 -04:00

61 lines
3.2 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**.
1b. **Roles** — the dashboard requires the **`fleet-admin`** project role. Create that
role on the project and grant it to each operator user. The login flow requests
roles **in-band** via the OIDC scope `urn:zitadel:iam:org:project:roles`, so the
project's **Assert Roles on Authentication** checkbox is **not** needed. Without
the role grant, users authenticate but get **403 Access denied**.
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` (the login flow appends
`urn:zitadel:iam:org:project:roles` so roles are always asserted); 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 requires both authentication **and** the `fleet-admin` role
> (see step 1b). A logged-in user without it gets a 403 with a sign-out link.