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>
14 KiB
Authentication & Authorization
Overview
The homelab uses Authentik as a centralized identity provider (IdP) for all services, providing single sign-on (SSO) via OIDC and forward authentication for ingress protection. Authentik integrates with social login providers (Google, GitHub, Facebook), manages user groups and RBAC policies, and enforces authentication at the Traefik ingress layer. The system supports both human authentication (OIDC SSO) and service-to-service authentication (Kubernetes SA JWT for CI/CD).
Architecture Diagram
graph TB
User[User Browser]
Traefik[Traefik Ingress]
ForwardAuth[ForwardAuth Middleware]
Authentik[Authentik<br/>3 server + 3 worker<br/>+ embedded outpost]
Backend[Protected Backend Service]
Social[Social Providers<br/>Google/GitHub/Facebook]
K8s[Kubernetes API]
Vault[Vault]
User -->|1. HTTPS Request| Traefik
Traefik -->|2. Auth Check| ForwardAuth
ForwardAuth -->|3. Verify Session| Authentik
Authentik -->|4a. Not Authenticated| User
User -->|4b. Login Flow| Authentik
Authentik -->|5. Social Login| Social
Social -->|6. OAuth Callback| Authentik
Authentik -->|7. Session Cookie| User
User -->|8. Retry Request| Traefik
ForwardAuth -->|9. Authenticated| Backend
Traefik -->|10. Forward Request| Backend
K8s -->|OIDC Groups| Authentik
Vault -->|OIDC Auth| Authentik
Components
| Component | Version | Location | Purpose |
|---|---|---|---|
| Authentik Server | 2026.2.2 | stacks/authentik/ |
Core IdP application servers (2 replicas) |
| Authentik Worker | 2026.2.2 | stacks/authentik/ |
Background task processors (2 replicas) |
| PgBouncer | Latest | stacks/authentik/ |
PostgreSQL connection pooler (3 replicas) |
| Embedded Outpost | - | Built into Authentik | Forward auth endpoint for Traefik |
| Traefik ForwardAuth | - | modules/kubernetes/ingress_factory/ |
Middleware attached when auth = "required" or "public" |
| Vault OIDC Method | - | stacks/vault/ |
Human SSO authentication to Vault |
| Vault K8s Auth | - | stacks/vault/ |
Service account JWT authentication |
How It Works
Forward Authentication Flow
Services pick an auth tier via the auth enum on the ingress_factory module (default "required", fail-closed):
| Tier | Effect | When to use |
|---|---|---|
"required" |
Authentik forward-auth gates every request | Backend has no own user auth — Authentik is the only gate |
"app" |
No Authentik middleware; backend's own login is the gate | Backend handles its own user auth (NextAuth, Django, OAuth, bearer-token API) |
"public" |
Authentik anonymous binding via public outpost |
Audit trail without gating; only works for top-level browser navigation |
"none" |
No Authentik middleware at all | Anubis-fronted content, webhooks, OAuth callbacks, native-client APIs (CalDAV, WebDAV, Git) |
When auth = "required", an unauthenticated request flows:
- Request hits Traefik ingress
- ForwardAuth middleware calls Authentik embedded outpost
- Authentik checks for valid session cookie
- If missing/invalid, redirects to Authentik login page (authentik.viktorbarzin.me)
- User authenticates via social provider (Google/GitHub/Facebook)
- Authentik creates session, sets cookie, redirects back to original URL
- Subsequent requests include session cookie, pass auth check, reach backend
Authentik adds authentication headers (user, email, groups) to forwarded requests. These headers are stripped before reaching the backend to prevent confusion.
Anti-exposure guard: every auth = "app" or auth = "none" line MUST have a preceding # auth = "<tier>": <reason> comment documenting what gates the backend (for "app") or why the endpoint is intentionally public (for "none"). The convention is enforced by scripts/check-ingress-auth-comments.py, which scripts/tg runs on every plan/apply/destroy/refresh and blocks the terragrunt invocation if violated. Stack-scoped — each stack documents itself.
Social Login & Invitation Flow
All new users must use an invitation link to register. The invitation-enrollment flow:
- invitation-validation - Validates invitation token
- enrollment-identification - Social login (Google/GitHub/Facebook) + passkey registration
- enrollment-prompt - Collect name/email
- enrollment-user-write - Create user account
- enrollment-login - Auto-login after creation
Group membership is auto-assigned from the invitation's fixed_data field. This prevents open registration while maintaining SSO convenience.
OIDC Applications
Authentik provides OIDC for 10 applications:
| Application | Type | Purpose |
|---|---|---|
| Cloudflare Access | OIDC | Cloudflare Zero Trust tunnels |
| Domain-wide catch-all | Proxy (Forward Auth) | Protect all *.viktorbarzin.me services |
| Forgejo | OIDC | Git repository SSO |
| Grafana | OIDC | Monitoring dashboard SSO |
| Headscale | OIDC | Tailscale control plane auth |
| Immich | OIDC | Photo management SSO |
| 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
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:
| 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) |
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:
- User authenticates to Authentik (via the
kubectlplugin, or via oauth2-proxy for the web Dashboard). - Receives an OIDC id_token with
email+groupsclaims. - K8s API validates the token against the matching issuer's Authentik JWKS.
- RBAC binds the user (by email) to roles — see
stacks/rbac/modules/rbac/main.tf:adminrole users →cluster-adminpower-userrole → custom cluster ClusterRole (read-mostly, limited write)namespace-ownerrole →adminRoleBinding in their namespace(s) + cluster read-only
Web Dashboard SSO: the
k8s.viktorbarzin.meingress 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 thek8s-dashboardconfidential client and injects the user's id_token asAuthorization: Bearerupstream 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 thekubernetes-*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-configlives in the kube-apiserver static-pod manifest, whichkubeadm upgraderegenerates — re-apply therbacstack after any control-plane upgrade to restore apiserver OIDC.
Authentik Groups
9 groups manage authorization:
- Allow Login Users - Base group, can authenticate to any OIDC app
- authentik Admins - Full Authentik admin UI access
- Headscale Users - Can access Headscale control plane
- Home Server Admins - Admin access to homelab services
- Wrongmove Users - Access to Wrongmove real estate app
- kubernetes-admins - K8s cluster-admin role
- kubernetes-power-users - K8s read-mostly access
- kubernetes-namespace-owners - K8s namespace-scoped admin
- Task Submitters - Can submit tasks to cluster task runner
Vault Authentication
For humans:
- OIDC method using Authentik as provider
- SSO login to Vault UI and CLI
- Group-based policy assignment
For services (CI/CD):
- Kubernetes SA JWT authentication
- Woodpecker CI uses service account token
- Vault K8s secrets engine roles:
dashboard-admin- K8s dashboard admin tokenci-deployer- Deploy workloads via CI/CDopenclaw- AI assistant cluster accesslocal-admin- Local development access
Configuration
Key Config Files
| Path | Purpose |
|---|---|
stacks/authentik/ |
Authentik deployment (servers, workers, PgBouncer) |
modules/kubernetes/ingress_factory/ |
Auth-tier enum + per-ingress middleware composition |
stacks/traefik/modules/traefik/middleware.tf |
ForwardAuth middleware definitions (required + public outposts) |
scripts/check-ingress-auth-comments.py |
Comment-convention guard wired into scripts/tg |
stacks/vault/auth.tf |
Vault OIDC and K8s auth methods |
Vault Paths
- OIDC config:
auth/oidc- Authentik integration settings - K8s auth:
auth/kubernetes- SA JWT validation - K8s secrets engine:
kubernetes/- Dynamic kubeconfig/SA token generation
Terraform Stacks
stacks/authentik/- Authentik infrastructurestacks/platform/- Traefik ingress with ForwardAuthstacks/vault/- Vault auth methods
Ingress Protection Examples
Authentik-gated admin UI (default):
module "myapp_ingress" {
source = "../../modules/kubernetes/ingress_factory"
name = "myapp"
namespace = "myapp"
tls_secret_name = var.tls_secret_name
# auth = "required" is the default — Authentik forward-auth is the gate.
}
Backend with its own user auth (no Authentik in the way):
module "myapp_ingress" {
source = "../../modules/kubernetes/ingress_factory"
name = "myapp"
namespace = "myapp"
tls_secret_name = var.tls_secret_name
# auth = "app": myapp uses NextAuth + Google OAuth; mobile clients can't follow Authentik 302.
auth = "app"
}
Intentionally public webhook receiver:
module "myapp_ingress" {
source = "../../modules/kubernetes/ingress_factory"
name = "webhook"
namespace = "webhooks"
tls_secret_name = var.tls_secret_name
# auth = "none": upstream signs payloads with HMAC; no user identity expected.
auth = "none"
}
Decisions & Rationale
Why Authentik over Keycloak?
- Lighter weight: Lower resource footprint (3+3+3 replicas vs Keycloak's heavier Java runtime)
- Better UX: Modern UI, simpler admin experience, better mobile support
- Python-based: Easier to extend, faster startup times, better developer experience
- Active development: More frequent releases, responsive community
Why Forward Auth over Sidecar?
- Simpler architecture: Single auth check at ingress, no sidecar per pod
- Works with any backend: Language/framework agnostic, no SDK required
- Centralized policy: All auth logic in Authentik, not distributed across sidecars
- Performance: Single auth check per session, not per request
Why OIDC for Kubernetes?
- SSO integration: Same login as all other services, no separate credentials
- No credential management: No kubeconfig secrets to rotate, tokens are short-lived
- Group-based RBAC: Centralized group management in Authentik, automatic K8s role mapping
- Public client flow: No client secret needed, works in kubectl plugins and dashboards
Why Invitation-Only Enrollment?
- Security: Prevents open internet access to homelab services
- Controlled onboarding: Explicit approval before granting access
- Social login convenience: No password management, leverages trusted providers
- Group auto-assignment: Invitation encodes initial group membership
Troubleshooting
Headers Not Stripped
Problem: Backend receives X-Authentik-Username, X-Authentik-Email, X-Authentik-Groups headers and breaks.
Fix: Traefik middleware should strip these headers before forwarding. Check ingress_factory module for header stripping config.
OIDC Token Expired
Problem: kubectl returns 401 Unauthorized.
Fix: Re-authenticate to refresh token:
kubectl oidc-login setup --oidc-issuer-url=https://authentik.viktorbarzin.me/application/o/kubernetes/
Social Login Redirect Loop
Problem: After social login, redirects to Authentik login page instead of destination.
Fix: Check Authentik application's redirect URIs. Must include https://authentik.viktorbarzin.me/source/oauth/callback/* for social providers.
User Not in Correct Group
Problem: User authenticated but lacks permissions.
Fix: Check group membership in Authentik admin UI. Verify invitation fixed_data specified correct group. Manually add to group if needed.
Vault OIDC Login Fails
Problem: Vault UI redirects to Authentik but returns error.
Fix:
- Verify Vault OIDC client credentials in Authentik
- Check Vault OIDC issuer URL matches Authentik
- Ensure Vault redirect URI (
https://vault.viktorbarzin.me/ui/vault/auth/oidc/oidc/callback) is registered in Authentik
K8s Auth Group Mapping Not Working
Problem: User authenticated but kubectl shows limited permissions despite being in kubernetes-admins.
Fix:
- Verify group claim is present in token:
kubectl oidc-login get-token | jq -R 'split(".") | .[1] | @base64d | fromjson' - Check ClusterRoleBinding maps group correctly:
kubectl get clusterrolebinding -o yaml | grep kubernetes-admins - Ensure Authentik OIDC app includes
groupsscope
Related
- Security & L7 Protection - CrowdSec, anti-AI scraping, rate limiting
- Networking - Ingress, DNS, load balancing
- Vault Runbook - Vault operations and troubleshooting
- Kubernetes Access Runbook - Setting up kubectl with OIDC