TL;DR
I am deploying Authentik as a centralized identity provider for my k3s cluster. It replaces the current OAuth2 Proxy setup with proper SSO, federates Google as a social login source, and introduces group-based RBAC (admins, writers, readers) across all services. The migration is phased – public services first via Traefik forwardAuth, then internal services via native OIDC, then proxy-protected apps that have no OIDC support. OAuth2 Proxy stays in git for instant rollback. This post covers the architecture, the user model, the edge security design, and the gotchas I expect to hit.
The Problem with OAuth2 Proxy
My cluster currently runs OAuth2 Proxy in front of public-facing services. It works. You hit a protected URL, get redirected to Google login, and OAuth2 Proxy validates the token and passes X-Auth-Request-Email downstream. Simple.
But it has limitations that are becoming painful as the cluster grows:
No groups or roles. OAuth2 Proxy is binary – you are authenticated or you are not. I cannot give someone access to Jellyfin but not Radarr. Every authenticated user gets access to everything behind the proxy.
No centralized user management. Adding or removing a user means editing the OAuth2 Proxy config and restarting the pod. There is no admin UI, no user directory, no audit trail.
Internal services are unprotected. OAuth2 Proxy only covers public ingresses. Internal services like Grafana, Wiki.js, and Harbor each have their own auth – separate credentials, separate sessions, no SSO.
No per-app access control. My friends have access to media services, but I do not want them poking around in Radarr or the container registry. Today there is no way to enforce that without separate OAuth2 Proxy instances per service.
Why Authentik
I evaluated Authentik, Keycloak, and Authelia. Authentik won for homelab use because:
| Feature | Authentik | Keycloak | Authelia |
|---|---|---|---|
| Built-in Traefik forwardAuth | Yes (embedded outpost) | No (requires separate proxy) | Yes |
| Admin UI | Modern, clean | Functional but heavy | Minimal config file |
| Resource footprint | ~1.4Gi total | ~2Gi minimum | ~200Mi |
| OIDC + LDAP + SAML | All three | All three | OIDC only |
| Application blueprints | 100+ pre-built | Manual config | N/A |
| Google social login | Native | Native | Not supported |
Keycloak is the enterprise choice, but it is heavy for a homelab. Authelia is lightweight but does not support social login or LDAP, and its configuration is file-based with no admin UI. Authentik hits the sweet spot – real identity provider features without enterprise-scale overhead.
Architecture
Internet → UDM Pro :443 → Traefik LB (192.168.20.200)
│
┌─────────▼──────────────────────────┐
│ Traefik Middlewares │
│ Rate Limit → Sec Headers → fwdAuth │
└─────────┬──────────────────────────┘
│
┌─────────▼──────────────────────────┐
│ Authentik (authentik namespace) │
│ │
│ Server (Django) Worker (Celery) │
│ PostgreSQL 16 Redis │
│ Embedded Proxy Outpost │
└─────────┬──────────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
Public Apps Internal Apps Proxy-Protected
(forwardAuth) (Native OIDC) (forwardAuth)
OpenClaw, HAM Grafana, Wiki.js Radarr, Sonarr
Home Assistant Gitea, Harbor qBittorrent
Jellyseerr Jellyfin (plugin) Prowlarr, Tdarr
All authentication flows through one system. Public apps use Traefik forwardAuth (same pattern as OAuth2 Proxy, just pointing at Authentik’s outpost instead). Internal apps that support OIDC get direct integration. Apps with no OIDC support get wrapped in a proxy provider.
Users and Groups
The Role Model
Three global roles, simple hierarchy:
| Group | Scope | Who |
|---|---|---|
admins | Full control of everything | Me |
writers | Can use and interact with services | Friends with cluster access |
readers | View-only (future use) | Not assigned yet |
Per-App Access Groups
On top of global roles, per-app groups control which services each user can reach:
| App Group | Services | Default Access |
|---|---|---|
app-media-streaming | Jellyfin | All writers + admins |
app-media-requests | Jellyseerr | All writers + admins |
app-media-management | Radarr, Sonarr, Prowlarr, Bazarr, qBit, Tdarr | Admins only |
app-wiki | Wiki.js | All writers + admins |
app-grafana | Grafana | All writers + admins |
app-harbor | Harbor registry | Admins only |
app-dev-tools | Trade Bot, Cardboard, Digital Signage | Admins only |
This is the key improvement over OAuth2 Proxy. My friends can log in via Google and reach Jellyfin and Jellyseerr, but they cannot access Radarr or the container registry. If I want to grant someone access to a new service, I add them to an app group in the Authentik admin UI – no config files, no pod restarts.
Initial Users
Six users, all authenticating via Google social login:
| Name | Role | Notes |
|---|---|---|
| Me | admins | Also has local break-glass akadmin account |
| 5 friends | writers | All app groups by default |
The local akadmin account is a recovery mechanism. If Google federation goes down, I can still administer Authentik and manually issue tokens.
Edge Security
The auth.k3s.zolty.systems endpoint is internet-facing. It gets hardened beyond what internal services need:
Traefik Middleware Chain
# Rate limiting -- 30 req/min sustained, 50 burst
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: auth-rate-limit
spec:
rateLimit:
average: 30
burst: 50
period: 1m
# Security headers -- HSTS, CSP, clickjack prevention
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers
spec:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: "strict-origin-when-cross-origin"
Authentik Native Security
Authentik adds its own layer on top:
- Reputation scoring – IPs and usernames with repeated failures get auto-blocked
- Session management – configurable duration and concurrent session limits
- Token rotation – OIDC refresh tokens auto-rotate
- Audit trail – every auth event logged centrally
NetworkPolicy
The authentik namespace gets locked down with default-deny and explicit allows:
- Ingress only from Traefik (kube-system) and forwardAuth validation requests
- Egress only to CoreDNS, Google OAuth, PostgreSQL, Redis, and Prometheus
- No direct internet access from the server or worker pods
Resource Budget
| Component | CPU | Memory | Storage |
|---|---|---|---|
| Server (Django) | 500m | 512Mi | – |
| Worker (Celery) | 500m | 512Mi | – |
| PostgreSQL 16 | 250m | 256Mi | 5Gi Longhorn PVC |
| Redis | 100m | 128Mi | – |
| Total | 1.35 CPU | ~1.4Gi | 5Gi |
That is roughly 5% of cluster capacity. Acceptable for a service that protects everything else.
The Migration Plan
Phase 1: Replace OAuth2 Proxy
Deploy Authentik, configure Google federation, create all users and groups, set up forwardAuth for public services. Cut over by swapping Traefik middleware annotations from OAuth2 Proxy to Authentik in one commit. Users see the same Google login flow – no UX change. OAuth2 Proxy manifests stay in git for instant rollback.
Phase 2: Internal Services (Native OIDC)
Services with built-in OIDC support get direct integration. Grafana maps Authentik groups to roles (admins → Admin, writers → Editor, readers → Viewer). Wiki.js gets OIDC-based edit permissions. Harbor and Gitea sync users via OIDC. Jellyfin uses the SSO plugin for the web UI, but TV apps keep native auth since Roku and Apple TV cannot do OIDC redirects.
Phase 3: Proxy-Protected Services
Internal services without OIDC (Radarr, Sonarr, qBittorrent, etc.) get wrapped in Authentik proxy providers with forwardAuth. Access policies restrict media management tools to admins only.
Phase 4: Collective Mesh
When the WireGuard mesh network goes live, Authentik becomes the identity layer for the collective. Mesh-specific groups, LDAP outpost for services that need it, and centralized auth event logging across all sites.
Known Gotchas
A few things I expect to trip over based on reading the docs and other people’s deployment notes:
Embedded outpost bootstrap. The outpost needs the server to be healthy first. Expect 1-2 CrashLoopBackOff restarts on initial deploy. Set
initialDelaySeconds: 30on readiness probes.Service selector trap. Authentik’s namespace will have PostgreSQL alongside the server. The Service selector must include
app.kubernetes.io/component: serverto avoid routing requests to the database pod. This is the same bug that hits every namespace with a shared database.Google OAuth redirect URI. Google requires exact redirect URI matches. The Authentik callback format is
/source/slug/<source-slug>/callback/. This must be registered in Google Cloud Console before testing.Header name changes. OAuth2 Proxy sends
X-Auth-Request-Email. Authentik sendsX-authentik-username,X-authentik-groups,X-authentik-email. Every app that reads auth headers needs updating.Cookie domain scope. Current OAuth2 Proxy uses
.k3s.zolty.systemswildcard cookie. Internal services under.k3s.internal.zolty.systemsneed a second cookie domain or a broader wildcard.
What This Does Not Cover
- Machine-to-machine auth. API keys for OpenClaw, GitHub PATs, and Bedrock credentials stay as K8s Secrets. Authentik is for human identity.
- MFA beyond Google. Google’s own MFA covers all users via social login. Adding TOTP on top is possible but overkill for a homelab.
- Database credential rotation. That is a separate problem (External Secrets Operator, maybe).
Why Write About a Plan?
I am publishing this before deploying because planning in public helps me think clearly. Writing down the gotchas section forced me to read the docs more carefully than I would have otherwise. And if someone else is evaluating Authentik for their homelab, seeing the full decision process – not just the happy-path tutorial – is more useful than a “how to install Authentik in 5 minutes” post.