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

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:

  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). 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 /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, 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).

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.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), 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.