
As self-hosted infrastructure expands, managing independent user credentials across dozens of containerized services—such as Grafana, Portainer, Proxmox, and internal microservices—rapidly introduces credential sprawl and severe security vulnerabilities. Many homelab and internal applications either lack native Multi-Factor Authentication (MFA), rely on outdated basic authentication, or provide no built-in authorization mechanisms at all.
Authentik is an open-source, enterprise-grade Identity Provider (IdP) that unifies authentication, single sign-on (SSO), and role-based access control. When paired with the modern, high-performance Caddy web server, you can implement robust Forward Authentication. With this pattern, Caddy intercepts incoming HTTP requests, validates user sessions against Authentik before forwarding traffic, and enforces hardware-backed MFA (FIDO2/WebAuthn or TOTP) across all self-hosted web applications—even those with zero native auth capabilities.
In this technical implementation guide, we will deploy a production-ready Authentik stack with PostgreSQL, Redis, and Caddy using Docker Compose, configure Caddy’s forward_auth directive, and secure internal dashboards behind unified identity governance.
Architecture: Forward Authentication Flow
Unlike traditional OpenID Connect (OIDC) or SAML workflows where each application must support federated protocols natively, Forward Auth acts at the reverse proxy layer. Caddy queries Authentik on every request to determine whether the client holds a valid cryptographic session cookie.
+-----------------------------------------------------------------------------+
| Client Browser |
| | |
| | 1. HTTPS Request (e.g. app.lab.lan) |
| v |
| +-----------------------------------------------------------------------+ |
| | Caddy Reverse Proxy (Ports 80 / 443) | |
| | Handles TLS Certificates & Routing | |
| +-----------------------------------+-----------------------------------+ |
| | |
| 2. forward_auth | |
| (Checks Session) v 4. Authorized Traffic |
| +---------------------------------------+ +---------------------------+ |
| | Authentik Embedded Outpost | | Protected Backend App | |
| | (Port 9000 /outpost.goauthentik.io) | | (Portainer, Grafana, etc) | |
| +-------------------+-------------------+ +---------------------------+ |
| | |
| Session Lookup | Valid / Invalid Status |
| v |
| +---------------------------------------+ |
| | Authentik Server & Worker | |
| | + PostgreSQL (Database) | |
| | + Redis (Session & Task Cache) | |
| +---------------------------------------+ |
+-----------------------------------------------------------------------------+
Step-by-Step Traffic Lifecycle
- 1. Request Arrival: The client initiates an HTTPS request to an internal application (e.g.,
https://monitor.example.com). - 2. Forward Auth Interception: Caddy pauses the request and submits a sub-request containing the client’s headers and cookies to Authentik’s outpost endpoint (
/outpost.goauthentik.io/auth/caddy). - 3. Challenge or Validation: If the client lacks an active session, Authentik returns an HTTP
302 Redirectto the Authentik login portal, prompting the user for username, password, and WebAuthn/TOTP. Upon successful challenge completion, a signed session cookie is written. - 4. Upstream Proxying: If the session is valid, Authentik returns an HTTP
200 OKalong with injected identity headers (e.g.,X-authentik-usernameandX-authentik-email). Caddy then forwards the original request to the backend container.
Step 1: Directory Setup & Cryptographic Secrets
Create an isolated directory structure for Authentik state, database storage, and Caddy configuration files:
sudo mkdir -p /opt/authentik/{database,redis,media,templates,certs,caddy-data,caddy-config}
cd /opt/authentik
Generate high-entropy cryptographic keys for Authentik’s internal encryption and PostgreSQL access. We will store these variables in a secure .env file:
cat <<EOF > .env
# Authentik Core Settings
AUTHENTIK_SECRET_KEY=$(openssl rand -base64 36)
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true
# PostgreSQL Credentials
PG_PASS=$(openssl rand -base64 24)
PG_USER=authentik
PG_DB=authentik
# Domain Configuration
AUTHENTIK_DOMAIN=auth.example.com
BASE_DOMAIN=example.com
EOF
sudo chmod 600 .env
Step 2: Production Docker Compose Configuration
Create the docker-compose.yml file. This definition bundles PostgreSQL 16, Redis 7 (alpine), the Authentik Server (API and web frontend), the Authentik Worker (background tasks and directory synchronization), Caddy, and a demonstration backend service (Whoami) to validate Forward Auth enforcement.
services:
postgresql:
image: docker.io/library/postgres:16-alpine
container_name: authentik-postgres
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- /opt/authentik/database:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${PG_PASS}
POSTGRES_USER: ${PG_USER}
POSTGRES_DB: ${PG_DB}
networks:
- authentik-internal
redis:
image: docker.io/library/redis:7-alpine
container_name: authentik-redis
restart: unless-stopped
command: --save 60 1 --loglevel warning
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 10s
timeout: 3s
retries: 5
volumes:
- /opt/authentik/redis:/data
networks:
- authentik-internal
server:
image: ghcr.io/goauthentik/server:2024.8.3
container_name: authentik-server
restart: unless-stopped
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING__ENABLED}
volumes:
- /opt/authentik/media:/media
- /opt/authentik/templates:/templates
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- authentik-internal
- web-proxy
worker:
image: ghcr.io/goauthentik/server:2024.8.3
container_name: authentik-worker
restart: unless-stopped
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
volumes:
- /opt/authentik/media:/media
- /opt/authentik/templates:/templates
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- authentik-internal
caddy:
image: docker.io/library/caddy:2.8-alpine
container_name: caddy-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /opt/authentik/Caddyfile:/etc/caddy/Caddyfile:ro
- /opt/authentik/caddy-data:/data
- /opt/authentik/caddy-config:/config
depends_on:
- server
networks:
- web-proxy
# Test application to verify Forward Auth
demo-app:
image: traefik/whoami:latest
container_name: demo-whoami
restart: unless-stopped
networks:
- web-proxy
networks:
authentik-internal:
internal: true
web-proxy:
driver: bridge
Step 3: Caddyfile Configuration for Forward Authentication
Create the /opt/authentik/Caddyfile. This configuration handles automatic TLS issuance via Let’s Encrypt, passes Authentik’s portal directly, and protects demo.example.com using Caddy’s native forward_auth directive:
{
email admin@example.com
admin off
}
# 1. Authentik Web Portal & Outpost Endpoints
auth.example.com {
reverse_proxy server:9000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
# 2. Protected Application (demo.example.com)
demo.example.com {
# Forward all requests to Authentik Outpost for authorization
forward_auth server:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
}
# Pass traffic to internal upstream once authorized
reverse_proxy demo-app:80
}
Understanding Caddy’s forward_auth Directives
uri /outpost.goauthentik.io/auth/caddy: Specifies the internal sub-request destination where Authentik inspects cookies and authorization tokens.copy_headers: Forwards the authenticated user’s metadata downstream to the protected application. For applications like Grafana, these headers allow automatic user provisioning and role assignment.- Automatic Let’s Encrypt: Caddy automatically requests and renews valid zero-configuration ACME certificates for both
auth.example.comanddemo.example.com.
Step 4: Launching and Initializing Authentik
Start the stack in detached mode:
docker compose up -d
Track the database migrations and server initialization:
docker compose logs -f server worker
Once the logs show Starting authentik server..., navigate to the initial setup URL in your browser:
https://auth.example.com/if/flow/initial-setup/
Set a secure password for the primary administrator account (akadmin). Once authenticated, you will enter the comprehensive Authentik Admin Management Interface.
Step 5: Configuring Applications and Proxy Providers
To enforce Forward Authentication on demo.example.com, we must create a corresponding Provider and Application in Authentik:
1. Create the Proxy Provider
- Navigate to Applications > Providers > Create.
- Select Proxy Provider and click Next.
- Configure the provider fields:
- Name:
Demo App Provider - Authorization flow:
default-provider-authorization-implicit-consent - Forward auth (single application): Checked
- External host:
https://demo.example.com
- Name:
- Click Finish.
2. Create the Application
- Navigate to Applications > Applications > Create.
- Configure the application parameters:
- Name:
Demo Whoami - Slug:
demo-whoami - Provider: Select
Demo App Provider
- Name:
- Click Create.
3. Bind Provider to the Embedded Outpost
Authentik manages Forward Auth endpoints via Outposts. By default, Authentik runs an authentik Embedded Outpost inside the server container:
- Navigate to Applications > Outposts.
- Edit the authentik Embedded Outpost.
- Under Applications, ensure both
authentikandDemo Whoamiare selected. - Click Update.
Step 6: Enforcing Hardware MFA (FIDO2 / WebAuthn & TOTP)
One of Authentik’s strongest capabilities is mandating multi-factor authentication before any access to proxy-protected services is granted:
- Open Flows and Stages > Flows > default-authentication-flow.
- Under Stage Bindings, verify that
default-authentication-mfa-validationis bound following password validation. - In the User Settings menu (top right user profile > User Settings), users can enroll hardware security keys (YubiKey / FIDO2) or authenticator apps (TOTP).
- Any access attempt to
https://demo.example.comwithout an active, MFA-verified session will immediately bounce to the challenge stage.
Troubleshooting Common Forward Auth Issues
Issue 1: Infinite Redirect Loop Between Caddy and Authentik
Symptom: Attempting to access https://demo.example.com results in rapid URL flickering between the app and Authentik, eventually terminating with an HTTP 414 (Request-URI Too Long) or browser redirect limit error.
Root Cause: The session cookie domain is misconfigured, preventing the browser from presenting the authenticated session cookie to the application’s subdomain.
Resolution: In Authentik Admin, open Applications > Providers > edit your Proxy Provider > expand Advanced protocol settings. Ensure the Cookie domain is set to your root parent domain (e.g., example.com with no leading dot or port), allowing cookie inheritance across all subdomains.
Issue 2: Authentik Database Migration Lockout on First Boot
Symptom: The Authentik server container continually restarts or logs django.db.utils.OperationalError: could not connect to server: Connection refused.
Root Cause: Authentik attempted to apply database schemas before the PostgreSQL container completed initialization.
Resolution: Ensure the docker-compose.yml file utilizes formal Docker healthchecks on the postgresql service, and that depends_on in the server service specifies condition: service_healthy.
Issue 3: Upstream Headers Missing or Stripped
Symptom: The application opens, but user authentication variables (e.g., X-Authentik-Username) are absent in the application environment.
Root Cause: The Caddy forward_auth block omitted the copy_headers directive, or the upstream application ignores untrusted incoming proxy headers.
Resolution: Explicitly declare the headers to mirror in your Caddyfile:
forward_auth server:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Email X-Authentik-Name
}
Conclusion: Centralized Zero Trust for Homelabs
Combining Caddy’s lightweight reverse proxy with Authentik’s forward authentication brings enterprise-grade Zero Trust architecture to self-hosted environments. You no longer need to rely on the disparate, unvetted authentication mechanisms of individual container projects. Every HTTP request is strictly authenticated, logged, and shielded with hardware-backed Multi-Factor Authentication before it ever reaches your application backends.
For applications that support standard OpenID Connect (such as Nextcloud, Proxmox, and Grafana), you can easily transition from simple Forward Auth to native OIDC providers inside Authentik, enabling seamless single-click login with granular user claims and group synchronization.
Hi, I’m Mark, the author of Clever IT Solutions: Mastering Technology for Success. I am passionate about empowering individuals to navigate the ever-changing world of information technology. With years of experience in the industry, I have honed my skills and knowledge to share with you. At Clever IT Solutions, we are dedicated to teaching you how to tackle any IT challenge, helping you stay ahead in today’s digital world. From troubleshooting common issues to mastering complex technologies, I am here to guide you every step of the way. Join me on this journey as we unlock the secrets to IT success.


