Merge feature/idp-keycloak into main
Add Keycloak IdP/SSO (ADR-0014, docs/15, R-IDP-*); resolves the C2 auth open question. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
14 files changed
+243
-14
No files matched your search
+14
@@ -58,6 +58,20 @@ two things (e.g. "VM"), this file disambiguates.
|
||||
- **cert-manager / ACME DNS-01 / Let's Encrypt** — the trusted, automated TLS pipeline (R-TLS-*).
|
||||
- **ExternalDNS** — controller that publishes DNS records from declared service intent.
|
||||
|
||||
## Identity & access
|
||||
- **IdP (Identity Provider)** — the central authority for user identity and login. Here: **Keycloak**
|
||||
([`docs/15-identity-and-access.md`](docs/15-identity-and-access.md)).
|
||||
- **SSO (Single Sign-On)** — log in once, reach every integrated service without re-authenticating.
|
||||
- **OIDC (OpenID Connect)** — the token-based auth protocol services use to federate to Keycloak
|
||||
natively (a Keycloak **client**).
|
||||
- **Forward-auth** — an identity-aware proxy (**oauth2-proxy**) at the Gateway that authenticates a
|
||||
request against Keycloak before forwarding it — used for apps without native OIDC.
|
||||
- **Realm** — a Keycloak tenant/namespace holding the platform's users, groups, and clients.
|
||||
- **Group** — a Keycloak grouping that drives per-service authorization (`auth.groups` in a
|
||||
ServiceDefinition); the basis for least-privilege access (R-IDP-06).
|
||||
- **Break-glass** — a documented emergency access path (e.g. a local admin on Gitea) so an IdP outage
|
||||
doesn't lock the operator out of recovery tools.
|
||||
|
||||
## Environments
|
||||
- **dev/test** — local KVM cluster; the active build target where everything is proven.
|
||||
- **prod** — 3× mini-PC bare-metal cluster; design-only until roadmap Phase 7.
|
||||
|
||||
@@ -78,7 +78,8 @@ home-network-k3s/
|
||||
│ ├── 11-automated-updates.md
|
||||
│ ├── 12-command-center-ui.md
|
||||
│ ├── 13-service-catalog.md
|
||||
│ └── 14-testing-and-conformance.md
|
||||
│ ├── 14-testing-and-conformance.md
|
||||
│ └── 15-identity-and-access.md
|
||||
├── decisions/ ← ADRs (one decision per file) + index/template
|
||||
├── roadmap/ ← phases.md, workstreams.md
|
||||
└── agents/ ← roles, coordination protocol, DoD, task board
|
||||
|
||||
@@ -78,6 +78,8 @@ Seeded from [`../roadmap/phases.md`](../roadmap/phases.md) + [`../roadmap/workst
|
||||
| T-6.1 | Onboard **pihole** (DNS underpinning) | WS-APPS | R-DNS-03 | T-2.5 | — | todo |
|
||||
| T-6.1a | Onboard **gitea** early (bootstrapping-critical; self-hosts GitOps source + CI/CD) | WS-APPS | R-STATE-03 | T-4.4 | — | todo |
|
||||
| T-6.1b | Migrate GitOps source local→Gitea; move CI to **Gitea Actions** (Kyverno gate + Chainsaw Job trigger; PR merge gate); off-cluster repo mirror | WS-GITOPS | R-GITOPS-01, R-TEST-03 | T-6.1a | — | todo |
|
||||
| T-6.1c | Onboard **Keycloak** (IdP) + PostgreSQL (Longhorn, DR-critical) + **oauth2-proxy** forward-auth at Gateway; declarative realm/clients/groups; MFA; break-glass path | WS-SEC | R-IDP-01..06 | T-2.5, T-2.6 | — | todo |
|
||||
| T-6.1d | Render ServiceDefinition `auth` block → Keycloak OIDC client / oauth2-proxy protection | WS-SEC/WS-GITOPS | R-IDP-02/03 | T-0.3, T-6.1c | — | todo |
|
||||
| T-6.2 | Onboard **plex** (bulk tier + public exposure) | WS-APPS | R-STATE-04 | T-2.6, T-6.1 | — | todo |
|
||||
| T-6.3 | Onboard **qbittorrent, jackett** (VPN + kill-switch) | WS-APPS | R-NET-05/06 | T-3.4 | — | todo |
|
||||
| T-6.4 | Onboard **sonarr, radarr** (VPN + arr↔downloader allows) | WS-APPS | R-NET-02/05 | T-6.3 | — | todo |
|
||||
|
||||
@@ -55,3 +55,4 @@ serves. **Do not edit an accepted decision in place** — supersede it with a ne
|
||||
| [0011](adr-0011-test-tooling-kyverno-chainsaw.md) | Conformance test tooling: Kyverno + Chainsaw | Accepted | R-TEST-01..05 |
|
||||
| [0012](adr-0012-vpn-egress-single-expressvpn-gateway.md) | VPN egress: single shared in-cluster ExpressVPN gateway | ~~Superseded by 0013~~ | R-NET-05/06/07 |
|
||||
| [0013](adr-0013-vpn-provider-pia-gluetun-wireguard.md) | VPN provider: PIA via Gluetun (WireGuard + port forwarding) | Accepted | R-NET-05/06/07 |
|
||||
| [0014](adr-0014-idp-keycloak.md) | Identity Provider: Keycloak (SSO via OIDC + forward-auth) | Accepted | R-IDP-01..06 |
|
||||
@@ -0,0 +1,45 @@
|
||||
# ADR-0014 — Identity Provider: Keycloak (SSO via OIDC + forward-auth)
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-15
|
||||
- Serves: R-IDP-01, R-IDP-02, R-IDP-03, R-IDP-04, R-IDP-05, R-IDP-06
|
||||
- Supersedes: —
|
||||
|
||||
## Context
|
||||
The platform needs one identity and single sign-on: a central user directory, group-based
|
||||
authorization, MFA, and a way to protect both OIDC-capable apps and bare management UIs — without
|
||||
per-app passwords scattered across services. This also resolves the previously-open C2 auth question
|
||||
([12](../docs/12-command-center-ui.md)).
|
||||
|
||||
## Decision
|
||||
Use **Keycloak** as the platform IdP, integrated two ways behind one identity:
|
||||
- **Native OIDC** for apps that support it (Gitea, Nextcloud, Grafana, Kasm, Semaphore, Headlamp).
|
||||
- **Forward-auth proxy (oauth2-proxy → Keycloak)** at the Gateway for apps without usable OIDC
|
||||
(PiHole, *arr UIs, qBittorrent, Homepage, Prometheus, Alertmanager, Hubble UI).
|
||||
|
||||
Supporting decisions:
|
||||
- **Config as code**: realms/clients/groups are declarative and GitOps-reconciled (Keycloak Operator/
|
||||
Helm for the instance; `keycloak-config-cli` or realm-import for config). Secrets SOPS-encrypted.
|
||||
- **Authorization by groups**; **MFA** required for privileged groups.
|
||||
- **PostgreSQL on Longhorn**, DR-critical, backed up off-cluster; Keycloak rebuilds from DB + git.
|
||||
- **`public` exposure** (hardened) so login flows work for internet-exposed services.
|
||||
- **Exceptions**: Plex/Seerr use Plex auth; Minecraft uses Mojang/Microsoft; UniFi keeps native auth.
|
||||
|
||||
## Alternatives considered
|
||||
- **Authelia / Authentik** — lighter, and Authelia pairs cleanly with forward-auth; but Keycloak is
|
||||
the operator's explicit choice and offers the broadest OIDC/SAML, realm, and federation feature set
|
||||
for heterogeneous apps. (Chosen.)
|
||||
- **Per-app local auth only** — rejected: no SSO, password sprawl, weak central policy/MFA (fails
|
||||
R-IDP-01/06).
|
||||
- **Cloud IdP (e.g. hosted OIDC)** — rejected: this is a self-hosted, offline-capable home platform;
|
||||
identity should not depend on an external SaaS.
|
||||
|
||||
## Consequences
|
||||
- One login, one directory, central MFA and group policy; management UIs are never unauthenticated on
|
||||
the network (R-IDP-02).
|
||||
- **Keycloak becomes a critical dependency**: an IdP outage impairs SSO logins → DR-critical storage
|
||||
+ a documented **break-glass** local-admin path (e.g. on Gitea) so recovery tools stay reachable.
|
||||
- New platform components to run and secure: Keycloak, PostgreSQL, oauth2-proxy — all under
|
||||
default-deny with explicit allows and trusted TLS.
|
||||
- Onboarded **early** (with PiHole/Gitea) so later services integrate SSO as they come up.
|
||||
- Adds an `auth` block to the ServiceDefinition ([13](../docs/13-service-catalog.md)) so SSO mode +
|
||||
allowed groups are declarative per service.
|
||||
@@ -138,6 +138,23 @@ mandatory; `SHOULD` is strongly preferred and requires an ADR to deviate.
|
||||
- **R-SEC-03** — All the above properties MUST be demonstrable/auditable (an operator can prove
|
||||
isolation, egress path, cert validity, and backup integrity).
|
||||
|
||||
### Identity & access (SSO) — `R-IDP-*`
|
||||
- **R-IDP-01** — The platform MUST provide a central Identity Provider (SSO) as the single user
|
||||
directory and login for platform services.
|
||||
- **R-IDP-02** — Services MUST authenticate against the IdP — natively via OIDC where supported, or
|
||||
behind an identity-aware (forward-auth) proxy otherwise. No management UI may be exposed
|
||||
unauthenticated on the network. (Documented exceptions: Plex/Seerr use Plex auth; Minecraft uses
|
||||
Mojang/Microsoft.)
|
||||
- **R-IDP-03** — Identity configuration (realms, clients, groups, roles) MUST be declarative and
|
||||
GitOps-managed, not console-only.
|
||||
- **R-IDP-04** — IdP secrets (admin credential, OIDC client secrets) MUST be SOPS-encrypted; MFA MUST
|
||||
be available to all users and required for privileged access.
|
||||
- **R-IDP-05** — The IdP MUST be treated as DR-critical (replicated + off-cluster-backed datastore)
|
||||
with a documented **break-glass** path so an IdP outage does not lock the operator out of recovery
|
||||
tools.
|
||||
- **R-IDP-06** — Access MUST be least-privilege via groups/roles; sensitive surfaces (C2, cluster
|
||||
console, alerting) require an authenticated user in an authorized group, not merely "logged in."
|
||||
|
||||
### Environments — `R-ENV-*`
|
||||
- **R-ENV-01** — dev/test MUST be provisioned as local KVMs via IaC: created, baselined, cluster-
|
||||
joined, and managed as code.
|
||||
|
||||
@@ -22,7 +22,8 @@ choice here without a superseding ADR.**
|
||||
| Observability | **kube-prometheus-stack** + **Loki** + **Grafana Alloy** + Alertmanager→SMTP | R-OBS-01..04 | — | [10](10-observability-and-alerting.md) |
|
||||
| Backup/DR | **Velero** + Longhorn backups → off-cluster S3/NFS | R-STATE-03 | [ADR-0003](../decisions/adr-0003-storage-longhorn.md) | [04](04-storage-and-state.md) |
|
||||
| Node/infra provisioning | **OpenTofu** (libvirt) + **cloud-init** + **Ansible** (k3s bootstrap) | R-ENV-01..03 | [ADR-0001](../decisions/adr-0001-kubernetes-distribution-k3s.md) | [02](02-environments-and-provisioning.md) |
|
||||
| Command center UI | **Homepage** + **Headlamp** + **Grafana** panels | R-C2-01/02 | — | [12](12-command-center-ui.md) |
|
||||
| Identity / SSO | **Keycloak** (OIDC + SAML) + **oauth2-proxy** forward-auth | R-IDP-01..06 | [ADR-0014](../decisions/adr-0014-idp-keycloak.md) | [15](15-identity-and-access.md) |
|
||||
| Command center UI | **Homepage** + **Headlamp** + **Grafana** panels (behind Keycloak SSO) | R-C2-01/02 | — | [12](12-command-center-ui.md) |
|
||||
| Conformance testing | **Kyverno** (L0 policy) + **Chainsaw** (L1/L2/L3 behavior + fault-injection) | R-TEST-01..06 | [ADR-0010](../decisions/adr-0010-conformance-testing.md), [ADR-0011](../decisions/adr-0011-test-tooling-kyverno-chainsaw.md) | [14](14-testing-and-conformance.md) |
|
||||
|
||||
## Layer cake
|
||||
@@ -38,6 +39,7 @@ choice here without a superseding ADR.**
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
│ Platform services │
|
||||
│ Flux (GitOps) · cert-manager (TLS) · ExternalDNS · Gateway API │
|
||||
│ Keycloak (SSO/IdP) + oauth2-proxy (forward-auth) │
|
||||
│ kube-prometheus-stack + Loki + Alloy (observability) │
|
||||
│ Velero (backup) · Renovate (chart updates) · SOPS (secrets) │
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
|
||||
@@ -16,6 +16,11 @@ Defense in depth with declarative, auditable controls:
|
||||
- **TLS**: trusted, auto-managed certificates end to end ([08](08-tls-and-certificates.md), R-TLS-*).
|
||||
- **Supply chain**: pin images by digest where automation allows; prefer known-good upstreams;
|
||||
updates flow through policy, not ad-hoc pulls ([11](11-automated-updates.md)).
|
||||
- **Identity / SSO**: a central IdP (**Keycloak**) provides single sign-on, one user directory,
|
||||
group-based authorization, and MFA; services authenticate via native OIDC or an oauth2-proxy
|
||||
forward-auth layer so no management UI is unauthenticated ([15](15-identity-and-access.md),
|
||||
[ADR-0014](../decisions/adr-0014-idp-keycloak.md), R-IDP-*). This is the primary *authentication*
|
||||
control, complementing network default-deny (the primary *isolation* control).
|
||||
- **Policy enforcement (admission)**: **Kyverno** runs in-cluster as an admission controller and
|
||||
rejects non-conforming resources at reconcile time — e.g. a namespace missing its default-deny
|
||||
baseline, a plaintext Secret, a workload without a required egress-class label. The *same* policies
|
||||
@@ -37,6 +42,7 @@ Defense in depth with declarative, auditable controls:
|
||||
|
||||
### Secret categories to manage
|
||||
- VPN credentials (PIA username/password; WireGuard) for the egress model (ADR-0013).
|
||||
- Keycloak admin credential and per-service **OIDC client secrets** ([15](15-identity-and-access.md)).
|
||||
- DNS provider API token (Cloudflare) for ACME DNS-01 + ExternalDNS.
|
||||
- SMTP credentials for Alertmanager email.
|
||||
- Service credentials (Nextcloud, Gitea admin, UniFi, Semaphore, Kasm, DB passwords).
|
||||
|
||||
@@ -27,6 +27,8 @@ Platform health (R-OBS-04) — first-class alerts:
|
||||
NetworkPolicy reconcile errors.
|
||||
- **Backups (Velero/Longhorn)**: backup job failure, missed schedule, restore-test failures.
|
||||
- **KubeVirt**: VM crash-loops / failed launches.
|
||||
- **Identity (Keycloak)**: IdP down/unhealthy or its DB unavailable (SSO is a critical dependency —
|
||||
[15](15-identity-and-access.md)); certificate/login-endpoint failures.
|
||||
|
||||
## Alert delivery (R-OBS-02)
|
||||
|
||||
|
||||
@@ -49,8 +49,10 @@ kubeconfig in a browser:
|
||||
|
||||
- The C2 root and its sub-UIs are `internal` exposure by default (LAN-only) unless the operator
|
||||
explicitly makes a component `public`.
|
||||
- Authentication in front of the C2 (and sensitive sub-UIs like Headlamp) — SSO/reverse-proxy auth —
|
||||
is required; the mechanism (e.g. an identity-aware proxy) is pinned during the C2 workstream.
|
||||
- Authentication is via **Keycloak SSO** ([15](15-identity-and-access.md), [ADR-0014](../decisions/adr-0014-idp-keycloak.md)):
|
||||
Headlamp/Grafana use **native OIDC**; UIs without OIDC (Homepage, Prometheus, Alertmanager, Hubble)
|
||||
sit behind the **oauth2-proxy forward-auth** at the Gateway. Access to the C2 and sensitive
|
||||
surfaces requires an authenticated user **in an authorized group** with **MFA** (R-IDP-06).
|
||||
- Trusted TLS on the root and all sub-UIs ([08](08-tls-and-certificates.md)).
|
||||
|
||||
## Acceptance gates
|
||||
@@ -59,6 +61,9 @@ kubeconfig in a browser:
|
||||
- Platform health (nodes, storage, Flux, certs, backups, VPN) is visible without leaving the C2.
|
||||
- Safe operational actions work and are access-controlled + audited; destructive actions confirm.
|
||||
|
||||
## Resolved
|
||||
- Auth mechanism — **Keycloak SSO** (native OIDC + oauth2-proxy forward-auth), [15](15-identity-and-access.md) /
|
||||
[ADR-0014](../decisions/adr-0014-idp-keycloak.md).
|
||||
|
||||
## Open questions
|
||||
- Auth mechanism for the C2 (identity-aware proxy vs per-app auth) — pin in the C2 workstream.
|
||||
- Custom portal vs. Homepage-as-root long term (ADR if we diverge from Homepage).
|
||||
@@ -34,6 +34,13 @@ dependencies: # explicit allow rules; everything else denie
|
||||
ingressRoutes: # hostnames/paths to expose (rendered to HTTPRoute + TLS + DNS)
|
||||
- host: <name>.home.keithjackson.dev
|
||||
port: <servicePort>
|
||||
auth: # SSO integration (R-IDP-02; see 15-identity-and-access.md)
|
||||
mode: oidc | forward-auth | native | none
|
||||
# oidc → app configured as a Keycloak OIDC client (does its own login)
|
||||
# forward-auth → protected by oauth2-proxy → Keycloak at the Gateway (for UIs w/o OIDC)
|
||||
# native → app manages its own auth (e.g. Plex/Seerr via Plex) — documented exception
|
||||
# none → no auth (internal-only, discouraged)
|
||||
groups: [ <keycloak-group>, ... ] # groups allowed access (R-IDP-06)
|
||||
update:
|
||||
channel: stable # never pre-release unless opted in (R-UPD-01)
|
||||
range: <semver range | tag regex># acceptable versions
|
||||
@@ -53,10 +60,12 @@ backup:
|
||||
## Default posture for every service (unless overridden)
|
||||
- `kind: container`, `exposure: internal`, `egress: internet`.
|
||||
- **Default-deny**: no `dependencies` ⇒ can reach only DNS ([03](03-networking-isolation-and-egress.md)).
|
||||
- **Auth default `forward-auth`**: any exposed UI is protected by Keycloak SSO unless it declares
|
||||
native OIDC (`oidc`) or a documented exception (`native`) ([15](15-identity-and-access.md), R-IDP-02).
|
||||
- Longhorn-backed config PVC; off-cluster backup; trusted TLS; auto-DNS; monitored; auto-updated on
|
||||
`stable`.
|
||||
|
||||
## The initial catalog (13 services)
|
||||
## The initial catalog (14 services)
|
||||
|
||||
Egress and dependencies below are **normative intent**; implementers render the exact policies.
|
||||
"Bulk" = large media tier ([04](04-storage-and-state.md)). All get trusted TLS + auto-DNS + monitoring
|
||||
@@ -64,7 +73,8 @@ Egress and dependencies below are **normative intent**; implementers render the
|
||||
|
||||
| # | Service | kind | exposure | egress | Storage | Key dependencies (explicit allows) | Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1 | **plex** | container | public | **internet** | config: longhorn (small, replicated); media: **bulk** | — (clients reach it; Seerr → Plex) | Largest service; media on bulk tier, metadata DB on Longhorn ([04](04-storage-and-state.md)) |
|
||||
| 0 | **keycloak** | container | public | internet | db (PostgreSQL): longhorn (irreplaceable → backed up) | → its DB; ← all SSO clients + oauth2-proxy | **IdP/SSO** ([15](15-identity-and-access.md)); **foundational**, onboarded **early**; DR-critical + break-glass path; hardened public exposure |
|
||||
| 1 | **plex** | container | public | **internet** | config: longhorn (small, replicated); media: **bulk** | — (clients reach it; Seerr → Plex) | Largest service; media on bulk tier, metadata DB on Longhorn ([04](04-storage-and-state.md)); auth: **native** (Plex) |
|
||||
| 2 | **sonarr** | container | internal | **vpn** | config: longhorn; media/downloads: bulk | → qbittorrent, → jackett; ← seerr | VPN + kill-switch (R-NET-06) |
|
||||
| 3 | **radarr** | container | internal | **vpn** | config: longhorn; media/downloads: bulk | → qbittorrent, → jackett; ← seerr | VPN + kill-switch |
|
||||
| 4 | **jackett** | container | internal | **vpn** | config: longhorn | ← sonarr, ← radarr | VPN + kill-switch (indexer) |
|
||||
@@ -90,6 +100,15 @@ radarr → qbittorrent, jackett
|
||||
(everything → DNS only, by default)
|
||||
```
|
||||
|
||||
**SSO integration (`auth.mode`, R-IDP-02; see [15](15-identity-and-access.md)):**
|
||||
- **oidc** (native Keycloak client): gitea, nextcloud, semaphore, kasm, grafana*, headlamp*
|
||||
(*platform UIs, not in the numbered app list).
|
||||
- **forward-auth** (oauth2-proxy → Keycloak): pihole, sonarr, radarr, jackett, qbittorrent, unifi,
|
||||
and platform UIs (Homepage, Prometheus, Alertmanager, Hubble).
|
||||
- **native** (own auth, documented exceptions): plex, seerr (Plex accounts); minecraft
|
||||
(Mojang/Microsoft).
|
||||
- **keycloak** itself is the IdP — its own admin login is local + MFA (the root of trust).
|
||||
|
||||
**"…but not limited to":** the catalog is open-ended. New services follow the same ServiceDefinition
|
||||
and inherit all guarantees automatically — that is the point of the abstraction.
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# 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, Kasm, Semaphore,
|
||||
Headlamp**.
|
||||
2. **Forward-auth proxy** — for apps with **no** usable OIDC, an identity-aware proxy
|
||||
(**oauth2-proxy**) sits in front at the Gateway, authenticates the user against Keycloak, and only
|
||||
then forwards the request. Used for: **PiHole admin, the *arr web UIs (Sonarr/Radarr/Jackett),
|
||||
qBittorrent UI, Homepage, Prometheus, Alertmanager, Hubble UI**, and any other bare UI.
|
||||
|
||||
> 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.
|
||||
|
||||
### Exceptions (documented, not oversights)
|
||||
- **Plex** and **Seerr** use **Plex's own account system** (Plex OAuth) — they do not federate to
|
||||
Keycloak.
|
||||
- **Minecraft** authenticates players via Mojang/Microsoft — N/A.
|
||||
- **UniFi** keeps its native account model (limited SSO support); protect its UI via forward-auth if
|
||||
desired.
|
||||
|
||||
## 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`).
|
||||
|
||||
## 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** (or Helm), and **realm/client/group
|
||||
configuration is reconciled declaratively** (e.g. `keycloak-config-cli` or realm-import CRs). Exact
|
||||
tool is an implementation detail; the requirement is that identity config lives in git and
|
||||
reconciles like everything else.
|
||||
- **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**; 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. If the operator prefers internal-only, public services fall back to
|
||||
their native auth.
|
||||
- **HA**: a single replica with DB on Longhorn + backups is acceptable for the home scale; note
|
||||
clustered Keycloak as a future hardening. **Availability caveat:** SSO becomes a dependency for
|
||||
logging into federated services — document a break-glass path (e.g. a local admin on Gitea) so an
|
||||
IdP outage doesn't lock the operator out of recovery tools.
|
||||
|
||||
## 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, proxy → Keycloak, Keycloak → its DB) are
|
||||
explicit allow rules.
|
||||
- Forward-auth is enforced at the **Gateway** ([09](09-dns-and-ingress.md)) so protection happens
|
||||
before traffic reaches the app.
|
||||
|
||||
## 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 the proxy + 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.
|
||||
|
||||
## Open questions
|
||||
- Final declarative-config tool (`keycloak-config-cli` vs operator realm-import CRs) — implementation
|
||||
detail for WS-SEC.
|
||||
- Confirm `public` exposure for Keycloak (recommended) vs internal-only with native-auth fallback for
|
||||
public services.
|
||||
+4
-2
@@ -76,8 +76,10 @@ survives rebuild; default-deny + `egress` apply to the VM; guest auto-updates.
|
||||
|
||||
## Phase 6 — Service Catalog Onboarding + C2 UI
|
||||
**Entry:** Phases 2–4 done.
|
||||
**Do:** onboard the 13 services via ServiceDefinitions in dependency order (see workstream WS-APPS);
|
||||
**bring Gitea online early in this phase** and cut the GitOps source + CI/CD over to it (Gitea
|
||||
**Do:** onboard the 14 services via ServiceDefinitions in dependency order (see workstream WS-APPS);
|
||||
**bring Gitea and Keycloak online early in this phase** — Keycloak is the foundational IdP so
|
||||
services adopt SSO (native OIDC / forward-auth) as they onboard ([`../docs/15-identity-and-access.md`](../docs/15-identity-and-access.md));
|
||||
and cut the GitOps source + CI/CD over to Gitea (Gitea
|
||||
Actions running the Kyverno gate + Chainsaw conformance Job; PRs as the enforced merge gate —
|
||||
[`../docs/05-gitops-and-delivery.md`](../docs/05-gitops-and-delivery.md), [`../CONTRIBUTING.md`](../CONTRIBUTING.md));
|
||||
stand up the C2 UI (Homepage root + Grafana + Headlamp + Flux status).
|
||||
|
||||
+14
-5
@@ -11,7 +11,7 @@ only via [`../agents/coordination-protocol.md`](../agents/coordination-protocol.
|
||||
| **WS-GITOPS** | GitOps, CI, catalog renderer | [05](../docs/05-gitops-and-delivery.md), [13](../docs/13-service-catalog.md) | R-GITOPS-*, R-UPD-02 | WS-INFRA |
|
||||
| **WS-NET** | Networking, isolation, egress, ingress | [03](../docs/03-networking-isolation-and-egress.md), [09](../docs/09-dns-and-ingress.md) | R-NET-*, R-DNS-* | WS-INFRA |
|
||||
| **WS-STORAGE** | Storage, state, backup/DR | [04](../docs/04-storage-and-state.md) | R-STATE-* | WS-INFRA |
|
||||
| **WS-SEC** | Secrets, TLS, RBAC, auditability | [07](../docs/07-security-and-secrets.md), [08](../docs/08-tls-and-certificates.md) | R-SEC-*, R-TLS-* | WS-INFRA, WS-NET |
|
||||
| **WS-SEC** | Secrets, TLS, RBAC, **identity/SSO**, auditability | [07](../docs/07-security-and-secrets.md), [08](../docs/08-tls-and-certificates.md), [15](../docs/15-identity-and-access.md) | R-SEC-*, R-TLS-*, R-IDP-* | WS-INFRA, WS-NET |
|
||||
| **WS-OBS** | Observability & alerting | [10](../docs/10-observability-and-alerting.md) | R-OBS-* | WS-INFRA, WS-STORAGE |
|
||||
| **WS-UPDATE** | Automated updates | [11](../docs/11-automated-updates.md) | R-UPD-* | WS-GITOPS, WS-OBS |
|
||||
| **WS-VM** | KubeVirt IaaS | [06](../docs/06-vm-workloads-kubevirt.md) | R-PLAT-03 | WS-NET, WS-STORAGE |
|
||||
@@ -34,6 +34,9 @@ only via [`../agents/coordination-protocol.md`](../agents/coordination-protocol.
|
||||
dashboards. Consumed by WS-APPS/WS-VM.
|
||||
- **Update policy contract** (WS-UPDATE) — ImagePolicy/Renovate conventions a ServiceDefinition's
|
||||
`update` block maps to.
|
||||
- **SSO/auth contract** (WS-SEC) — how a ServiceDefinition's `auth` block (oidc / forward-auth /
|
||||
native / groups) maps to a Keycloak client or oauth2-proxy protection ([15](../docs/15-identity-and-access.md)).
|
||||
Consumed by WS-APPS/WS-C2.
|
||||
- **Conformance harness + gate** (WS-TEST) — the reproducible test framework (**Kyverno** for L0
|
||||
policy, **Chainsaw** for L1/L2/L3 behavior + fault-injection, [ADR-0011](../decisions/adr-0011-test-tooling-kyverno-chainsaw.md)),
|
||||
requirement-coverage report, and CI/phase gates every workstream's tests plug into. Each workstream
|
||||
@@ -55,10 +58,16 @@ only via [`../agents/coordination-protocol.md`](../agents/coordination-protocol.
|
||||
and CI/CD; after it is stable, WS-GITOPS cuts Flux's source over to Gitea and moves CI to Gitea
|
||||
Actions ([`../docs/05-gitops-and-delivery.md`](../docs/05-gitops-and-delivery.md),
|
||||
[`../CONTRIBUTING.md`](../CONTRIBUTING.md)). DR-critical → off-cluster backup + mirror. →
|
||||
3. **plex** (largest; validates bulk tier + public exposure) →
|
||||
4. **qbittorrent, jackett** (VPN + kill-switch proof) → 5. **sonarr, radarr** (VPN + arr↔downloader
|
||||
allows) → 6. **seerr** (cross-service allows to arr + plex) → 7. **nextcloud** (DR-critical data +
|
||||
backups) → 8. **minecraft, unifi, semaphore, kasm** (remaining).
|
||||
3. **keycloak (+ oauth2-proxy)** — **foundational IdP/SSO**, brought online early so later services
|
||||
integrate SSO as they onboard ([`../docs/15-identity-and-access.md`](../docs/15-identity-and-access.md)).
|
||||
DR-critical + break-glass path. →
|
||||
4. **plex** (largest; validates bulk tier + public exposure) →
|
||||
5. **qbittorrent, jackett** (VPN + kill-switch proof) → 6. **sonarr, radarr** (VPN + arr↔downloader
|
||||
allows) → 7. **seerr** (cross-service allows to arr + plex) → 8. **nextcloud** (DR-critical data +
|
||||
backups) → 9. **minecraft, unifi, semaphore, kasm** (remaining).
|
||||
|
||||
As each service onboards, it adopts SSO per its `auth` mode (native OIDC or forward-auth) —
|
||||
[`../docs/15-identity-and-access.md`](../docs/15-identity-and-access.md).
|
||||
|
||||
**GitOps source migration** (WS-GITOPS, tied to step 2): local bootstrap source → self-hosted Gitea;
|
||||
CI (Kyverno gate + Chainsaw conformance Job trigger) moves to Gitea Actions; PRs become the enforced
|
||||
|
||||
Reference in new issue
Block a user