Retire Kasm from the catalog. Drop ACME revocation from R-TLS-02. Make L3 fault injection operator-run and exclude it from the merge gate (R-TEST-03, ADR-0026). Update the gap register to match; Homepage authentication is now ruled must-enforce and stays a gap until the implementation lands. Co-Authored-By: Claude Opus 5.5 <[email protected]>
7.0 KiB
15 — Identity & Access (SSO)
Serves: R-IDP-01..06, supports R-C2-02, R-SEC-02/03. Related: ADR-0014 (Keycloak).
Purpose
One place to log in. A central Identity Provider (IdP) gives the platform single sign-on (SSO), one user directory, group-based authorization, and MFA — so every service is protected by the same identity, and no management UI sits unauthenticated on the network.
Choice: Keycloak
Keycloak is the platform IdP (ADR-0014): a mature, self-hostable OpenID Connect (OIDC) + SAML provider with users, groups, roles, clients, MFA (TOTP/WebAuthn), and brute-force protection.
Two integration patterns (behind one identity)
Every service authenticates against Keycloak, via whichever pattern it supports:
- Native OIDC — the app is configured as a Keycloak OIDC client and does its own login redirect. Preferred where available. Initial set: Gitea, Nextcloud, Grafana, Semaphore, Headlamp.
- Forward-auth gate — for apps with no usable OIDC, a per-app identity-aware proxy (oauth2-proxy) runs as a sidecar in the app's pod, authenticates the user against Keycloak, and only then forwards the request (ADR-0022). Used for: *PiHole admin, the arr web UIs (Sonarr/Radarr/Jackett), qBittorrent UI, the Crafty web UI, and any other bare UI; Homepage requires the same (see known gaps). Prometheus, Alertmanager and Hubble UI are not exposed; any future exposure MUST be gated.
This resolves the C2 auth open question (12): it is both — native OIDC where supported, forward-auth everywhere else, all backed by Keycloak.
Each gate is an independent instance with its own IdP client and credentials, and enforces the
service's auth.groups. Session cookies are per-gate: a unique name, scoped to the app's own host,
because instances that share a cookie evict one another's sessions. A service MAY declare its API
paths so that unauthenticated programmatic calls are refused rather than redirected to a login page.
Exceptions (documented, not oversights)
- Plex and Seerr use Plex's own account system (Plex OAuth) — they do not federate to Keycloak.
- Minecraft players authenticate via Mojang/Microsoft — N/A. The Crafty web UI that manages it is behind a forward-auth gate and then presents its own local login (no OIDC); the second login is intended.
- UniFi keeps its native account model and login on its own L4 VIP (limited SSO support).
- Runbooks served read-only under
/runbookscarry no login. - Keycloak itself is
native: it is the root of trust.
Authorization model (R-IDP-06)
- Groups drive access. Keycloak groups (e.g.
platform-admins,media-users,viewers) map to per-service access and roles. A service's ServiceDefinition declares which groups may reach it (13,auth.groups). - Least privilege: the C2 console, Headlamp, Alertmanager, and other sensitive UIs require an authenticated user in an authorized group — not merely "logged in."
- MFA: TOTP/WebAuthn available for all users and required for privileged groups
(
platform-admins). Passkeys (WebAuthn passwordless) are an optional additive login path, bound into the login flow by an explicit switch in git. - Kubernetes API: humans authenticate with Keycloak OIDC, and cluster roles are bound to Keycloak
groups, so
kubectland the Kubernetes console share this model.
Identity config as code (R-IDP-03)
Realms, clients, groups, and roles are declarative and GitOps-managed — not clicked into the admin console and forgotten:
- The Keycloak instance is deployed via the Keycloak Operator, and a realm-import CR seeds realm/client/group configuration from git. A realm import is one-shot and fails open, so reconcilers continuously converge what git declares after the first import: client secrets, client URLs, the WebAuthn policy and the login flow.
- OIDC client secrets and the Keycloak admin credential are SOPS-encrypted (07).
State, exposure & resilience
- Datastore: Keycloak runs on PostgreSQL via CloudNativePG (ADR-0019); the DB lives on 3-replica Longhorn and is DR-critical → backed up off-cluster (04). Keycloak is stateless itself and rebuilds from the DB + git-managed realm config.
- Exposure:
keycloak.home.keithjackson.devispublicso it can serve login/redirect flows for the internet-exposed services (Nextcloud, Gitea) — but it is security-sensitive and hardened: brute-force detection on, strong admin auth + MFA, trusted TLS (08), minimal exposed endpoints. - HA: Keycloak runs multiple replicas against a database with one instance per node and a hot standby to promote, because a single node must not be able to take SSO down. Availability caveat: SSO is a dependency for logging into federated services and the Kubernetes API, so a break-glass path exists: local admin logins on Gitea, Nextcloud and Grafana, the Keycloak bootstrap admin, and a certificate kubeconfig for the cluster.
Interaction with default-deny & the gateway
- Keycloak and oauth2-proxy are ordinary workloads under default-deny (03); the specific flows they need (app → Keycloak OIDC, gate → Keycloak, Keycloak → its DB) are explicit allow rules.
- Forward-auth is enforced in the data path: the Gateway (09) routes only to the gate's port and network policy closes the app's own port to it, so protection happens before traffic reaches the app and a failed gate fails closed.
Bootstrapping & order
- Keycloak is foundational and onboarded early (with PiHole/Gitea, 13) so later services can integrate SSO as they come up.
- No hard circular dependency at bring-up: Gitea can start with a local admin and add OIDC once Keycloak is live; the C2 and sensitive UIs gain SSO as soon as their gate + the realm exist.
Acceptance gates
- A user logs in once at Keycloak and reaches all OIDC-integrated services without re-entering credentials (SSO proven).
- A forward-auth-protected UI (e.g. PiHole admin) is unreachable without a valid Keycloak session and reachable with one, restricted to the allowed group.
- Realm/client/group config applied purely from git reproduces a working SSO setup (no console clicking); destroying and rebuilding Keycloak from DB + git restores identity.
- Privileged access (C2/Headlamp) requires MFA and group membership; a non-member is denied.
- Client secrets and admin credentials exist only SOPS-encrypted.
Resolved
- Declarative-config tool — operator realm-import CRs, plus the reconcilers above.
- Keycloak exposure —
public, hardened.