Authelia 2FA SSO for VPS Linux

📅 October 4, 2026 · 🏷️ Security, Authentication, SSO

Install Authelia: 2FA + SSO for VPS on Linux

Authelia is an open-source authentication and authorization server that provides Two-Factor Authentication (2FA) and Single Sign-On (SSO) for all applications running on your VPS. It integrates seamlessly with reverse proxies like Nginx and Traefik.

Instead of each application managing its own login system, Authelia acts as a central security gateway — users log in once and gain access to all permitted services, protected by 2FA to prevent unauthorized access.

Why Use Authelia?

FeatureDetails
Single Sign-OnLog in once, access all services
2FA/MFASupports TOTP (Google Authenticator), WebAuthn, Push notifications
Access ControlFine-grained policies down to path-level
Brute-Force ProtectionAccount lockout after repeated failed logins
Open SourceFree, self-hosted, no monthly fees

Prerequisites

Architecture Overview

We'll deploy Authelia with Docker Compose alongside Redis (for sessions), with Nginx handling the reverse proxy and auth_request module.

VPS
├── Nginx (reverse proxy + auth_request)
│   ├── authelia.example.com → Authelia
│   ├── app1.example.com → App1 (protected)
│   └── app2.example.com → App2 (protected)
└── Docker Compose
    ├── authelia (authentication server)
    └── redis (session storage)

Step 1: Create Directory Structure

mkdir -p ~/authelia/{config,data}
cd ~/authelia

Step 2: Create docker-compose.yml

cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    restart: unless-stopped
    volumes:
      - ./config:/config
      - ./data:/data
    environment:
      - AUTHELIA_JWT_SECRET_FILE=/config/jwt_secret
      - AUTHELIA_SESSION_SECRET_FILE=/config/session_secret
      - AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/config/storage_encryption_key
    ports:
      - "9091:9091"
    depends_on:
      - redis
    networks:
      - authelia_net

  redis:
    image: redis:7-alpine
    container_name: authelia_redis
    restart: unless-stopped
    volumes:
      - redis_data:/data
    networks:
      - authelia_net

volumes:
  redis_data:

networks:
  authelia_net:
    driver: bridge
EOF

Step 3: Generate Secret Files

# Generate random secret keys
openssl rand -hex 64 > ~/authelia/config/jwt_secret
openssl rand -hex 64 > ~/authelia/config/session_secret
openssl rand -hex 64 > ~/authelia/config/storage_encryption_key

chmod 600 ~/authelia/config/*_secret ~/authelia/config/storage_encryption_key

Step 4: Create configuration.yml

cat > ~/authelia/config/configuration.yml << 'EOF'
server:
  host: 0.0.0.0
  port: 9091

log:
  level: info

theme: light

jwt_secret_file: /config/jwt_secret

default_redirection_url: https://example.com

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

authentication_backend:
  file:
    path: /config/users_database.yml
    password:
      algorithm: bcrypt
      iterations: 12

access_control:
  default_policy: deny
  rules:
    # Allow public access to main website
    - domain: "www.example.com"
      policy: bypass
    # Require 2FA for admin tools
    - domain: "*.example.com"
      subject: "group:admins"
      policy: two_factor
    # Require single-factor for regular users
    - domain: "app1.example.com"
      subject: "group:users"
      policy: one_factor

session:
  name: authelia_session
  secret_file: /config/session_secret
  expiration: 1h
  inactivity: 15m
  domain: example.com
  redis:
    host: redis
    port: 6379

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

storage:
  local:
    path: /data/db.sqlite3
  encryption_key_file: /config/storage_encryption_key

notifier:
  filesystem:
    filename: /data/notification.txt
EOF
Note: Replace every instance of example.com with your actual domain. For production, use an SMTP notifier instead of filesystem to receive password reset emails.

Step 5: Create Users Database

# Generate a password hash
docker run --rm authelia/authelia:latest authelia hash-password 'your_secure_password'
# Output example: $argon2id$v=19$m=65536,t=3,p=4$...

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

  alice:
    displayname: "Alice"
    password: "$argon2id$v=19$m=65536,t=3,p=4$ALICE_HASH_HERE"
    email: [email protected]
    groups:
      - users
EOF

Step 6: Configure Nginx

Add the auth_request location block and enable authentication for each virtual host you want to protect.

# /etc/nginx/snippets/authelia.conf
location /internal/authelia/authz {
    internal;
    proxy_pass http://127.0.0.1:9091/api/authz/auth-request;
    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-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
}

# /etc/nginx/sites-available/app1.example.com
server {
    listen 443 ssl http2;
    server_name app1.example.com;

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

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

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

        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# Authelia's own virtual host
server {
    listen 443 ssl http2;
    server_name authelia.example.com;

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

Step 7: Start Authelia

cd ~/authelia
docker compose up -d

# Watch logs
docker compose logs -f authelia

Step 8: Register TOTP (2FA)

On first login, Authelia will prompt you to register a TOTP device. Scan the QR code with Google Authenticator or Authy to complete enrollment.

# Check if a user has registered TOTP
docker exec authelia authelia storage user totp export --config /config/configuration.yml

Verify Authelia Is Working

# Check containers are running
docker compose ps

# Test health endpoint
curl -s http://127.0.0.1:9091/api/health | python3 -m json.tool

# Check Redis connectivity
docker exec authelia_redis redis-cli ping

# Tail Authelia logs in real time
docker compose logs -f authelia --tail=50

# Test auth endpoint (expect 401 - no active session)
curl -I http://127.0.0.1:9091/api/authz/auth-request
Tip: If Authelia fails to start, run docker compose logs authelia. The most common issue is incorrect permissions on config files — run chmod 600 ~/authelia/config/* to fix it.

Common Issues

Error: "level=fatal msg=\"configuration: secrets: error loading secret\""

The secret files have wrong permissions or the Authelia container cannot read them.

# Fix permissions
chmod 600 ~/authelia/config/jwt_secret
chmod 600 ~/authelia/config/session_secret
chmod 600 ~/authelia/config/storage_encryption_key
# Restart the container
docker compose restart authelia

Error: "dial tcp redis:6379: connect: connection refused"

The Redis container isn't ready yet, or the service name in docker-compose.yml doesn't match configuration.yml.

# Check Redis service name in docker-compose.yml
docker compose ps
# The name in session.redis.host must match the service name
# Start Redis first
docker compose up -d redis
sleep 3
docker compose up -d authelia

Error: "auth_request module returned 500"

Nginx cannot reach Authelia, or the proxy_pass URL is incorrect.

# Verify Authelia is bound to port 9091
ss -tlnp | grep 9091
# Test Nginx configuration
nginx -t
# Check Nginx error log
tail -50 /var/log/nginx/error.log

Error: "level=error msg=\"session: error reading session\""

The session secret changed after a restart, invalidating all existing sessions.

# Flush all Redis sessions (users must re-login)
docker exec authelia_redis redis-cli FLUSHDB
docker compose restart authelia

Error: "totp: failed to verify code"

The server clock is more than 30 seconds out of sync with the user's device.

# Check server time
timedatectl status
# Enable NTP sync
timedatectl set-ntp true
# Force immediate sync
chronyc makestep

Frequently Asked Questions

How is Authelia different from Keycloak?

Authelia is lightweight and simple — ideal for resource-constrained VPS instances. It uses a config file instead of a database and provides 2FA + SSO directly through the reverse proxy. Keycloak offers more features (full OAuth2 server, SAML) but requires significantly more RAM (512 MB or more) and has a steeper learning curve.

Can Authelia work with Traefik?

Yes, and Traefik supports Authelia natively via the forwardAuth middleware. Simply add the appropriate labels to each service's docker-compose definition instead of editing configuration files manually — making it easier to manage than the Nginx integration.

If Authelia goes down, can users still access protected services?

No. When Nginx's auth_request cannot reach the upstream Authelia instance, it returns an error. To mitigate this, run Authelia on Docker Swarm or set restart: always with health checks in docker-compose so the container automatically restarts on failure.