Files
home-network-k3s/docs/15-identity-and-access.md
T
jakman85andClaude Opus 5.5 250fb54154 docs: bring the specification in line with the accepted implementation
The operator ruled the implementation repo's main (50c8719) the latest
and accepted state. Sync the spec to it.

Accepted changes are reflected: one bare-metal environment, TrueNAS bulk
storage, R2 off-cluster backup, CloudNativePG, single-node VPN gateway
with Cilium fail-closed, inbound WireGuard, per-app auth sidecars,
Renovate, the CI gate as approver, and the pm-agent. New ADR-0017..0025;
ADR-0016 corrected and accepted; R-* IDs unchanged, eight amended.

Where the implementation has simply not met a requirement, the
requirement stands and the shortfall is recorded in docs/16-known-gaps.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 22:46:00 -06:00

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** (Kasm, not onboarded, joins this set when it is).
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.