docs(k8s-dashboard): dashboard SSO as-built (Option B multi-issuer apiserver)
Update authentication.md (structured multi-issuer AuthenticationConfiguration + dashboard SSO flow), multi-tenancy.md (web dashboard access), authentik-state (new k8s-dashboard app + gheorghe groups), service-catalog (dashboard auth), and the k8s-version-upgrade runbook (kubeadm wipes --authentication-config → re-apply rbac post-upgrade). Design/plan addenda record the issuer-constraint pivot from the original dual-aud approach. [ci skip] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c9b22c7dd3
commit
ad3432d685
7 changed files with 147 additions and 13 deletions
|
|
@ -99,24 +99,55 @@ Authentik provides OIDC for 10 applications:
|
|||
| Grafana | OIDC | Monitoring dashboard SSO |
|
||||
| Headscale | OIDC | Tailscale control plane auth |
|
||||
| Immich | OIDC | Photo management SSO |
|
||||
| Kubernetes | OIDC (public client) | K8s API authentication |
|
||||
| Kubernetes | OIDC (public client) | K8s API authentication (kubectl / kubelogin CLI) |
|
||||
| Kubernetes Dashboard | OIDC (confidential, via oauth2-proxy) | Web dashboard SSO with per-user RBAC |
|
||||
| Linkwarden | OIDC | Bookmark manager SSO |
|
||||
| Matrix | OIDC | Matrix homeserver SSO |
|
||||
| Wrongmove | OIDC | Real estate app SSO |
|
||||
|
||||
### Kubernetes RBAC via OIDC
|
||||
|
||||
Kubernetes API server is configured with OIDC issuer: `https://authentik.viktorbarzin.me/application/o/kubernetes/`
|
||||
The kube-apiserver uses a **structured `AuthenticationConfiguration`**
|
||||
(`apiserver.config.k8s.io/v1`, file `/etc/kubernetes/pki/auth-config.yaml`,
|
||||
flag `--authentication-config`) that trusts **two** Authentik issuers — managed
|
||||
by `stacks/rbac/modules/rbac/apiserver-oidc.tf`:
|
||||
|
||||
The public client flow:
|
||||
| Issuer (Authentik app) | Audience | Used by |
|
||||
|---|---|---|
|
||||
| `…/application/o/kubernetes/` | `kubernetes` | `kubectl` / kubelogin CLI (public client) |
|
||||
| `…/application/o/k8s-dashboard/` | `k8s-dashboard` | oauth2-proxy in front of the web Dashboard (confidential client) |
|
||||
|
||||
1. User authenticates to Authentik via `kubectl` plugin or dashboard
|
||||
2. Receives OIDC token with group claims
|
||||
3. K8s API validates token against Authentik JWKS endpoint
|
||||
4. Maps groups to ClusterRoleBindings:
|
||||
- `kubernetes-admins` → `cluster-admin` (full cluster access)
|
||||
- `kubernetes-power-users` → custom ClusterRole (read-mostly, limited write)
|
||||
- `kubernetes-namespace-owners` → RoleBindings per namespace (namespace-scoped admin)
|
||||
Both map `username <- email` and `groups <- groups` with **empty prefixes** (so
|
||||
tokens map to RBAC subjects `kind: User, name: <raw email>` and verbatim group
|
||||
names). This replaced the legacy single `--oidc-*` flags (one issuer only),
|
||||
which a kubeadm upgrade had silently wiped.
|
||||
|
||||
The flow:
|
||||
|
||||
1. User authenticates to Authentik (via the `kubectl` plugin, or via oauth2-proxy
|
||||
for the web Dashboard).
|
||||
2. Receives an OIDC id_token with `email` + `groups` claims.
|
||||
3. K8s API validates the token against the matching issuer's Authentik JWKS.
|
||||
4. RBAC binds the user (by email) to roles — see `stacks/rbac/modules/rbac/main.tf`:
|
||||
- `admin` role users → `cluster-admin`
|
||||
- `power-user` role → custom cluster ClusterRole (read-mostly, limited write)
|
||||
- `namespace-owner` role → `admin` RoleBinding in their namespace(s) + cluster read-only
|
||||
|
||||
> **Web Dashboard SSO:** the `k8s.viktorbarzin.me` ingress points at
|
||||
> **oauth2-proxy** (`stacks/k8s-dashboard/oauth2_proxy.tf`, `auth = "none"` —
|
||||
> oauth2-proxy is the gate), which runs the Authentik OIDC code-flow against the
|
||||
> `k8s-dashboard` confidential client and injects the user's id_token as
|
||||
> `Authorization: Bearer` upstream to the Dashboard's Kong proxy. The Dashboard
|
||||
> then talks to the apiserver **as the user**, so per-user RBAC applies (a
|
||||
> namespace-owner manages only their namespace; admins see everything). A group
|
||||
> policy on the Authentik app restricts login to the `kubernetes-*` groups.
|
||||
> Replaced the prior forward-auth + static cluster-admin ServiceAccount (which
|
||||
> made every authenticated user cluster-admin). Design:
|
||||
> `docs/plans/2026-06-04-k8s-dashboard-sso-design.md`.
|
||||
|
||||
> **Upgrade caveat:** `--authentication-config` lives in the kube-apiserver
|
||||
> static-pod manifest, which `kubeadm upgrade` regenerates — **re-apply the
|
||||
> `rbac` stack after any control-plane upgrade** to restore apiserver OIDC.
|
||||
|
||||
### Authentik Groups
|
||||
|
||||
|
|
|
|||
|
|
@ -171,6 +171,18 @@ Each user receives:
|
|||
```
|
||||
6. User can now run `kubectl` commands
|
||||
|
||||
### Web Dashboard (no CLI needed)
|
||||
|
||||
Namespace-owners can also manage their namespace from the **Kubernetes
|
||||
Dashboard** at `https://k8s.viktorbarzin.me` using their Authentik account — no
|
||||
kubectl, no token paste. oauth2-proxy runs the SSO flow and injects the user's
|
||||
OIDC id_token, so the dashboard talks to the apiserver **as the user**: a
|
||||
namespace-owner gets full control of their namespace(s) and read-only
|
||||
visibility elsewhere; admins see everything. Login is restricted (Authentik
|
||||
group policy) to the `kubernetes-*` groups. See
|
||||
`docs/architecture/authentication.md` → "Kubernetes RBAC via OIDC" and
|
||||
`docs/plans/2026-06-04-k8s-dashboard-sso-design.md`.
|
||||
|
||||
### RBAC Groups
|
||||
|
||||
| Group | ClusterRole | Scope | Members |
|
||||
|
|
|
|||
|
|
@ -266,3 +266,55 @@ follow-up. No apiserver, RBAC, data, or CLI changes to unwind.
|
|||
| Dashboard v7 ignores a pre-set Authorization header (known friction: kubernetes/dashboard #5105, #1213) | `pass_authorization_header` + `set_authorization_header`; validate in §7; kong forwards headers by default |
|
||||
| ESO first-apply ordering | `terragrunt apply -target` the ExternalSecret first (documented plan-time pattern) |
|
||||
| Single-master apiserver assumption (memory id=2484) | We don't touch apiserver flags; no new exposure |
|
||||
|
||||
---
|
||||
|
||||
## 12. ADDENDUM (2026-06-04) — As-built pivoted to Option B (apiserver multi-issuer)
|
||||
|
||||
Sections 4–5 above describe the *original* plan: a separate `k8s-dashboard`
|
||||
confidential client whose token carries a dual `aud` so the apiserver (pinned
|
||||
to `--oidc-client-id=kubernetes`) would accept it **without** an apiserver
|
||||
change. **That approach does not work**, for a reason discovered during
|
||||
implementation:
|
||||
|
||||
1. **The issuer is the binding constraint, not the audience.** Every Authentik
|
||||
OAuth2 application has its own per-slug issuer. A token from the
|
||||
`k8s-dashboard` app has `iss=…/o/k8s-dashboard/`, but the apiserver does an
|
||||
**exact issuer-string match** against its single configured issuer
|
||||
(`…/o/kubernetes/`). The dual-`aud` scope mapping is irrelevant — the token
|
||||
is rejected on issuer before audience is even considered.
|
||||
|
||||
2. **Apiserver OIDC was already silently broken.** Inspecting the live
|
||||
`kube-apiserver` static-pod manifest showed **no `--oidc-*` flags at all** —
|
||||
the kubeadm v1.34 upgrade had regenerated the manifest and dropped the
|
||||
flags the `rbac` stack's `null_resource` had injected (its content-hash
|
||||
trigger never re-fired). So OIDC apiserver auth was off cluster-wide.
|
||||
|
||||
3. **Reusing the `kubernetes` app (make it confidential) — rejected.** It would
|
||||
force distributing the now-confidential client secret to every CLI user via
|
||||
the **public** k8s-portal `/setup/script` endpoint (a leak), plus
|
||||
re-onboarding existing CLI users. Too invasive.
|
||||
|
||||
**As-built = Option B: structured `AuthenticationConfiguration` on the
|
||||
apiserver trusting BOTH issuers.** `stacks/rbac/modules/rbac/apiserver-oidc.tf`
|
||||
now writes `/etc/kubernetes/pki/auth-config.yaml`
|
||||
(`apiserver.config.k8s.io/v1`) with two `jwt` issuers — `kubernetes`
|
||||
(audience `kubernetes`, for the kubelogin CLI) and `k8s-dashboard` (audience
|
||||
`k8s-dashboard`, for oauth2-proxy) — each mapping `username<-email` and
|
||||
`groups<-groups` with empty prefixes (to match existing RBAC subjects). The
|
||||
legacy `--oidc-*` flags are replaced by `--authentication-config=…`. The remote
|
||||
script health-gates `/livez` and **auto-rolls-back** the manifest if the
|
||||
single-master apiserver doesn't recover. The oauth2-proxy + `k8s-dashboard`
|
||||
Authentik app from §4 are reused unchanged (the dual-`aud` mapping is now
|
||||
harmless — issuer2 only requires `k8s-dashboard ∈ aud`).
|
||||
|
||||
This keeps the CLI flow 100% untouched (its own `kubernetes` issuer is one of
|
||||
the two trusted issuers) and restores the apiserver OIDC that the kubeadm
|
||||
upgrade had broken.
|
||||
|
||||
**Known drift (carried forward):** a future `kubeadm upgrade` will again
|
||||
regenerate the manifest and drop `--authentication-config`. The
|
||||
content-hash trigger won't auto-detect this. **Operational mitigation:
|
||||
re-apply the `rbac` stack after every k8s control-plane upgrade** (add to the
|
||||
upgrade runbook). The `rbac` provisioner needs `TF_VAR_ssh_private_key` (an SSH
|
||||
key authorized on the master) — it is not wired from Vault yet.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
# K8s Dashboard SSO via Authentik (oauth2-proxy) — Implementation Plan
|
||||
|
||||
> **⚠️ AS-BUILT DIVERGED (2026-06-04).** Tasks 2–3 (oauth2-proxy + `k8s-dashboard`
|
||||
> Authentik app) shipped as written, but the audience strategy here is WRONG: the
|
||||
> apiserver matches the token **issuer** exactly, and a separate app has a
|
||||
> different per-slug issuer — so the dual-`aud` trick can't avoid an apiserver
|
||||
> change. The implementation pivoted to **Option B**: a structured multi-issuer
|
||||
> `AuthenticationConfiguration` on the apiserver (`stacks/rbac`). See the
|
||||
> **ADDENDUM (§12)** in `2026-06-04-k8s-dashboard-sso-design.md` for the as-built.
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Let namespace-owner users (e.g. gheorghe / `vabbit81`) open `https://k8s.viktorbarzin.me`, log in once with Authentik, and manage their own namespace in the Kubernetes Dashboard under their existing per-user RBAC.
|
||||
|
|
|
|||
|
|
@ -127,6 +127,28 @@ Exposed in K8s via ExternalSecret `k8s-upgrade-creds` in the `k8s-upgrade` names
|
|||
|
||||
## Common Operations
|
||||
|
||||
### Post-upgrade: restore apiserver OIDC (REQUIRED after any control-plane bump)
|
||||
|
||||
`kubeadm upgrade apply` **regenerates `/etc/kubernetes/manifests/kube-apiserver.yaml`
|
||||
and drops the `--authentication-config` flag**, silently disabling apiserver
|
||||
OIDC (kubectl/kubelogin CLI **and** the web dashboard SSO break — tokens get
|
||||
401). This is not auto-detected (the `rbac` stack's `null_resource` trigger is a
|
||||
content hash that doesn't change). After any control-plane upgrade, re-apply:
|
||||
|
||||
```bash
|
||||
cd stacks/rbac
|
||||
TF_VAR_ssh_private_key="$(cat ~/.ssh/id_ed25519)" \
|
||||
VAULT_ADDR=https://vault.viktorbarzin.me ../../scripts/tg apply \
|
||||
--non-interactive -target=module.rbac.null_resource.apiserver_oidc_config
|
||||
```
|
||||
|
||||
(`ssh_private_key` must be a key authorized for `wizard@<master>`; it is not yet
|
||||
wired from Vault.) The provisioner re-writes `/etc/kubernetes/pki/auth-config.yaml`
|
||||
(both `kubernetes` + `k8s-dashboard` issuers), re-adds the flag, and
|
||||
health-gates `/livez` with auto-rollback. Verify: `curl -sk
|
||||
https://localhost:6443/livez` on the master = `ok`, and the apiserver manifest
|
||||
contains `--authentication-config`. See `docs/plans/2026-06-04-k8s-dashboard-sso-design.md`.
|
||||
|
||||
### Verify the pipeline is healthy
|
||||
```bash
|
||||
# CronJob present + not suspended
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue