Skip to content
sign in

==> read the decisions

ADR-0019: Sign-in goes through an identity provider; every host keeps its own session

Status: Superseded by ADR-0024 (2026-10-15): Zitadel, with the platform's own sign-in and account pages at the identity host. Was: accepted and implemented 2026-10-15 (Authentik, the app as its OIDC client).

The API signs users in itself: a password checked with scrypt, an invite link to set the first one (ADR-0016). Three needs have outgrown that:

  • Stronger factors. Passkeys, hardware security keys and TOTP are wanted. Each is a protocol of its own (WebAuthn registration and assertion, attestation, authenticator models, TOTP enrolment, recovery), and each is security-critical code the platform would otherwise write and maintain.
  • More than one signed-in host. The documentation host (ADR-0022) needs to know who is reading, and sign-in should not be implemented twice.
  • Login protection. There is no rate limiting or lockout on the password check, which runs scrypt on a small CPU.

A session cookie shared across the whole domain would answer the second need by itself, but every subdomain's server would then receive every visitor's credential, and any subdomain could set or overwrite it. The session stays host-only (__Host-), and a second host gets its own.

  • Authentik is the identity provider, self-hosted, storing its data in the platform's Postgres (a database and role of its own, peer-authenticated like the others; ADR-0010). Kanidm was the lighter candidate but cannot use Postgres; Authelia keeps users in a file or LDAP and has no invitation flow; Zitadel adds an organisation model the platform does not need and a separate login service. Keycloak was ruled out for its weight.
  • It is public at id.<domain>, serving only the sign-in, enrolment and recovery flows. Its administration interface is never public (the fleet serves it on the mesh only).
  • Credentials:
    • passkeys are the default. They are bound to id.<domain> (the WebAuthn relying-party ID): no other host of the domain can even ask the browser for them. Moving the provider to another host would mean enrolling again; switching provider products means that anyway, since stored public keys do not move between products;
    • hardware security keys, as passkeys or as second factors, with an allow-list of authenticator models (by AAGUID) where the policy wants attested hardware;
    • a password with TOTP as the fallback. TOTP can be phished and a passkey cannot, so it is the fallback, not the default.
  • Onboarding and recovery are the provider's. An invitation is the provider's enrolment link and a reset is its recovery link, created by the operator and delivered out of band; single use and short lived, as before. cs-admin wraps them.
  • Every host is an OIDC client with its own session. app. (and later docs.) sends a visitor without a session to the provider (authorization code with PKCE). The API exchanges the code server-to-server, maps the identity to the cs.users row by username, and sets its own __Host-cs_session, host-only, as today. The browser never holds an OAuth token. A second host signs in through the provider's own session without a prompt.
  • Sessions end everywhere at once. The provider's back-channel logout (OpenID-certified in the chosen version) notifies each host with a signed logout token when a session ends there; the API validates it (signature, audience, expiry) and ends the matching sessions. Signing out of a host ends that host's session and, through the provider's logout endpoint, the provider's.
  • The roster still decides who exists. The fleet provisions the provider's users and clients from the inventory; a user dropped from the roster is deactivated in the provider and in the control database, which ends their sessions, tokens and invitations (ADR-0016).
  • What stays: the sessions list and per-session sign-out, the homepage's signed-in hint (a username, not a credential), personal access tokens (ADR-0020), and the rule that changing a password does not end other sessions.
  • Passkeys, hardware keys, TOTP, rate limiting and lockout come from a maintained, certified implementation instead of the platform's own code.
  • The platform's password check, invite table and scrypt code go once the move is done; existing users enrol their new credentials through a recovery link once.
  • The provider is a large internet-facing application on the hub: it is patched promptly, confined like the platform's own units, and only its user-facing flows are public.
  • Sign-in has one more hop; a signed-in visitor opening a second host sees a redirect, not a prompt.
  • A session cookie for the whole domain. Every subdomain's server would receive every visitor's session, and any subdomain could plant one; __Host- exists to rule out both.
  • A hand-off between hosts, written by the platform. It is a private OIDC: one-time codes, redirect allow-lists, state, linked sign-out. The provider implements the standard version, and tests it.
  • Building the identity provider into the API. One user database and full control of the flows, but the platform would own WebAuthn, attestation and TOTP code for the life of the project.