Implementing Zero-Trust Security with Authelia Behind a Reverse Proxy

Implementing Zero-Trust Security with Authelia Behind a Reverse Proxy

I've been running self-hosted services for years, and the moment I added Authelia to my stack was transformative. Before that, I relied on obscure ports and IP allowlists—which felt secure but were fragile. Now? Every request to every internal service is authenticated. Every session is verified. That's the zero-trust model, and it's easier to implement than you might think.

This guide walks you through deploying Authelia behind a reverse proxy (I'll show both Caddy and Nginx examples), configuring multi-factor authentication, and securing your entire homelab with minimal effort. By the end, you'll have a bulletproof authentication layer that works whether you're accessing Nextcloud from your phone or monitoring Uptime Kuma from a random coffee shop.

Why Zero-Trust Matters for Self-Hosters

Traditional network security relies on a "trust but verify" model—your internal network is assumed safe, and external access is restricted. That works until someone on your WiFi isn't who you think they are, or an attacker pivots from a compromised service to another running on the same machine.

Zero-trust flips this: nothing is trusted by default, regardless of where the request comes from. Every access requires authentication. Every session can be revoked. This is especially critical when you're running services like Nextcloud, Vaultwarden, or Jellyfin across a VPS or your home network.

Authelia is the perfect tool for this because it acts as a central authentication and authorization gateway. It sits in front of your reverse proxy and intercepts every request, checking credentials before allowing access to your applications.

Architecture: How Authelia Fits Into Your Stack

Here's the flow I use:

  1. UserReverse Proxy (Caddy or Nginx) → Authelia (auth check) → Backend Service (Nextcloud, Jellyfin, etc.)

The reverse proxy receives the request, forwards it to Authelia for authentication, and only passes it through if Authelia gives the green light. Sessions are stored in Redis, and multi-factor authentication (TOTP, WebAuthn) is optional but highly recommended.

I prefer this architecture over embedding authentication in each application because it's centralized, consistent, and I can rotate auth policies without touching my actual services.

Prerequisites

Step 1: Deploy Authelia with Docker Compose

I'm using Docker Compose because it handles all the dependencies in one file. Here's my complete setup:

version: '3.8'

services:
  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    ports:
      - "9091:9091"
    environment:
      TZ: America/Denver
    volumes:
      - /path/to/config/authelia:/config
      - /path/to/config/authelia/db.sqlite3:/config/db.sqlite3
    networks:
      - proxy
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: authelia-redis
    command: redis-server --requirepass your_redis_password --appendonly yes
    volumes:
      - redis-data:/data
    networks:
      - proxy
    restart: unless-stopped

networks:
  proxy:
    external: true

volumes:
  redis-data:

Create the external network first:

docker network create proxy

Then create your config directory and save this as docker-compose.yml. The key volumes are /path/to/config/authelia—change this to your actual path.

Step 2: Create the Authelia Configuration File

Create /path/to/config/authelia/configuration.yml. This is where the magic happens:

---
jwt_secret: use_a_random_64_char_string_here
default_redirection_url: https://example.com
default_2fa_method: totp

server:
  host: 0.0.0.0
  port: 9091
  path_prefix: /authelia/

session:
  secret: another_random_64_char_string_here
  name: authelia_session
  domain: example.com
  lifetime: 1h
  inactivity: 15m
  remember_me: 1M
  redis:
    host: redis
    port: 6379
    password: your_redis_password
    database_index: 0

authentication_backend:
  file:
    path: /config/users_database.yml

access_control:
  default_policy: deny
  rules:
    - domain: "*.example.com"
      policy: two_factor
    - domain: "admin.example.com"
      policy: two_factor
      subject: "group:admins"

regulation:
  max_retries: 3
  find_time: 10m
  ban_time: 30m

notifier:
  filesystem:
    filename: /config/notification.txt

password_policy:
  standard:
    enabled: true
    min_length: 8
    require_uppercase: true
    require_lowercase: true
    require_numbers: true
    require_special: true
Watch out: Generate your JWT and session secrets using openssl rand -base64 32. Never use placeholder values in production. Authelia will fail silently if these are too short or insecure.

Now create /path/to/config/authelia/users_database.yml with your user accounts:

users:
  admin:
    displayname: "Admin User"
    password: "$argon2id$v=19$m=65540,t=3,p=4$your_hashed_password_here"
    email: [email protected]
    groups:
      - admins
  
  user:
    displayname: "Regular User"
    password: "$argon2id$v=19$m=65540,t=3,p=4$user_hashed_password_here"
    email: [email protected]
    groups:
      - users

Generate password hashes using Authelia's CLI (or use an online Argon2 generator, though I don't recommend sharing passwords online). Once your container is running, you can use:

docker exec authelia authelia crypto hash generate argon2 --password "your_plain_password"

Copy the hash output into the users file.

Step 3: Configure Your Reverse Proxy

This is where your reverse proxy (Caddy or Nginx) sends requests to Authelia for verification. I prefer Caddy because the syntax is cleaner:

Caddy example (Caddyfile):

nextcloud.example.com {
  forward_auth localhost:9091 {
    uri /authelia/api/verify?rd=https://auth.example.com
    copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
  }
  reverse_proxy localhost:8080
}

jellyfin.example.com {
  forward_auth localhost:9091 {
    uri /authelia/api/verify?rd=https://auth.example.com
    copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
  }
  reverse_proxy localhost:8096
}

auth.example.com {
  reverse_proxy localhost:9091
}

Nginx example (simplified):

upstream authelia {
  server localhost:9091;
}

server {
  listen 80;
  server_name nextcloud.example.com;

  location / {
    auth_request /authelia/api/verify;
    auth_request_set $user $upstream_http_remote_user;
    auth_request_set $groups $upstream_http_remote_groups;
    
    proxy_pass http://localhost:8080;
    proxy_set_header Remote-User $user;
    proxy_set_header Remote-Groups $groups;
  }

  location /authelia {
    proxy_pass http://authelia;
  }
}
Tip: The `forward_auth` directive in Caddy (or `auth_request` in Nginx) is the key. It tells your reverse proxy to check with Authelia before forwarding traffic. If Authelia responds with 401, the request is redirected to the login page.

Step 4: Start Everything and Test

Start your Authelia stack:

cd /path/to/config && docker-compose up -d

Check the logs:

docker-compose logs -f authelia

Visit `https://nextcloud.example.com`. You should be redirected to the Authelia login page. Log in with the credentials you set in `users_database.yml`.

If you configured TOTP (two-factor authentication), Authelia will prompt you to scan a QR code with Google Authenticator or Authy. Save those codes somewhere safe.

Step 5: Fine-Tune Access Control

Authelia's access control is powerful. Update your `configuration.yml` to be more granular:

access_control:
  default_policy: deny
  rules:
    # Admin only: Vaultwarden admin panel
    - domain: "vault.example.com"
      path: "/admin"
      policy: two_factor
      subject: "group:admins"

    # Regular users: Nextcloud
    - domain: "nextcloud.example.com"
      policy: two_factor
      subject: "group:users"

    # Public but requires MFA: Jellyfin
    - domain: "jellyfin.example.com"
      policy: two_factor

    # Deny everything else
    - policy: deny

After updating, restart Authelia:

docker-compose restart authelia

Session Management and Logout

One thing I love about Authelia is session control. Users can log out by visiting `https://auth.example.com/logout`. Sessions are stored in Redis with automatic expiration (I set mine to 1 hour of inactivity, 1 month remember-me).

If you need to force logout all users (e.g., after a security incident), flush Redis:

docker exec authelia-redis redis-cli -a your_redis_password FLUSHDB

Monitoring and Troubleshooting

Check Authelia logs for auth failures:

docker-compose logs authelia | grep "authentication failed"

If users are getting 401 errors randomly, it's usually session expiration or Redis connection issues. Verify Redis is running:

docker exec authelia-redis redis-cli -a your_redis_password PING

Should respond with `PONG`.

Scaling Across Multiple Services

The beauty of this setup is scalability. Want to add Vaultwarden, Uptime Kuma, and Immich? Just add more rules to your reverse proxy's forward_auth config. They all use the same Authelia instance, the same user database, the same MFA settings.

I'm currently running 12 services behind one Authelia instance with negligible overhead.

Next Steps

From here, I recommend:

Zero-trust security doesn't have to be complex. Authelia makes it accessible for anyone running self-hosted infrastructure. Once you've got it working, you'll sleep better knowing every request to your services is authenticated and logged.

Discussion