The most annoying symptom of a growing homelab or small enterprise is identity fragmentation. You start with three containers, and a year later you are running fifteen distinct services—Grafana, Zabbix, Proxmox, TrueNAS, Wazuh, Portainer, Dockge, and internal Wikis. Suddenly, you have fifteen separate login prompts, fifteen sets of credentials to rotate, and half the tools lack native two-factor authentication. I solved this by deploying a centralized Identity Provider (IdP) using Authentik and hardware WebAuthn passkeys.
While enterprise teams often default to Microsoft Entra ID or bloated Java behemoths like Keycloak, Authentik is the sweet spot for modern self-hosters and agile IT shops. It delivers full OpenID Connect (OIDC), OAuth 2.0 with PKCE, SAML 2.0, and reverse-proxy Forward Authentication in a clean, resource-friendly footprint. Here is how I set up Authentik on docker02, enforced physical YubiKey authentication, and wired it into our production monitoring stack.
1. Why Authentik Over Keycloak and Authelia?
When selecting a single sign-on engine, three options usually dominate the conversation:
- Keycloak: Highly powerful, but heavy. It requires an extensive Java runtime that idles at 1.5GB to 2GB of RAM and has a steep, Byzantine configuration curve.
- Authelia: Fast and lightweight, but historically focused primarily on reverse-proxy forward authentication rather than full-featured OIDC provider flows with self-service user portals.
- Authentik: Built with a Python/Go architecture. It idles under 500MB of RAM, offers a beautiful management UI, supports conditional stage execution (e.g. prompt for MFA only when coming from untrusted subnets), and handles both native OIDC and forward auth out of the box.
2. Production Docker Compose Setup
Authentik runs containerized on docker02 within our management stack. It requires PostgreSQL 16 for user state and Redis for caching and background celery tasks:
# /opt/stacks/authentik/docker-compose.yml excerpt
version: '3.4'
services:
postgresql:
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
volumes:
- database:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD:
POSTGRES_USER:
POSTGRES_DB:
redis:
image: docker.io/library/redis:alpine
restart: unless-stopped
command: --save 60 1 --loglevel warning
volumes:
- redis:/data
server:
image: ghcr.io/goauthentik/server:2024.8.2
restart: unless-stopped
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER:
AUTHENTIK_POSTGRESQL__NAME:
AUTHENTIK_POSTGRESQL__PASSWORD:
AUTHENTIK_SECRET_KEY:
ports:
- "9000:9000"
- "9443:9443"
worker:
image: ghcr.io/goauthentik/server:2024.8.2
restart: unless-stopped
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER:
AUTHENTIK_POSTGRESQL__NAME:
AUTHENTIK_POSTGRESQL__PASSWORD:
AUTHENTIK_SECRET_KEY:
volumes:
database:
redis:
Our edge Caddy reverse proxy publishes Authentik at https://auth.ittechstl.com with automated internal wildcard certificates, making the SSO portal accessible across all 10 internal VLANs.
3. Enforcing Hardware Passkeys (WebAuthn / FIDO2)
The Zero-Trust Standard: Passwords can be phished, and SMS/TOTP codes can be intercepted or bypassed with adversary-in-the-middle (AiTM) proxies. Hardware security keys (YubiKey / FIDO2) cryptographically bind the authentication handshake to the exact domain name in the browser bar.
In Authentik, authentication flows are built using modular Stages. I customized our primary authentication flow to enforce physical hardware tokens:
- Identification Stage: User enters their username or primary email.
- Password Stage: User inputs their master passphrase.
- MFA Validation Stage: Authentik prompts for a WebAuthn device. I tap my physical YubiKey 5 NFC (or use Windows Hello biometric passkeys). If no hardware key is registered, access to privileged administrative groups is denied.
4. Connecting Services via OpenID Connect (The Grafana Example)
Once Authentik is running, hooking up modern applications takes under five minutes. For example, our Grafana Syslog & Telemetry dashboard uses native OIDC with role-mapping:
# Grafana SSO Environment Variables
GF_AUTH_GENERIC_OAUTH_ENABLED=true
GF_AUTH_GENERIC_OAUTH_NAME="IT Tech STL Sign-In"
GF_AUTH_GENERIC_OAUTH_CLIENT_ID=
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=
GF_AUTH_GENERIC_OAUTH_SCOPES=openid email profile
GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.ittechstl.com/application/o/authorize/
GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.ittechstl.com/application/o/token/
GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.ittechstl.com/application/o/userinfo/
GF_AUTH_GENERIC_OAUTH_USE_PKCE=true
GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH="contains(groups[*], 'authentik Admins') && 'Admin' || 'Editor'"
Notice the GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH expression. This allows Authentik to pass the user’s group memberships directly in the ID token. If I am in the authentik Admins group, Grafana automatically provisions me as a full Administrator. If an intern or guest logs in, they get a read-only Editor role without any manual intervention.
5. Securing Legacy Apps with Caddy Forward Auth
What about older tools or internal dashboards that don’t support modern OIDC authentication? Authentik provides an Outpost architecture that acts as a gatekeeper in front of Caddy:
# Caddyfile Forward Authentication snippet
tool.ittechstl.local {
# Forward unauthenticated requests to Authentik
forward_auth 10.1.20.19:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Email
}
reverse_proxy 10.1.20.19:8080
}
Whenever anyone visits tool.ittechstl.local, Caddy pauses the request, checks Authentik to verify an active session with valid MFA, and only proxies the traffic if authenticated. If no session exists, Caddy redirects the browser to the central login screen.
6. The Verdict: Seamless Operational Hygiene
Centralizing authentication through Authentik has eliminated hours of credential wrangling. When I hire a contractor or invite a collaborator to the lab, I create one single user account, assign their security group, and they instantly have single-sign-on access to exactly what they need—and when they finish, revoking that single account immediately severs access across all fifteen services.