How to Add Single Sign-On to Your Apps with Authelia (One Login, 2FA, Behind Nginx)

Authelia: one login and 2FA for every app — Nginx asks before it serves. Log in once.

Every app behind your reverse proxy has its own login, some of them weak, some of them none at all (a Grafana you forgot to password, Homepage, a dev tool). Authelia sits beside Nginx: before a request reaches an app, Nginx asks Authelia "is this person logged in and allowed?" If not, they get Authelia's login page — username, password, and a TOTP code or security key — and once they're in, every protected app opens without another prompt. One account, one 2FA, every app.

Quick reference

PieceWhat
Containerauthelia/authelia on port 9091, behind auth.example.com
Usersconfig/users_database.yml (file-based; LDAP possible)
Rulesaccess_control in config/configuration.yml
Nginxauth_request /authelia in each protected server block
Require 2FApolicy two_factor
Apps with their own good loginleave them out, or use bypass for their API paths

1. The compose file

mkdir -p ~/docker/authelia/config && cd ~/docker/authelia
nano compose.yaml

services:
authelia:
image: authelia/authelia:latest
container_name: authelia
ports:
- "127.0.0.1:9091:9091"
environment:
TZ: Europe/London
AUTHELIA_JWT_SECRET: ${JWT_SECRET}
AUTHELIA_SESSION_SECRET: ${SESSION_SECRET}
AUTHELIA_STORAGE_ENCRYPTION_KEY: ${STORAGE_KEY}
volumes:
- ./config:/config
restart: unless-stopped

.env with three long random strings (openssl rand -hex 32 each).

2. Configuration

nano config/configuration.yml

server:
address: tcp://0.0.0.0:9091

log:
level: info

totp:
issuer: example.com

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

access_control:
default_policy: deny
rules:
- domain: "auth.example.com"
policy: bypass
- domain: ["grafana.example.com", "home.example.com", "status.example.com"]
policy: two_factor
- domain: "*.example.com"
policy: one_factor

session:
name: authelia_session
cookies:
- domain: example.com
authelia_url: https://auth.example.com
default_redirection_url: https://home.example.com

storage:
local:
path: /config/db.sqlite3

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

The notifier writes 2FA-enrolment links to a file instead of emailing them; swap for smtp: once msmtp-style details are to hand (tutorial 49).

3. Users

docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'your-password'

nano config/users_database.yml

users:
john:
displayname: "John"
password: "$argon2id$v=19$m=65536,t=3,p=4$...paste the hash..."
email: john@example.com
groups: [admins]

docker compose up -d
docker compose logs -f authelia

4. Nginx: the auth portal

sudo nano /etc/nginx/sites-available/auth.example.com

server {
listen 80;
server_name auth.example.com;
location / {
proxy_pass http://127.0.0.1:9091;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
}

Enable it, certbot --nginx -d auth.example.com.

5. Nginx: protect an app

Create a reusable snippet:

sudo nano /etc/nginx/snippets/authelia.conf

location /authelia {
internal;
proxy_pass http://127.0.0.1:9091/api/authz/forward-auth;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}

Then in any app's server block, inside location /:

include snippets/authelia.conf; # (put this at server level, above location /)
location / {
auth_request /authelia;
auth_request_set $redirect $upstream_http_location;
error_page 401 =302 https://auth.example.com/?rd=$scheme://$http_host$request_uri;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header Remote-User $user;
proxy_pass http://127.0.0.1:3030;
# ...the usual headers...
}

nginx -t && systemctl reload nginx. Open https://grafana.example.com: you're sent to Authelia, log in, enrol 2FA (the link is in config/notification.txt), scan the QR, enter the code, and you're bounced back to Grafana. Open Homepage next — no prompt; the session cookie covers *.example.com.

6. What to protect

Troubleshooting

Redirect loop between the app and auth. Cookie domain doesn't match (example.com must be the parent of both hosts), or the app is on plain http — everything must be https.

"Authelia is not configured" / 500. configuration.yml has an error; the log names the line.

2FA link never appears. It's in config/notification.txt with the filesystem notifier.

Works in the browser, phone app can't connect. Expected — that app needs a bypass rule or to be unprotected.

Wrapping up

One container, a config with rules per domain, a user with a hashed password, two Nginx snippets. Every admin page and loginless app now sits behind one login and one authenticator code, and you stop wondering whether that dashboard you set up last year was ever actually protected.

Call us