Logo

Authelia: SSO and 2FA for self-hosted services

Authelia: SSO and 2FA for self-hosted services

Centralize authentication for all your services (Nginx Proxy Manager, Grafana, Portainer, etc.) behind a single Authelia login with TOTP/WebAuthn 2FA. Open-source Single Sign-On replacing basic HTTP auth with a real security gateway.

Introduction

The more services you host (Portainer, Grafana, Uptime Kuma, NextCloud, Vaultwarden...), the more accounts and passwords pile up. Authelia solves this:

  • Single login for all your services
  • TOTP 2FA (Google Authenticator) or WebAuthn (YubiKey, Touch ID)
  • Per-URL policies (e.g. /admin requires 2FA, /public open)
  • Built-in IP whitelist
  • Built-in brute-force protection

Authelia sits in front of your reverse proxy (Nginx, Traefik, Caddy) and intercepts requests to check authentication.

Prerequisites

  • Debian 12 / Ubuntu 22.04+ VPS
  • Docker + Docker Compose installed
  • A reverse proxy (Nginx or Traefik) already in place
  • A domain name with a subdomain for Authelia (e.g. auth.your-domain.com)

Step 1: Prepare structure

sudo mkdir -p /opt/authelia/{config,secrets}
cd /opt/authelia

Step 2: Generate secrets

Authelia needs several cryptographic secrets. Generate them:

cd /opt/authelia/secrets

# JWT secret (sessions)
openssl rand -base64 64 > JWT_SECRET

# Session key
openssl rand -base64 64 > SESSION_SECRET

# DB encryption key
openssl rand -base64 64 > STORAGE_ENCRYPTION_KEY

# Redis password (used later)
openssl rand -base64 32 > REDIS_PASSWORD

# Permissions
chmod 600 *

Step 3: configuration.yml

sudo nano /opt/authelia/config/configuration.yml

Minimal content:

# Server
server:
  address: 'tcp://0.0.0.0:9091'

# Logs
log:
  level: 'info'

# TOTP
totp:
  issuer: 'verycloud.fr'

# Authentication - using local YAML for demo
authentication_backend:
  file:
    path: '/config/users_database.yml'

# Access Control - per-URL rules
access_control:
  default_policy: 'deny'
  rules:
    # Auth UI itself
    - domain: 'auth.your-domain.com'
      policy: 'bypass'
    
    # Protected services (2FA required)
    - domain: 'grafana.your-domain.com'
      policy: 'two_factor'
    
    - domain: 'portainer.your-domain.com'
      policy: 'two_factor'
    
    # Service where 1FA is enough
    - domain: 'kuma.your-domain.com'
      policy: 'one_factor'

# Sessions (stored in Redis)
session:
  name: 'authelia_session'
  expiration: '1h'
  inactivity: '5m'
  remember_me: '1M'
  cookies:
    - domain: 'your-domain.com'
      authelia_url: 'https://auth.your-domain.com'
  redis:
    host: 'redis'
    port: 6379

# Brute-force protection
regulation:
  max_retries: 3
  find_time: '2m'
  ban_time: '5m'

# Data storage (SQLite for simplicity)
storage:
  local:
    path: '/config/db.sqlite3'

# Notifier (sends codes by email)
notifier:
  filesystem:
    filename: '/config/notifications.txt'
  # For real SMTP:
  # smtp:
  #   host: 'mail.your-domain.com'
  #   port: 587
  #   username: '[email protected]'
  #   password: 'password'
  #   sender: '[email protected]'

Step 4: Create users database

sudo nano /opt/authelia/config/users_database.yml

Format:

users:
  mathys:
    disabled: false
    displayname: "Mathys"
    password: "$argon2id$v=19$m=65536,t=3,p=4$..." # see next step
    email: "[email protected]"
    groups:
      - "admin"
      - "users"

Generate password hash

sudo docker run --rm authelia/authelia:latest authelia crypto hash generate argon2

Enter your password. Copy the resulting hash and paste it in users_database.yml.

Step 5: Docker Compose

sudo nano /opt/authelia/docker-compose.yml
services:
  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    restart: unless-stopped
    volumes:
      - ./config:/config
    environment:
      - TZ=Europe/Paris
      - AUTHELIA_JWT_SECRET_FILE=/secrets/JWT_SECRET
      - AUTHELIA_SESSION_SECRET_FILE=/secrets/SESSION_SECRET
      - AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/secrets/STORAGE_ENCRYPTION_KEY
      - AUTHELIA_SESSION_REDIS_PASSWORD_FILE=/secrets/REDIS_PASSWORD
    secrets:
      - JWT_SECRET
      - SESSION_SECRET
      - STORAGE_ENCRYPTION_KEY
      - REDIS_PASSWORD
    ports:
      - "9091:9091"
    depends_on:
      - redis

  redis:
    image: redis:alpine
    container_name: authelia-redis
    restart: unless-stopped
    command: >
      sh -c 'redis-server --requirepass "$$(cat /run/secrets/REDIS_PASSWORD)"'
    secrets:
      - REDIS_PASSWORD
    volumes:
      - ./redis-data:/data

secrets:
  JWT_SECRET:
    file: ./secrets/JWT_SECRET
  SESSION_SECRET:
    file: ./secrets/SESSION_SECRET
  STORAGE_ENCRYPTION_KEY:
    file: ./secrets/STORAGE_ENCRYPTION_KEY
  REDIS_PASSWORD:
    file: ./secrets/REDIS_PASSWORD

Start:

sudo docker compose up -d
sudo docker compose logs -f authelia

Step 6: Nginx reverse proxy

Authelia integrates via forwardAuth or the Nginx snippet.

sudo nano /etc/nginx/snippets/authelia.conf
# Authorization endpoint
location /authelia {
    internal;
    set $upstream_authelia http://127.0.0.1:9091/api/verify;
    proxy_pass_request_body off;
    proxy_pass $upstream_authelia;
    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;
}

auth.your-domain.com site

sudo nano /etc/nginx/sites-available/authelia
server {
    listen 443 ssl http2;
    server_name auth.your-domain.com;

    ssl_certificate /etc/letsencrypt/live/auth.your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.your-domain.com/privkey.pem;

    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;
    }
}

Protect an existing service (e.g. Grafana)

sudo nano /etc/nginx/sites-available/grafana
server {
    listen 443 ssl http2;
    server_name grafana.your-domain.com;

    ssl_certificate /etc/letsencrypt/live/grafana.your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grafana.your-domain.com/privkey.pem;

    include /etc/nginx/snippets/authelia.conf;

    location / {
        # Authelia check before each request
        auth_request /authelia;
        auth_request_set $target_url $scheme://$http_host$request_uri;
        auth_request_set $user $upstream_http_remote_user;
        auth_request_set $groups $upstream_http_remote_groups;
        error_page 401 =302 https://auth.your-domain.com/?rd=$target_url;

        proxy_set_header Remote-User $user;
        proxy_set_header Remote-Groups $groups;

        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
    }
}

Reload:

sudo nginx -t && sudo systemctl reload nginx

Step 7: Get SSL certificates

sudo certbot --nginx -d auth.your-domain.com -d grafana.your-domain.com

Step 8: First login

Open https://grafana.your-domain.com → you're redirected to Authelia.

Log in with your username/password. Authelia offers to enroll 2FA:

  1. Click Enroll TOTP
  2. Authelia sends an email with a link (or writes to /config/notifications.txt if you kept the filesystem notifier)
  3. Click the link → QR code → scan with your app
  4. Enter the first TOTP code to validate

From now on, you're automatically logged in to Grafana.

Step 9: Add WebAuthn (YubiKey, Touch ID)

Safer than TOTP. In configuration.yml:

webauthn:
  display_name: 'VeryCloud Authelia'
  attestation_conveyance_preference: 'indirect'
  user_verification: 'preferred'
  timeout: '60s'

And create a rule requiring WebAuthn:

access_control:
  rules:
    - domain: 'admin.your-domain.com'
      policy: 'two_factor'
      methods: ['webauthn']

User side, in Authelia → 2FA methods → Enroll security key.

Step 10: LDAP / Active Directory backend (advanced)

To manage dozens of users, replace the file backend with LDAP:

authentication_backend:
  ldap:
    url: 'ldap://ldap.your-domain.com'
    base_dn: 'dc=verycloud,dc=fr'
    username_attribute: 'uid'
    additional_users_dn: 'ou=people'
    users_filter: '(&({username_attribute}={input})(objectClass=person))'
    additional_groups_dn: 'ou=groups'
    groups_filter: '(member={dn})'
    group_name_attribute: 'cn'
    mail_attribute: 'mail'
    user: 'cn=admin,dc=verycloud,dc=fr'
    password: 'admin_password'

Troubleshooting

Authelia redirect loop

Often due to misconfigured session cookies. Check that domain in session.cookies matches your root domain (your-domain.com, not auth.your-domain.com).

"Authentication required" loop

The authelia.conf snippet isn't loaded. Check the include in the service's Nginx config.

TOTP code rejected

VPS clock is out of sync:

sudo systemctl status chrony
sudo chronyc tracking

Permission denied on secrets

sudo chown -R 1000:1000 /opt/authelia/secrets
sudo chmod 600 /opt/authelia/secrets/*

Useful commands

# Authelia status
sudo docker compose ps

# Logs
sudo docker compose logs -f authelia

# Reload config without restart
sudo docker compose kill -s HUP authelia

# Test config
sudo docker compose exec authelia authelia validate-config

# List users
sudo cat /opt/authelia/config/users_database.yml

# Reset a user's 2FA (delete their record)
sudo docker compose exec authelia sqlite3 /config/db.sqlite3 "DELETE FROM totp_configurations WHERE username='mathys';"

Conclusion

Authelia turns your self-hosted infra into a professional environment with SSO and 2FA. Benefits:

  • Single password to remember
  • Mandatory 2FA on critical services
  • Per-rule IP whitelist
  • Complete audit logs

Going further:

  • Migrate to LLDAP (lightweight LDAP) for multi-user management
  • Integrate with Vaultwarden for password storage
  • Add WebAuthn with YubiKey for admin accounts
  • Combine with Cloudflare Zero Trust for layered protection

Resources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members