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.
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)
- Zitadel app — create a Web application, auth method PKCE (no client
secret), redirect URI
https://fleet-stg.<base>/auth/callback, post-logout URIhttps://fleet-stg.<base>/. Copy its Client ID. - Seed config in OpenBao (namespace
fleet-staging) — the deploy derives every host frombase_domain, so you set only:FleetDeployConfig.operator_oidc_client_id= the Client IDFleetDeployConfig.operator_trusted_audiences=["<Client ID>"]FleetDeploySecrets.operator_cookie_key_b64=openssl rand -base64 64
- Deploy:
./fleet/scripts/dev-deploy-operator.sh - 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
issmismatch —zitadel_basemust equal the token issuer byte-for-byte, no trailing slash.audmismatch —trusted_audiencesmust contain the token'saud; 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_b64must 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).