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:
Viktor Barzin 2026-06-04 02:58:27 +00:00
parent c9b22c7dd3
commit ad3432d685
7 changed files with 147 additions and 13 deletions

View file

@ -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 45 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.

View file

@ -1,5 +1,13 @@
# K8s Dashboard SSO via Authentik (oauth2-proxy) — Implementation Plan
> **⚠️ AS-BUILT DIVERGED (2026-06-04).** Tasks 23 (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.