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]>
118 lines
7.0 KiB
Markdown
118 lines
7.0 KiB
Markdown
# 15 — Identity & Access (SSO)
|
|
|
|
Serves: **R-IDP-01..06**, supports **R-C2-02**, **R-SEC-02/03**. Related:
|
|
[ADR-0014 (Keycloak)](../decisions/adr-0014-idp-keycloak.md).
|
|
|
|
## 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](../decisions/adr-0014-idp-keycloak.md)): 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:
|
|
|
|
1. **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**.
|
|
2. **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](../decisions/adr-0022-forward-auth-per-app-oauth2-proxy-sidecars.md)).
|
|
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](16-known-gaps.md)). Prometheus, Alertmanager and Hubble UI are not exposed; any future
|
|
exposure MUST be gated.
|
|
|
|
> This resolves the C2 auth open question ([12](12-command-center-ui.md)): 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 `/runbooks` carry 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](13-service-catalog.md), `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 `kubectl` and 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](07-security-and-secrets.md)).
|
|
|
|
## 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](04-storage-and-state.md)).
|
|
Keycloak is stateless itself and rebuilds from the DB + git-managed realm config.
|
|
- **Exposure**: `keycloak.home.keithjackson.dev` is **`public`** so 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](08-tls-and-certificates.md)),
|
|
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](03-networking-isolation-and-egress.md));
|
|
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](09-dns-and-ingress.md)) 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](13-service-catalog.md)) 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.
|