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

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
| Piece | What |
|---|---|
| Container | authelia/authelia on port 9091, behind auth.example.com |
| Users | config/users_database.yml (file-based; LDAP possible) |
| Rules | access_control in config/configuration.yml |
| Nginx | auth_request /authelia in each protected server block |
| Require 2FA | policy two_factor |
| Apps with their own good login | leave 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
- Yes: anything with no login (Homepage, some dashboards), weak login, or admin consoles you'd rather never expose (Portainer, Cockpit, Netdata, Grafana).
- No, or carefully: apps that phone clients talk to with their own tokens (Nextcloud, Jellyfin, Immich, Vaultwarden) — the apps can't pass Authelia's login page. Either leave them out, or add bypass rules for their API paths (resources: ["^/api/.*"]).
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.
