Authelia Two-Factor Authentication Gateway

Authelia is a self-hosted authentication and authorization server that adds two-factor authentication (2FA) with TOTP and WebAuthn to any application behind your reverse proxy, without requiring changes to the application itself. This guide covers deploying Authelia, integrating with Nginx, configuring TOTP and WebAuthn 2FA, and setting up access control rules for fine-grained authorization.

Prerequisites

  • Docker and Docker Compose installed
  • Nginx or Traefik as your reverse proxy
  • A domain name with wildcard or multiple subdomains
  • SSL certificates for your domains
  • SMTP server for email notifications (optional but recommended)

Installing Authelia with Docker

mkdir -p ~/authelia/config && cd ~/authelia

cat > docker-compose.yml << 'EOF'
services:
  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    volumes:
      - ./config:/config
    ports:
      - "9091:9091"
    environment:
      TZ: UTC
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: authelia-redis
    volumes:
      - redis-data:/data
    command: redis-server --requirepass redis_password
    restart: unless-stopped

volumes:
  redis-data:
EOF

Core Configuration

cat > ~/authelia/config/configuration.yml << 'EOF'
---
theme: light
jwt_secret: a_very_long_jwt_secret_at_least_20_chars

default_redirection_url: https://auth.example.com

server:
  host: 0.0.0.0
  port: 9091

log:
  level: info

totp:
  issuer: authelia.com
  period: 30
  skew: 1

webauthn:
  timeout: 60s
  display_name: Example Corp
  attestation_conveyance_preference: indirect
  user_verification: preferred

authentication_backend:
  # Use file-based users (simple setup)
  file:
    path: /config/users.yml
    password:
      algorithm: argon2id
      iterations: 1
      key_length: 32
      salt_length: 16
      memory: 64
      parallelism: 8

  # Or use LDAP:
  # ldap:
  #   url: ldaps://ldap.example.com
  #   base_dn: dc=example,dc=com
  #   username_attribute: uid
  #   users_filter: (&({username_attribute}={input})(objectClass=inetOrgPerson))
  #   groups_filter: (&(member=uid={input},ou=users,dc=example,dc=com)(objectClass=groupOfNames))
  #   user: cn=readonly,dc=example,dc=com
  #   password: readonly_password

access_control:
  default_policy: deny

  rules:
    - domain: auth.example.com
      policy: bypass

    - domain: public.example.com
      policy: bypass

    - domain: "*.example.com"
      policy: two_factor

session:
  name: authelia_session
  secret: a_very_secret_session_key_change_this
  expiration: 1h
  inactivity: 5m
  domain: example.com  # Must be the root domain
  redis:
    host: redis
    port: 6379
    password: redis_password

regulation:
  max_retries: 3
  find_time: 2m
  ban_time: 5m

storage:
  local:
    path: /config/db.sqlite3
  # For production, use MySQL or PostgreSQL:
  # mysql:
  #   host: mysql
  #   port: 3306
  #   database: authelia
  #   username: authelia
  #   password: db_password

notifier:
  # For production, configure SMTP:
  # smtp:
  #   host: smtp.example.com
  #   port: 587
  #   username: [email protected]
  #   password: smtp_password
  #   sender: "Authelia <[email protected]>"
  #   tls:
  #     skip_verify: false

  # For testing — writes to file instead of email
  filesystem:
    filename: /config/notification.txt
EOF

Create the users file:

# Generate a password hash
docker run --rm authelia/authelia:latest \
  authelia crypto hash generate argon2 --password 'your_password'
# Copy the output hash (starts with $argon2id$...)

cat > ~/authelia/config/users.yml << 'EOF'
users:
  admin:
    displayname: Administrator
    password: "$argon2id$v=19$m=65536,t=1,p=8$HASH_OUTPUT_HERE"
    email: [email protected]
    groups:
      - admins
      - users

  jsmith:
    displayname: John Smith
    password: "$argon2id$v=19$m=65536,t=1,p=8$ANOTHER_HASH"
    email: [email protected]
    groups:
      - users
EOF

# Start Authelia
cd ~/authelia
docker compose up -d
docker compose logs -f authelia

Nginx Integration

The Nginx integration uses auth_request to verify sessions with Authelia:

# Create a shared Authelia snippet: /etc/nginx/snippets/authelia.conf
sudo tee /etc/nginx/snippets/authelia.conf << 'EOF'
# Authelia auth endpoint
location /authelia {
    internal;
    set $upstream_authelia http://127.0.0.1:9091/api/authz/auth-request;
    proxy_pass $upstream_authelia;

    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 Accept-Encoding "";
}
EOF

# Authelia portal — accessible at auth.example.com
sudo tee /etc/nginx/sites-available/authelia << 'EOF'
server {
    listen 443 ssl;
    server_name auth.example.com;

    ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.example.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;
    }
}
EOF

# Protected application behind Authelia
sudo tee /etc/nginx/sites-available/grafana << 'EOF'
server {
    listen 443 ssl;
    server_name grafana.example.com;

    ssl_certificate /etc/letsencrypt/live/grafana.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grafana.example.com/privkey.pem;

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

    location / {
        auth_request /authelia;
        auth_request_set $target_url $scheme://$http_host$request_uri;
        error_page 401 =302 https://auth.example.com/?rd=$target_url;

        # Pass auth headers to upstream
        auth_request_set $user $upstream_http_remote_user;
        auth_request_set $groups $upstream_http_remote_groups;
        auth_request_set $name $upstream_http_remote_name;
        proxy_set_header Remote-User $user;
        proxy_set_header Remote-Groups $groups;
        proxy_set_header Remote-Name $name;

        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
EOF

Traefik Integration

For Traefik, use Authelia as a ForwardAuth middleware:

# In traefik's dynamic configuration
http:
  middlewares:
    authelia:
      forwardAuth:
        address: "http://authelia:9091/api/authz/forward-auth"
        trustForwardHeader: true
        authResponseHeaders:
          - "Remote-User"
          - "Remote-Groups"
          - "Remote-Name"
          - "Remote-Email"

  routers:
    grafana:
      rule: "Host(`grafana.example.com`)"
      middlewares:
        - authelia
      service: grafana

Configuring 2FA Methods

TOTP (Time-Based One-Time Password)

After logging in for the first time, Authelia prompts users to enroll a TOTP device:

  1. Navigate to any protected resource
  2. Log in with username and password
  3. Authelia redirects to the 2FA enrollment page
  4. Scan the QR code with Google Authenticator, Authy, or any TOTP app
  5. Enter a verification code to confirm enrollment

WebAuthn (Hardware Keys / Passkeys)

  1. After first login, go to https://auth.example.com/
  2. Click Register Security Key
  3. Insert a YubiKey or use the browser's built-in passkey support
  4. Follow the browser prompts to register the authenticator

Email OTP (One-Time Password via Email)

# In configuration.yml
identity_validation:
  reset_password:
    jwt_lifespan: 5m
    jwt_algorithm: HS256

# Requires SMTP notifier configured:
notifier:
  smtp:
    host: smtp.example.com
    port: 587
    username: [email protected]
    password: smtp_password
    sender: "Authelia <[email protected]>"

Access Control Rules

Access control rules are evaluated top to bottom, first match wins:

access_control:
  default_policy: deny

  rules:
    # Allow public assets without any auth
    - domain: www.example.com
      resources:
        - "^/public/.*$"
        - "^/health$"
      policy: bypass

    # Admin area requires 2FA and admin group
    - domain: admin.example.com
      subject:
        - "group:admins"
      policy: two_factor

    # Internal tools require 2FA for users group
    - domain:
        - grafana.example.com
        - prometheus.example.com
        - kibana.example.com
      subject:
        - "group:users"
        - "group:admins"
      policy: two_factor

    # API endpoints require only 1FA (token-based apps)
    - domain: api.example.com
      resources:
        - "^/v1/.*$"
      policy: one_factor

    # Block everyone else from all other subdomains
    - domain: "*.example.com"
      policy: deny

Session and Storage Configuration

For production deployments with multiple servers:

session:
  name: authelia_session
  secret: very_long_secret_here
  expiration: 8h
  inactivity: 30m
  domain: example.com
  redis:
    host: redis-server
    port: 6379
    password: redis_password
    database_index: 0

storage:
  mysql:
    host: mysql-server
    port: 3306
    database: authelia
    username: authelia
    password: db_password

Troubleshooting

"Authentication failed" with correct credentials

# Check Authelia logs
docker logs authelia --tail 50

# Verify password hash is correct
docker run --rm authelia/authelia:latest \
  authelia crypto hash validate --password 'your_password' \
  '$argon2id$v=19$m=65536...'

Redirect loop between Nginx and Authelia

# Verify auth.example.com has policy: bypass in access_control
# Authelia portal must not require authentication to access itself

# Check that cookies are set for the correct domain
# domain: example.com (not auth.example.com)

2FA enrollment page never appears

# User may already have 2FA enrolled — reset it in users.yml or via admin API
docker exec authelia authelia users storage -u admin reset-totp --config /config/configuration.yml

TOTP codes always rejected

# Check server time sync (TOTP requires accurate time)
timedatectl status
sudo chronyc -a makestep

Conclusion

Authelia provides a self-hosted, privacy-respecting 2FA authentication gateway that integrates with Nginx and Traefik to protect any web application without code changes. Its flexible access control rules, support for TOTP and WebAuthn, and Redis-backed sessions make it a production-ready authentication layer for securing internal tools and sensitive applications behind your reverse proxy.