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:

  1. 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.

  2. 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.

  3. 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.

  4. 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:

FeatureAuthentikKeycloakAuthelia
Built-in Traefik forwardAuthYes (embedded outpost)No (requires separate proxy)Yes
Admin UIModern, cleanFunctional but heavyMinimal config file
Resource footprint~1.4Gi total~2Gi minimum~200Mi
OIDC + LDAP + SAMLAll threeAll threeOIDC only
Application blueprints100+ pre-builtManual configN/A
Google social loginNativeNativeNot 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:

GroupScopeWho
adminsFull control of everythingMe
writersCan use and interact with servicesFriends with cluster access
readersView-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 GroupServicesDefault Access
app-media-streamingJellyfinAll writers + admins
app-media-requestsJellyseerrAll writers + admins
app-media-managementRadarr, Sonarr, Prowlarr, Bazarr, qBit, TdarrAdmins only
app-wikiWiki.jsAll writers + admins
app-grafanaGrafanaAll writers + admins
app-harborHarbor registryAdmins only
app-dev-toolsTrade Bot, Cardboard, Digital SignageAdmins 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:

NameRoleNotes
MeadminsAlso has local break-glass akadmin account
5 friendswritersAll 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

ComponentCPUMemoryStorage
Server (Django)500m512Mi
Worker (Celery)500m512Mi
PostgreSQL 16250m256Mi5Gi Longhorn PVC
Redis100m128Mi
Total1.35 CPU~1.4Gi5Gi

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:

  1. Embedded outpost bootstrap. The outpost needs the server to be healthy first. Expect 1-2 CrashLoopBackOff restarts on initial deploy. Set initialDelaySeconds: 30 on readiness probes.

  2. Service selector trap. Authentik’s namespace will have PostgreSQL alongside the server. The Service selector must include app.kubernetes.io/component: server to avoid routing requests to the database pod. This is the same bug that hits every namespace with a shared database.

  3. 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.

  4. Header name changes. OAuth2 Proxy sends X-Auth-Request-Email. Authentik sends X-authentik-username, X-authentik-groups, X-authentik-email. Every app that reads auth headers needs updating.

  5. Cookie domain scope. Current OAuth2 Proxy uses .k3s.zolty.systems wildcard cookie. Internal services under .k3s.internal.zolty.systems need 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.