
Authelia SSO with Nginx: Two-Factor Authentication for Internal Apps
- AuthorKacey H.
- Date
- Time28 minutes
Authelia is an open-source authentication and authorization server that adds single sign-on (SSO) and two-factor authentication (2FA) in front of your internal applications. Instead of managing separate logins for Grafana, Proxmox, Portainer, and other admin panels, Authelia provides a unified login portal with TOTP, WebAuthn, or Duo push as a second factor. It integrates with Nginx, Caddy, and Traefik via forward-auth — your reverse proxy asks Authelia "is this user allowed?" before forwarding the request to the backend.
This guide deploys Authelia with Docker Compose, configures file-based users with Argon2id password hashing, sets up TOTP 2FA, and protects multiple backend services through Nginx forward-auth. The result is a production-grade authentication layer for any service that does not have built-in SSO support.
How Forward-Auth Works
- User requests grafana.example.com
- Nginx sends an auth subrequest to Authelia's verify endpoint
- If not authenticated → redirect to Authelia login portal
- User logs in + completes 2FA → Authelia sets a session cookie
- Nginx receives 200 from verify → forwards request to Grafana
Prerequisites
- Linux server with Docker and Docker Compose
- Nginx reverse proxy with TLS (Let's Encrypt)
- Subdomain for Authelia portal (e.g. auth.example.com)
- Services to protect (Grafana, Proxmox, internal tools)
Step 1: Create Directory Structure
sudo mkdir -p /opt/authelia/{config,secrets}
cd /opt/authelia
# Generate secrets:
openssl rand -hex 32 | sudo tee secrets/JWT_SECRET
openssl rand -hex 32 | sudo tee secrets/SESSION_SECRET
openssl rand -hex 32 | sudo tee secrets/STORAGE_ENCRYPTION_KEYStep 2: Create Users Database
# Generate password hash (replace 'yourpassword'):
docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'yourpassword'
# Create /opt/authelia/config/users_database.yml:
sudo tee config/users_database.yml >/dev/null <<'EOF'
users:
admin:
displayname: "Admin User"
password: "$argon2id$v=19$m=65536,t=3,p=4$PASTE_HASH_HERE"
email: admin@example.com
groups:
- admins
- dev
EOFStep 3: Write Authelia Configuration
sudo tee config/configuration.yml >/dev/null <<'EOF'
theme: dark
server:
address: 'tcp://0.0.0.0:9091'
log:
level: info
identity_validation:
reset_password:
jwt_secret: file:///secrets/JWT_SECRET
authentication_backend:
file:
path: /config/users_database.yml
password:
algorithm: argon2
argon2:
variant: argon2id
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16
access_control:
default_policy: deny
rules:
- domain: "grafana.example.com"
policy: two_factor
subject: "group:admins"
- domain: "proxmox.example.com"
policy: two_factor
subject: "group:admins"
- domain: "auth.example.com"
policy: bypass
session:
name: authelia_session
secret: file:///secrets/SESSION_SECRET
expiration: 12h
inactivity: 2h
cookies:
- domain: example.com
authelia_url: https://auth.example.com
default_redirection_url: https://grafana.example.com
storage:
encryption_key: file:///secrets/STORAGE_ENCRYPTION_KEY
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notification.txt
regulation:
max_retries: 5
find_time: 2m
ban_time: 10m
totp:
issuer: example.com
period: 30
skew: 1
EOFStep 4: Docker Compose Stack
sudo tee docker-compose.yml >/dev/null <<'EOF'
services:
authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
volumes:
- ./config:/config
- ./secrets:/secrets:ro
ports:
- "127.0.0.1:9091:9091"
environment:
- TZ=Europe/Berlin
EOF
sudo docker compose up -d
sudo docker compose logs -f autheliaStep 5: Nginx Forward-Auth for Protected Services
# /etc/nginx/snippets/authelia-auth.conf
set $authelia_url http://127.0.0.1:9091;
location /authelia {
internal;
proxy_pass $authelia_url/api/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-Method $request_method;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
}
# Protected service block:
server {
listen 443 ssl http2;
server_name grafana.example.com;
auth_request /authelia;
error_page 401 403 =302 https://auth.example.com/?rd=$scheme://$http_host$request_uri;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}
}Step 6: Nginx Config for Authelia Portal
server {
listen 443 ssl http2;
server_name auth.example.com;
location / {
proxy_pass http://127.0.0.1:9091;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Step 7: Register TOTP Device
Log in at https://auth.example.com. On first login, Authelia prompts you to register a TOTP device. Scan the QR code with Google Authenticator, Authy, or any TOTP app. Store backup codes securely.
Step 8: Test Protected Service Access
# Open in browser:
# https://grafana.example.com → redirects to auth.example.com
# Login + 2FA → redirects back to Grafana
# Verify from CLI:
curl -I https://grafana.example.com
# Should return 302 to auth.example.comAccess Control Policies
- bypass — no authentication required (use for auth portal itself).
- one_factor — password only. Suitable for low-risk internal tools.
- two_factor — password + TOTP/WebAuthn. Use for admin panels.
- deny — block access entirely regardless of authentication.
Production Checklist
- Back up /opt/authelia/config and secrets directory.
- Use two_factor policy for all admin-facing services.
- Configure SMTP notifier instead of filesystem for password reset emails.
- Review access control rules when adding new services.
- Test login flow after every Authelia upgrade.
"SSO is not just convenience — it is a security control that centralizes authentication, enforces 2FA, and gives you one place to revoke access."



