Authelia SSO with Nginx Proxy Manager

One login, MFA-enforced, in front of every internal app — without standing up a full Keycloak deployment.

What Authelia Gives You

Authelia is a single-binary auth portal that sits between your reverse proxy and your apps. It handles login, password storage (or LDAP), MFA (TOTP, WebAuthn, Duo), and emits an authentication cookie the proxy validates on every subsequent request. For a homelab, it is the sweet spot between "basic auth in nginx" and "full Keycloak/OIDC stack."

Architecture in One Diagram

browser ──► NPM ──(forward-auth)──► Authelia
                                       │
                                       ▼
                            authenticate / require 2FA
                                       │
                ◄──── 200 OK + cookie (or 401/302) ────
                                       │
                              NPM proxies to app

Every request to a protected host hits NPM. NPM does a side request to Authelia's /api/verify endpoint. If the cookie is valid and policy allows, the request passes through. Otherwise the user is redirected to the Authelia portal.

Deploy Authelia + Redis with Compose

services:
  authelia:
    image: authelia/authelia:4.38
    container_name: authelia
    restart: unless-stopped
    volumes:
      - ./authelia:/config
    environment:
      TZ: America/Los_Angeles
    networks:
      - edge
    depends_on:
      - redis
    expose:
      - "9091"

  redis:
    image: redis:7-alpine
    container_name: authelia-redis
    restart: unless-stopped
    volumes:
      - redis_data:/data
    networks:
      - edge

volumes:
  redis_data:

networks:
  edge:
    external: true

Redis stores session data so users do not get logged out every time you restart Authelia. The edge network is shared with NPM (see the Docker Compose guide).

Minimal configuration.yml

Drop this in ./authelia/configuration.yml:

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

log:
  level: info

identity_validation:
  reset_password:
    jwt_secret: <generate-with-openssl-rand-hex-32>

authentication_backend:
  file:
    path: /config/users_database.yml
    password:
      algorithm: argon2

access_control:
  default_policy: deny
  rules:
    - domain: 'auth.example.com'
      policy: bypass
    - domain: 'public.example.com'
      policy: bypass
    - domain: '*.lab.example.com'
      policy: two_factor

session:
  secret: <generate-with-openssl-rand-hex-32>
  cookies:
    - domain: 'example.com'
      authelia_url: 'https://auth.example.com'
      default_redirection_url: 'https://lab.example.com'
  redis:
    host: redis
    port: 6379

storage:
  encryption_key: <generate-with-openssl-rand-hex-32>
  local:
    path: /config/db.sqlite3

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

Generate each secret with openssl rand -hex 32. The filesystem notifier writes "emails" (password resets, 2FA enrollment links) to a local file during setup — replace with SMTP before going to production.

Create a User

Hash a password with the Authelia CLI:

docker run --rm authelia/authelia:4.38 \
  authelia crypto hash generate argon2 --password 'your-strong-password'

Put it in ./authelia/users_database.yml:

users:
  michael:
    disabled: false
    displayname: 'Michael'
    password: '$argon2id$v=19$m=65536,t=3,p=4$...'
    email: '[email protected]'
    groups:
      - admins

Restart Authelia and verify the portal loads at https://auth.example.com.

Wire NPM Forward-Auth

For each protected proxy host in NPM, add a custom location or use the Advanced tab's custom nginx config:

# In the proxy host advanced config:
location /authelia {
    internal;
    proxy_pass http://authelia:9091/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;
}

location / {
    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;
    proxy_set_header Remote-User $user;
    proxy_set_header Remote-Groups $groups;

    error_page 401 =302 https://auth.example.com/?rd=$target_url;

    proxy_pass http://upstream-app:8080;
}

Save it once as an NPM snippet/include and reference it from every protected host so you do not copy-paste twenty times. NPM's UI lets you drop these in the Advanced → Custom Nginx Configuration field.

MFA Enrollment

First time a user logs in, Authelia (with two_factor policy) prompts for TOTP enrollment. The QR code link is sent via the notifier — in our filesystem setup that means tail -f ./authelia/notification.txt. Pair with Aegis, Bitwarden, or 1Password.

Strongly recommend WebAuthn (security keys, platform authenticators) as the primary second factor. Enable in config under webauthn:. TOTP as backup.

LDAP Backend (When File Auth Stops Scaling)

Once you have more than a handful of users, swap the file backend for LDAP (LLDAP is a lightweight option):

authentication_backend:
  ldap:
    implementation: custom
    url: ldap://lldap:3890
    base_dn: dc=example,dc=com
    username_attribute: uid
    users_filter: '(&({username_attribute}={input})(objectClass=person))'
    groups_filter: '(member={dn})'
    user: uid=admin,ou=people,dc=example,dc=com
    password: '<bind-password>'

LLDAP gives you a clean UI for managing users/groups and runs in ~30MB of RAM. Pair with Authelia for the auth UX, LDAP for the directory.

Access Control Patterns

  • Per-app policy: Tighten two_factor to specific domains, leave less-sensitive ones at one_factor.
  • Group-based: Restrict admin UIs to a specific group: subject: 'group:admins'.
  • Network-based bypass: Skip MFA when coming from the lab subnet — useful for kiosks. Add networks: at the rule level.

Operational Notes

  • Backup ./authelia/: users, sessions DB, secrets. Without it, restoring means re-enrolling every user's 2FA.
  • Cookie domain matters: The session cookie applies to example.com and all subdomains. Subdomains across different parent domains need separate cookie configs.
  • Auth portal must be on the same parent domain as protected apps (cookie scope). Most homelabs put Authelia at auth.example.com and apps at *.example.com.
  • Watch for clock drift: TOTP fails silently if the Authelia container clock drifts. Use NTP on the host.

Common Pitfalls

  • Mixed-content redirect loops: Set X-Forwarded-Proto in NPM. Authelia builds redirect URLs from these headers.
  • Apps that do their own auth: Either strip the app's login (some support trusted-header SSO via Remote-User) or accept double login.
  • API access: Forward-auth breaks token-based API calls. Bypass Authelia for /api/* on apps that authenticate via tokens, or use Authelia's session API.
  • Cloudflare Tunnel + Authelia: Both work, but pick one to be the auth gate. Stacking forces two logins. See the Cloudflare Tunnel guide for the alternative.

Validation Checklist

  • curl -I https://auth.example.com serves the Authelia portal
  • Protected host redirects to portal on first visit, sets cookie after login
  • Second visit to a protected host with cookie passes through without re-auth
  • TOTP/WebAuthn enrollment completed for at least one user
  • SMTP notifier configured (filesystem only acceptable during setup)
  • ./authelia/ is in your backup job (see the Backup Strategies guide)

- Crafted by Axiom|Spectre