Files
homelab/k8s_authentik-migration.md
T

271 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kubernetes RBAC → Authentik Migration Plan
**Status:** Draft — awaiting decisions in [Open Decisions](#open-decisions)
**Date:** 2026-08-19
**Scope:** Move human identity + authorization for the cluster and its apps onto Authentik groups.
---
## 1. Finding: there is nothing to literally import
The original ask was "import the existing RBAC from the cluster into Authentik". Inventory of the live
cluster says that set is empty.
### 1.1 Cluster RBAC audit (live, 2026-08-19)
Every `ClusterRoleBinding` / `RoleBinding` carrying a `User` or `Group` subject:
| Binding | Role | Subjects |
|---|---|---|
| `cluster-admin` | `ClusterRole/cluster-admin` | `Group:system:masters` |
| `system-bootstrap-approve-node-client-csr` | `ClusterRole/system:certificates.k8s.io:certificatesigningrequests:nodeclient` | `Group:system:bootstrappers:nodes` |
| `system-bootstrap-node-bootstrapper` | `ClusterRole/system:node-bootstrapper` | `Group:system:bootstrappers:nodes`, `Group:system:nodes` |
| `system-bootstrap-node-renewal` | `ClusterRole/system:certificates.k8s.io:certificatesigningrequests:selfnodeclient` | `Group:system:nodes` |
| `kube-system/system::extension-apiserver-authentication-reader` | `Role/extension-apiserver-authentication-reader` | `User:system:kube-controller-manager`, `User:system:kube-scheduler` |
| `kube-system/system::leader-locking-kube-controller-manager` | `Role/…` | `User:system:kube-controller-manager` |
| `kube-system/system::leader-locking-kube-scheduler` | `Role/…` | `User:system:kube-scheduler` |
All of these are Talos/control-plane machinery. Every other binding in the cluster targets a
`ServiceAccount` (controllers: Argo CD, cert-manager, Cilium, Longhorn, MinIO operator, Prometheus, …).
**There are zero human users in Kubernetes RBAC.** The only human access path today is the Talos-issued
admin certificate in `cluster-config/kubeconfig`, which lands in `system:masters` — a single shared,
unattributable, non-revocable god credential.
The kube-apiserver has no OIDC configured. `apiServer:` in
[`terraform/templates/controlplane.tftpl:110`](terraform/templates/controlplane.tftpl#L110) contains only
`certSANs`, `image`, `admissionControl` and `auditPolicy`.
### 1.2 What human RBAC *does* exist (app level, scattered)
| Location | Rule |
|---|---|
| [`k8s/bootstrap/phase4-argocd/argocd-values.yaml:163-169`](k8s/bootstrap/phase4-argocd/argocd-values.yaml#L163-L169) | `policy.default: role:readonly`; `g, admin/rock/cicd, role:admin`; `g, homelab-admins, role:admin` |
| [`k8s/infra/logging/grafana-values.yaml:86`](k8s/infra/logging/grafana-values.yaml#L86) | `role_attribute_path: contains(groups[*], 'grafana-admins') && 'Admin' \|\| 'Viewer'` |
| [`k8s/infra/iam/scripts/authentik-provision.py`](k8s/infra/iam/scripts/authentik-provision.py) (`minio` scope mapping) | `homelab-admins → consoleAdmin`, everyone else → `readonly` |
| Authentik, provisioned | Groups `homelab-admins` (superuser), `grafana-admins`. Single user: `rock`, member of both. |
### 1.3 Known drift
[`project-usage/authentik-oidc.md`](project-usage/authentik-oidc.md) documents three groups —
`homelab-admins`, `homelab-devops`, `homelab-viewers`. Only `homelab-admins` and `grafana-admins` are
actually created by the provisioner. The documentation is wrong today; Phase 1 fixes it.
### 1.4 Restated goal
Not an import. This is: **define the group taxonomy in Authentik, make Authentik the sole issuer of human
identity, then bind those groups to Kubernetes roles and app roles declaratively through GitOps.**
---
## 2. Constraints
- **GitOps only.** No local `terraform apply`, no manual `kubectl apply`, no Authentik console clicking for
anything that must survive a rebuild. Changes land in git; Forgejo Actions and Argo CD do the applying.
- **Authentik config stays Argo CD-managed** via the `authentik-provision.py` PostSync hook. No Terraform
Authentik provider. A prior attempt at one is abandoned at
`.claude/worktrees/debug-auth-issue/terraform/authentik-config.tf` — do not revive it.
- **Terraform is limited to Talos machine config** (Phase 3 only), applied through the pipeline.
- **The Talos admin certificate stays as break-glass.** It is unaffected by OIDC and remains the recovery
path when Authentik is unavailable.
---
## 3. Phases
Each phase is independently shippable and independently revertible.
### Phase 1 — Group taxonomy
Extend the provisioner's group step (`[2/5]` in `authentik-provision.py`) from two groups to four:
| Group | `is_superuser` | Intent |
|---|---|---|
| `homelab-admins` | `true` | Full cluster + full app admin |
| `homelab-devops` | `false` | Deploy and operate workloads; no IAM, no cluster config |
| `homelab-viewers` | `false` | Read-only everywhere |
| `grafana-admins` | `false` | Grafana `Admin` role specifically |
Files: `k8s/infra/iam/scripts/authentik-provision.py`, `project-usage/authentik-oidc.md` (fix §1.3 drift).
**Verify**
```bash
kubectl -n iam exec deploy/authentik-server -- \
curl -sH "Authorization: Bearer $TOKEN" localhost:9000/api/v3/core/groups/ \
| jq -r '.results[].name'
# expect: homelab-admins, homelab-devops, homelab-viewers, grafana-admins
# and: rock still a member of homelab-admins + grafana-admins
```
**Rollback:** revert the commit. Extra groups with no bindings grant nothing.
---
### Phase 2 — Authentik OAuth2 provider for Kubernetes
Add a `kubernetes` entry to the `SERVICES` dict in `authentik-provision.py`, with two departures from the
existing web-app entries:
- `client_type: public` — kubectl is a public client and cannot hold a secret. No k8s Secret is created for
it, so the `client_secret_source` machinery is skipped for this service.
- `grant_types: ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"]`
plus PKCE, to support both browser and headless login.
- `redirect_uris`: `http://localhost:8000`, `http://localhost:18000` (kubelogin's loopback listeners).
Reuses the existing `groups` property mapping created in step `[1/5]`. No new claim work.
**Verify**
```bash
curl -s https://authentik.riotpiao.com/application/o/kubernetes/.well-known/openid-configuration | jq .
# device-code login, then decode:
kubectl oidc-login get-token --oidc-issuer-url=... --oidc-client-id=kubernetes --oidc-extra-scope=groups
# id_token payload must contain: preferred_username, groups[]
```
**Rollback:** delete provider + application in Authentik; nothing else consumes them yet.
---
### Phase 3 — Enable OIDC on kube-apiserver (Terraform, pipeline-applied)
Add to `apiServer.extraArgs` in
[`terraform/templates/controlplane.tftpl`](terraform/templates/controlplane.tftpl#L110):
```yaml
extraArgs:
oidc-issuer-url: https://authentik.riotpiao.com/application/o/kubernetes/
oidc-client-id: kubernetes
oidc-username-claim: preferred_username
oidc-username-prefix: "authentik:"
oidc-groups-claim: groups
oidc-groups-prefix: "authentik:"
```
> **Security — the prefixes are mandatory, not cosmetic.**
> Without `oidc-groups-prefix`, anyone able to create or rename an Authentik group to `system:masters`
> obtains `cluster-admin` on the cluster. Without `oidc-username-prefix`, an Authentik username can
> impersonate a built-in identity such as `system:kube-controller-manager`. Both prefixes must be present
> in the same change that enables OIDC. Do not ship this phase partially.
Operational notes:
- Apply is a rolling control-plane machine-config change: one node at a time, wait for `Ready` and for
etcd quorum before proceeding to the next.
- The Talos admin certificate authenticates via client cert, not OIDC, and is unaffected. It is the
recovery path if the issuer URL is wrong or Authentik is down.
- The issuer must be reachable from the control-plane nodes and its TLS chain must be trusted by them.
Confirm this before applying, because a bad issuer URL means the apiserver logs OIDC discovery failures
on every start.
**Verify** (per node, after each rolls)
```bash
kubectl auth whoami --token="$ID_TOKEN"
# expect Username: authentik:rock
# Groups: authentik:homelab-admins, authentik:grafana-admins, system:authenticated
kubectl --kubeconfig cluster-config/kubeconfig get nodes # cert path still works
```
**Rollback:** revert the template change, re-run the pipeline, roll the control plane back. Cert-based
access is never interrupted, so this rollback is safe at any point.
---
### Phase 4 — Bind Authentik groups to Kubernetes roles
New manifest `k8s/infra/iam/rbac-oidc-bindings.yaml`, referenced from
[`k8s/infra/iam/kustomization.yaml`](k8s/infra/iam/kustomization.yaml):
| Subject | Role | Kind |
|---|---|---|
| `Group:authentik:homelab-admins` | `ClusterRole/cluster-admin` | ClusterRoleBinding |
| `Group:authentik:homelab-devops` | `ClusterRole/edit` | see [Open Decisions](#open-decisions) #2 |
| `Group:authentik:homelab-viewers` | `ClusterRole/view` | ClusterRoleBinding |
`admin`, `edit` and `view` are the built-in aggregated ClusterRoles and already exist in the cluster — no
custom roles needed.
**Verify**
```bash
kubectl auth can-i '*' '*' --all-namespaces --as=probe --as-group=authentik:homelab-admins # yes
kubectl auth can-i delete pods -A --as=probe --as-group=authentik:homelab-viewers # no
kubectl auth can-i get pods -n apps --as=probe --as-group=authentik:homelab-viewers # yes
kubectl auth can-i get secrets -n iam --as=probe --as-group=authentik:homelab-devops # no
```
**Rollback:** revert the commit; Argo CD prunes the bindings.
---
### Phase 5 — Client wiring and documentation
- Ship a kubeconfig template using the `kubelogin` (`kubectl oidc-login`) exec credential plugin — no
embedded certs, no long-lived token on disk.
- Document login, token cache location, and the break-glass cert path in
[`project-usage/authentik-oidc.md`](project-usage/authentik-oidc.md).
- Same edit corrects the stale group list from §1.3.
**Verify:** on a clean machine with no certificates, `kubectl get nodes` triggers a browser login and
succeeds; `kubectl auth whoami` reports the `authentik:`-prefixed identity.
---
### Phase 6 — Converge app RBAC on the same four groups
| Target | Change |
|---|---|
| Argo CD `policy.csv` | add `g, homelab-devops, role:admin` (or a scoped custom role) and `g, homelab-viewers, role:readonly` |
| Grafana `role_attribute_path` | three-tier: `grafana-admins`/`homelab-admins``Admin`, `homelab-devops``Editor`, else `Viewer` |
| MinIO `policy` claim expression | `homelab-admins``consoleAdmin`, `homelab-devops``readwrite`, else `readonly` |
| Argo CD local accounts | retire `accounts.rock` once SSO admin is proven. **Keep `accounts.cicd`** — the pipeline needs a non-SSO apiKey. |
**Verify:** end-to-end browser login per app as a test user who is *only* in `homelab-devops`, confirming
the expected role in each of Argo CD, Grafana and MinIO. The e2e suite under
[`tests/e2e/tests/authentik.spec.ts`](tests/e2e/tests/authentik.spec.ts) is the place to encode this.
**Rollback:** per-app revert; each app's mapping is independent.
---
## 4. Risks
| Risk | Severity | Mitigation |
|---|---|---|
| Missing `oidc-groups-prefix` → group-name privilege escalation to `cluster-admin` | Critical | Prefixes ship in the same commit as the OIDC flags; Phase 4 verification asserts the prefixed form |
| Bad issuer URL / untrusted TLS bricks apiserver OIDC | Medium | Cert-based admin access is unaffected; roll one control-plane node at a time and check apiserver logs before continuing |
| Authentik outage blocks all human cluster access | Medium | Talos admin cert is the documented break-glass path and never depends on Authentik |
| Argo CD `cicd` account removed by accident → pipeline loses cluster access | Medium | Phase 6 explicitly retires only `accounts.rock` |
| Group renamed in Authentik silently revokes cluster access | Low | Group names are asserted by the provisioner on every Argo CD sync, so drift self-heals |
---
## 5. Open Decisions
1. **Is kubectl-via-OIDC actually wanted?**
Phases 35 only pay for themselves if humans besides the shared admin cert need API access. If the
answer is no, the plan collapses to Phases 1 and 6: no Terraform change, no control-plane roll, app-level
SSO consolidation only.
2. **`homelab-devops` blast radius** — cluster-wide `ClusterRoleBinding` to `edit`, or namespaced
`RoleBinding`s limited to application namespaces (excluding `iam`, `kube-system`, `argocd`)?
Namespaced is the tighter default; cluster-wide is less to maintain.
3. **Username claim**`preferred_username` (human-readable in audit logs, mutable) versus `sub`
(stable, opaque). Recommendation: `preferred_username`, given a single-operator homelab where audit
readability beats rename-safety.
---
## 6. Reference
- Kubernetes OIDC authentication: https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens
- Kubernetes RBAC: https://kubernetes.io/docs/reference/access-authn-authz/rbac/
- Talos apiserver `extraArgs`: https://www.talos.dev/latest/reference/configuration/v1alpha1/config/#Config.cluster.apiServer
- Authentik OAuth2 provider: https://docs.goauthentik.io/docs/add-secure-apps/providers/oauth2/
- Authentik property mappings (scope/claims): https://docs.goauthentik.io/docs/add-secure-apps/providers/property-mappings/
- kubelogin (`kubectl oidc-login`): https://github.com/int128/kubelogin
- Argo CD RBAC: https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/
- Grafana generic OAuth: https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/generic-oauth/
- MinIO OpenID identity management: https://min.io/docs/minio/linux/administration/identity-access-management/oidc-access-management.html