Authelia puts one login screen, one session cookie and a second factor in front of every web app you host. The current stable release is Authelia 4.39.28, which the project publishes under the Apache 2.0 license, and this tutorial wires it to nginx with Docker Compose in 13 steps. You finish with a working lab: a login portal, two protected demo apps, TOTP and passkey enrollment, per-domain access rules, and lockout protection.
Plan on about 60 minutes. If you already run a password manager on the same box, such as the setup in our self-hosted Bitwarden guide, you know the pattern: containers, a reverse proxy, TLS, and a strict habit of pinning image tags. Authelia follows the same rhythm, with one extra idea. The proxy asks Authelia whether each request may pass before it ever touches your app.
Run everything here only on machines and domains you own. The lab uses a private test domain, so nothing in it touches the public internet.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why Put Authelia in Front of Your Apps in 2026
Most self-hosted tools ship with a username and password form and little else. Some have no login at all. A dashboard, a media server, a wiki and a metrics page each end up with their own user database, their own session rules, and their own idea of what a strong second factor means. That sprawl is where attackers win. Verizon’s 2025 Data Breach Investigations Report found that credential abuse was the initial access vector in 22% of the breaches it reviewed, and the same research series reported that credential stuffing made up a median 19% of daily authentication attempts in the single sign-on provider logs it analyzed.
Authelia attacks the sprawl from the proxy layer. Users sign in once, the portal sets a session cookie for your whole domain, and every protected hostname trusts that cookie. Authelia’s own site describes three ways to hand identity to an app: the shared session cookie, OpenID Connect 1.0, and trusted headers. Its documentation also claims that CPU use stays under 1% in a small-business setup, with password hashing as the exception. Argon2 hashing is deliberately expensive, so you will see short CPU spikes at login and nothing otherwise.
Analysts see the same direction of travel. Gartner projects that by 2027, 90% of enterprises will meet their multi-factor needs for remote and cloud access with the native capabilities of access-management tools, according to its 2025 Market Guide for User Authentication, which Thales hosts. A homelab or a small team can copy that idea for free: one identity layer, one policy file, one place to audit.
What you will build
- auth.lab.test, the Authelia login portal behind nginx with TLS.
- whoami.lab.test, a demo app that needs a password (one factor).
- admin.lab.test, a second demo app that needs a password plus TOTP or a passkey, and only for members of the
adminsgroup. - A Redis container so sessions survive restarts, and local SQLite storage for TOTP and WebAuthn data.
- Lockout rules that ban a user after three failed attempts, and a deny-by-default policy for every hostname you forget to list.
The same files port to a real domain with three changes: a public hostname, a Let’s Encrypt certificate, and an SMTP notifier. Those changes appear in the advanced section near the end. The category hub for cybersecurity tutorials lists the neighboring hardening guides if you want to stack more layers afterward.
Prerequisites and Tested Versions
The table lists what the walkthrough assumes. Authelia is pinned to 4.39.28, the version shown on the project homepage on November 4, 2026. The nginx 1.31 series is current, with 1.31.7 shipping on October 14, 2026 and adding native GeoIP2 and GeoLite2 database support, improved HTTP/2 prioritization, and QUIC reload fixes. None of those changes affect forward authentication, so any 1.31 image works.
| Component | Version or tag | Role in the lab |
|---|---|---|
| Authelia | authelia/authelia:4.39.28 | Login portal and authorization endpoint |
| nginx | nginx:1.31 (1.31.7 current) | Reverse proxy, TLS, auth_request client |
| Docker Engine + Compose v2 | A current release with the Compose plugin | Runs the four containers |
| Redis | redis:alpine, pin a major tag after testing | Session storage |
| traefik/whoami | Latest tag is fine for a lab | Demo app that echoes request headers |
| mkcert | Current release from your package manager | Locally trusted TLS certificates |
| Browser | Any current Chrome, Firefox, Safari or Edge | WebAuthn passkeys need a secure context |
Docker is the one tool in that list you probably have already, so most readers can skip installation and go straight to the commands below. Docker’s release notes reportedly list Engine 29.9.3 as the newest build as of November 3, 2026, but any Engine from the 29.x line with Compose v2 works for this lab.
For the host, any Linux box with 1 vCPU and 1 GB of free memory is enough. Ubuntu Server LTS is the easy choice, since a long-term release gets security updates for years without forcing upgrades. If your Docker host is an Unraid server, the same files run unchanged, and our Unraid setup guide covers where to keep the appdata folders.
Check your tools before you start:
docker --version
docker compose version
openssl version
mkcert -version
Expected output looks like this, with your own build numbers:
Docker version 29.x.y, build abcdef1
Docker Compose version v2.x.y
OpenSSL 3.x.y (date)
v1.x.y
If mkcert reports command not found, you will install it in step 3. If docker compose fails but docker-compose works, you have the retired v1 binary. Install the Compose plugin before continuing, because the file format below targets v2.
Authelia, Authentik or Keycloak: Which Identity Layer Fits
Authelia is not the only way to add single sign-on to a self-hosted stack. Pick it when you want a small, file-driven gatekeeper. Pick something heavier when you need a user admin console, social login or large directory sync. The comparison below reflects each project’s design, not a benchmark.
| Question | Authelia | Authentik | Keycloak |
|---|---|---|---|
| Main job | Forward-auth gatekeeper with 2FA and access rules | Full identity provider with flows and a web console | Enterprise identity and access management |
| User management | YAML file or LDAP, no built-in user admin UI | Web console | Web console and admin API |
| Moving parts | One container plus optional Redis and SQL | Several services and a database | Java server and a database |
| Policy style | Declarative rules in one config file | Policies and flows in the UI | Realms, clients and roles |
| Best fit | Homelabs and small teams behind nginx, Traefik or Caddy | Teams that want a UI and many app integrations | Large organizations and custom auth flows |
If you outgrow the file backend, our walkthroughs for Authentik SSO and Keycloak show the heavier path. Both use the same standards Authelia speaks (OpenID Connect), so apps you integrate today usually move later with only a client-ID change.
Kubernetes users should note that the community Ingress-NGINX project is retired, according to the NGINX blog, which now points teams toward NGINX Ingress Controller or NGINX Gateway Fabric. Authelia still works there, but the annotations differ, and our Kubernetes ingress setup guide covers the replacement controllers. This article stays on plain Docker Compose.
How Authelia Forward Auth Works With nginx
Forward authentication splits one request into two. nginx receives a browser request for admin.lab.test. Before it proxies anything, the auth_request module fires a subrequest to Authelia’s /api/authz/auth-request endpoint, passing along the original method, URL and cookies. Authelia answers with a status code and nginx obeys it.
- The browser asks nginx for
https://admin.lab.test/. - nginx sends an internal subrequest to Authelia with headers such as
X-Original-URLandX-Original-Method. - Authelia reads the session cookie, checks the access control rules, and returns 200 (allow), 401 (log in first) or 403 (forbidden).
- On 200, nginx proxies the request to the app and adds identity headers such as
Remote-UserandRemote-Groups. - On 401, nginx redirects the browser to the portal at
auth.lab.testwith a return address attached. - After login, Authelia sets a session cookie on
lab.test, so every subdomain recognizes the user without another prompt.
Two details trip people up. First, the cookie domain is the parent domain, which is why the portal and every protected app must share it. Second, Authelia decides based on headers nginx supplies, so the proxy must overwrite them rather than trust the client. You will do that in step 8 by setting X-Forwarded-For from $remote_addr. If you want a refresher on the proxy side first, our nginx reverse proxy tutorial walks through upstreams, headers and TLS from scratch.
Authelia also speaks the ForwardAuth dialect used by Traefik and Caddy. This lab uses the AuthRequest implementation because it is the one nginx understands, and the configuration file in step 5 enables it explicitly.
Steps 1-3: Lay Out the Project, Secrets and Certificates
Step 1. Create the project folders. Everything lives under one directory so you can back it up, copy it or delete it in one move. The layout separates Authelia’s config, nginx’s config, certificates and secrets.
mkdir -p ~/authelia-lab/{authelia,nginx/conf.d,nginx/snippets,certs,secrets}
cd ~/authelia-lab
ls
You should see authelia certs nginx secrets. Every later command assumes you run it from ~/authelia-lab.
Step 2. Generate three secrets. Authelia needs a session secret, a storage encryption key and a JWT secret for password-reset links. Create each one as a random 64-character hex string and keep them in files rather than in the YAML. Authelia can read any of them from an environment variable that ends in _FILE, and Compose will mount the files as Docker secrets.
for n in session_secret storage_key jwt_secret; do
openssl rand -hex 32 > "secrets/$n"
done
chmod 600 secrets/*
ls -l secrets
Expected output (abridged):
-rw------- 1 you you 65 Nov 4 10:12 jwt_secret
-rw------- 1 you you 65 Nov 4 10:12 session_secret
-rw------- 1 you you 65 Nov 4 10:12 storage_key
Treat the storage key as irreplaceable. Authelia uses it to encrypt TOTP seeds and WebAuthn data inside the database. Lose it and every enrolled device becomes unreadable, so every user must re-enroll. Back it up somewhere offline the moment you finish this lab.
Step 3. Issue local TLS certificates. Browsers refuse to register passkeys on plain HTTP and Authelia marks its cookies as secure, so the lab needs HTTPS even on a laptop. The easiest route is mkcert, which creates a private certificate authority and installs it in your system and browser trust stores.
sudo apt install mkcert libnss3-tools
mkcert -install
mkcert -cert-file certs/lab.pem -key-file certs/lab-key.pem \
auth.lab.test whoami.lab.test admin.lab.test
echo "127.0.0.1 auth.lab.test whoami.lab.test admin.lab.test" | sudo tee -a /etc/hosts
If apt cannot find the package on your release, download the binary from the mkcert project’s release page instead. The mkcert -install command prints a line like The local CA is now installed in the system trust store!, and the second command ends with the certificate path and an expiry date.
Two notes on that last line. If your browser runs on a different machine than the Docker host, put the host’s LAN address in the hosts file instead of 127.0.0.1, and copy the mkcert root certificate to that machine. Also remember that .test is a reserved top-level domain that never resolves on the public internet, which makes it safe for experiments. On a real server you would skip mkcert entirely and use Let’s Encrypt, as shown in our Certbot on Ubuntu guide.
Steps 4-5: Create Users and Write the Authelia Configuration
Step 4. Hash a password and create the users file. Authelia’s file backend stores one Argon2id hash per user and never a plain password. Generate the hash with the Authelia image itself, and let it prompt you so the password stays out of your shell history.
docker run --rm -it authelia/authelia:4.39.28 \
authelia crypto hash generate argon2
Type a long unique password twice. The command prints a line that begins with Digest: followed by a string like this one:
Digest: $argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHQ$aGFzaGhhc2hoYXNo...
Copy everything after Digest: into the file below. Run the command a second time for Bob. Quote the hash with single quotes in YAML, because the dollar signs mean nothing special to YAML but mean a great deal to shells and to Compose.
# authelia/users_database.yml
users:
alice:
disabled: false
displayname: "Alice Example"
password: '$argon2id$v=19$m=65536,t=3,p=4$REPLACE_WITH_ALICE_HASH'
email: [email protected]
groups:
- admins
- dev
bob:
disabled: false
displayname: "Bob Example"
password: '$argon2id$v=19$m=65536,t=3,p=4$REPLACE_WITH_BOB_HASH'
email: [email protected]
groups:
- dev
Alice belongs to admins and Bob does not. That difference drives the access test in step 12. The file backend watches this file, so adding a user later does not need a restart on most setups, though a restart is the safe move after big edits.
Step 5. Write configuration.yml. This file holds the portal address, session rules, access policy, brute-force regulation and notifier. Authelia changes configuration keys between minor releases, which is why the compose file pins an exact tag. The schema below targets the 4.39 line.
# authelia/configuration.yml
theme: auto
server:
address: 'tcp://:9091/'
endpoints:
authz:
auth-request:
implementation: 'AuthRequest'
log:
level: 'info'
format: 'text'
totp:
issuer: 'lab.test'
period: 30
skew: 1
webauthn:
display_name: 'Lab Authelia'
attestation_conveyance_preference: 'indirect'
selection_criteria:
user_verification: 'preferred'
authentication_backend:
file:
path: '/config/users_database.yml'
password:
algorithm: 'argon2'
argon2:
variant: 'argon2id'
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16
access_control:
default_policy: 'deny'
rules:
- domain: 'whoami.lab.test'
resources:
- '^/health$'
policy: 'bypass'
- domain: 'whoami.lab.test'
policy: 'one_factor'
- domain: 'admin.lab.test'
subject: 'group:admins'
policy: 'two_factor'
session:
name: 'authelia_session'
cookies:
- domain: 'lab.test'
authelia_url: 'https://auth.lab.test'
default_redirection_url: 'https://whoami.lab.test'
expiration: '1h'
inactivity: '5m'
remember_me: '1M'
redis:
host: 'redis'
port: 6379
regulation:
max_retries: 3
find_time: '2m'
ban_time: '5m'
storage:
local:
path: '/config/db.sqlite3'
notifier:
filesystem:
filename: '/config/notification.txt'
Read the access control block slowly, because it is the heart of the system. Rules match from top to bottom and the first match wins. The health check path bypasses authentication so uptime monitors keep working. Everything else on whoami.lab.test needs one factor. The admin host needs two factors and membership of admins. A request for any hostname that matches no rule falls to deny, which is the reason default_policy must never be one_factor or bypass on an internet-facing box.
What the session block really controls
The expiration value caps a session at one hour no matter how busy the user is. The inactivity value ends it after five idle minutes. remember_me lets users tick a box at login for a month-long session, which you can disable by setting it to -1 if your policy forbids long sessions. The three secrets never appear in the file, because step 7 injects them through the _FILE variables.
The regulation block deserves a caveat. Authelia counts failed attempts per username, not per IP address. Three bad passwords for Alice lock Alice for five minutes, whoever typed them. That protects the account but lets an attacker annoy a real user, so pair Authelia with an IP-based tool like Fail2ban or CrowdSec on the proxy host if the portal faces the internet.
Steps 6-7: Validate the Config and Write compose.yaml
Step 6. Validate before you start anything. Authelia refuses to boot on a bad config, and the error messages are precise, so a dry run saves time. Run the validator from the same image you will deploy, mounting the config and the secrets exactly as the real container will see them.
docker run --rm \
-v "$PWD/authelia:/config" \
-v "$PWD/secrets:/run/secrets:ro" \
-e AUTHELIA_SESSION_SECRET_FILE=/run/secrets/session_secret \
-e AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/run/secrets/storage_key \
-e AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/run/secrets/jwt_secret \
authelia/authelia:4.39.28 \
authelia validate-config --config /config/configuration.yml
A clean run ends with a success line such as the one below. Warnings about deprecated keys also appear here, which makes this command the first thing to run after every upgrade.
Configuration parsed and loaded successfully without errors.
Authelia has renamed command-line subcommands between releases. If your pinned image answers with an unknown-command error, run authelia --help inside it and use the validation command it lists.
Step 7. Write compose.yaml. Four services share the default Compose network. Only nginx publishes ports. Authelia, Redis and both demo apps stay private, and that matters: if you also published the whoami port on the host, anyone could skip the proxy and the login with it.
# compose.yaml
services:
authelia:
image: authelia/authelia:4.39.28
container_name: authelia
restart: unless-stopped
volumes:
- ./authelia:/config
environment:
TZ: UTC
AUTHELIA_SESSION_SECRET_FILE: /run/secrets/session_secret
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /run/secrets/storage_key
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: /run/secrets/jwt_secret
secrets:
- session_secret
- storage_key
- jwt_secret
depends_on:
- redis
expose:
- "9091"
redis:
image: redis:alpine
container_name: authelia-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
nginx:
image: nginx:1.31
container_name: authelia-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/snippets:/etc/nginx/snippets:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- authelia
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
whoami-admin:
image: traefik/whoami
container_name: whoami-admin
restart: unless-stopped
volumes:
redis-data:
secrets:
session_secret:
file: ./secrets/session_secret
storage_key:
file: ./secrets/storage_key
jwt_secret:
file: ./secrets/jwt_secret
A few choices here are deliberate. The Authelia tag is exact, so a routine docker compose pull cannot drag you onto a release with different configuration keys. Redis runs with append-only persistence, which is what lets a session outlive a restart. The secrets use Compose’s file-based form, so they appear inside the container under /run/secrets and never in docker inspect output as plain environment values.
Steps 8-9: Configure nginx for the Portal and Protected Apps
Step 8. Create the shared snippets. Three small files keep the server blocks short and make a mistake in one place easy to fix. The first holds the TLS settings, the second the proxy headers, and the third the internal location nginx uses to ask Authelia for a verdict.
# nginx/snippets/tls.conf
ssl_certificate /etc/nginx/certs/lab.pem;
ssl_certificate_key /etc/nginx/certs/lab-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
add_header Strict-Transport-Security "max-age=31536000" always;
# nginx/snippets/proxy.conf
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-URI $request_uri;
proxy_set_header Upgrade $http_upgrade;
Look closely at the X-Forwarded-For line. It sets the header to $remote_addr rather than appending to whatever the client sent. A client can put anything it likes into that header, and Authelia uses the value for logging and for rules that match on network. Overwriting it closes a spoofing hole that the append form leaves open.
# nginx/snippets/authelia-location.conf
location /internal/authelia/authz {
internal;
proxy_pass http://authelia:9091/api/authz/auth-request;
proxy_pass_request_body off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Content-Length "";
proxy_set_header Host $host;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$host$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-URI $request_uri;
}
The internal directive hides this location from outside requests, so nobody can call it directly. The empty body and empty Content-Length keep the subrequest tiny. Authelia updated its nginx examples and tests on October 20, 2026, according to the project’s commit history, so compare this snippet with the current nginx integration page for your pinned version before you move it to production.
# nginx/snippets/authelia-authrequest.conf
auth_request /internal/authelia/authz;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
proxy_set_header Remote-Name $name;
proxy_set_header Remote-Email $email;
auth_request_set $redirection_url $upstream_http_location;
error_page 401 =302 $redirection_url;
This snippet goes inside each protected location. It calls the internal endpoint, copies the identity Authelia returns into request headers for the app, and converts a 401 into a redirect to the portal. Because proxy_set_header overwrites, a client cannot forge Remote-User by sending its own.
Step 9. Define the server blocks. One file handles the plain-HTTP redirect, the portal, and both apps. Docker’s embedded DNS resolves the container names, so no resolver directive is needed as long as you keep the upstreams as literal names.
# nginx/conf.d/lab.conf
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name auth.lab.test;
include /etc/nginx/snippets/tls.conf;
location / {
include /etc/nginx/snippets/proxy.conf;
proxy_pass http://authelia:9091;
}
}
server {
listen 443 ssl;
http2 on;
server_name whoami.lab.test;
include /etc/nginx/snippets/tls.conf;
include /etc/nginx/snippets/authelia-location.conf;
location / {
include /etc/nginx/snippets/authelia-authrequest.conf;
include /etc/nginx/snippets/proxy.conf;
proxy_pass http://whoami:80;
}
}
server {
listen 443 ssl;
http2 on;
server_name admin.lab.test;
include /etc/nginx/snippets/tls.conf;
include /etc/nginx/snippets/authelia-location.conf;
location / {
include /etc/nginx/snippets/authelia-authrequest.conf;
include /etc/nginx/snippets/proxy.conf;
proxy_pass http://whoami-admin:80;
}
}
Note the http2 on directive. Current nginx releases take HTTP/2 as its own setting instead of a parameter on listen, and the old form triggers a deprecation warning. The portal block has no auth_request at all, since the login page must be reachable by anonymous visitors.
Steps 10-11: Start the Stack and Enroll Two-Factor
Step 10. Test the nginx config, then start everything. The first command checks the Compose file for syntax errors, which costs you five seconds instead of a crash loop. After startup, a second check asks nginx to parse its own config.
docker compose config --quiet && echo "compose file OK"
docker compose up -d
docker compose ps
Expected output (abridged):
compose file OK
NAME IMAGE STATUS PORTS
authelia authelia/authelia:4.39.28 Up 8 seconds 9091/tcp
authelia-nginx nginx:1.31 Up 7 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
authelia-redis redis:alpine Up 9 seconds 6379/tcp
whoami traefik/whoami Up 9 seconds 80/tcp
whoami-admin traefik/whoami Up 9 seconds 80/tcp
Now read the Authelia log. A healthy start prints the version, loads the user file, connects to Redis and begins listening on port 9091. Exact wording shifts between releases, so look for the absence of level=error lines rather than for one specific sentence.
docker compose logs authelia --tail 20
docker compose exec nginx nginx -t
The second command should report syntax is ok and test is successful. If nginx exits at startup, run docker compose logs nginx. The most common cause at this stage is a typo in a snippet path.
Step 11. Log in and enroll a second factor. Open https://whoami.lab.test in a browser. nginx should redirect you to https://auth.lab.test with a return address in the URL. Sign in as alice. Because the first rule for that host only demands one factor, Authelia lets you through to the demo app straight away. To see the two-factor path, open https://admin.lab.test next.
Authelia now asks for a second factor and has none on file for Alice, so it offers to register one. Choose the one-time password method. Authelia wants to confirm your identity first, and with the filesystem notifier it writes the message to a file instead of sending email. Read it from the container:
docker compose exec authelia cat /config/notification.txt
The file contains the verification text for Alice, which includes a link or a one-time code depending on the release. Use it in the browser, then scan the QR code with an authenticator app. Enter the six-digit code the app shows. Authelia confirms the device and returns you to the admin host.
If the six digits keep failing, check the clocks first. TOTP depends on the server and the phone agreeing on the time to within roughly thirty seconds, and the config allows one period of drift through skew: 1.
When the admin app loads, the demo prints the request it received. Look for the identity headers that nginx copied over from Authelia:
Hostname: 3f1c9a7d2b10
IP: 172.19.0.5
GET / HTTP/1.1
Host: admin.lab.test
Remote-Email: [email protected]
Remote-Groups: admins,dev
Remote-Name: Alice Example
Remote-User: alice
X-Forwarded-For: 172.19.0.1
Real apps can read Remote-User and skip their own login entirely, which is the trusted-headers pattern. Only do that when the app is unreachable except through nginx. If someone can hit the container directly, they can send those headers themselves.
Steps 12-13: Test Access Rules, Passkeys and Lockouts
Step 12. Prove each rule with a test. A policy you have not tested is a guess. Run these checks from the terminal first. They need no browser and no cookie, so they show exactly what an anonymous visitor sees.
CA="$(mkcert -CAROOT)/rootCA.pem"
# 1. Anonymous request to a protected host: expect a redirect to the portal
curl -sI --cacert "$CA" https://whoami.lab.test/ | head -n 5
# 2. The bypass path: expect the app to answer without any login
curl -sI --cacert "$CA" https://whoami.lab.test/health | head -n 1
# 3. HTTP should bounce to HTTPS
curl -sI http://whoami.lab.test/ | head -n 3
Expected output, with timestamps and header casing varying by version:
HTTP/2 302
server: nginx/1.31.7
location: https://auth.lab.test/?rd=https%3A%2F%2Fwhoami.lab.test%2F&rm=GET
HTTP/2 200
HTTP/1.1 301 Moved Permanently
Server: nginx/1.31.7
Location: https://whoami.lab.test/
Now test the group rule in the browser. Open a private window, sign in as bob, and visit https://admin.lab.test. Bob has a valid password but sits outside admins, so the rule never matches him and the default deny applies. Expect a 403 Forbidden page. Then sign out and sign back in as Alice with her second factor and confirm the app loads. Two users, one door, two outcomes: that is the policy working.
Step 13. Add a passkey and trigger the lockout. Sign in as Alice, open the portal settings at https://auth.lab.test, and add a WebAuthn credential. Use a hardware key, your phone or the platform authenticator on your laptop. Authelia stores only the public half. On the next visit to the admin host, choose the security key option instead of the six-digit code. The hostname must match the one you registered, which is why IP-address access never works for WebAuthn.
Reportedly, the 4.39 line can also accept passkeys as a first factor when you set enable_passkey_login to true in the WebAuthn block. Check the configuration reference for your pinned release before you rely on it, since this option is newer than the rest of the lab.
Finally, exercise the regulation rule. Sign out, then enter a wrong password for Bob three times within two minutes. The fourth attempt, even with the right password, should fail with a generic error and the log should show the ban:
docker compose logs authelia --since 5m | grep -i -E "regulation|banned|mark"
Wait five minutes and Bob can sign in again. Run this test once so you know what a lockout looks like in your logs, because the first time you see one should not be during a real incident.
The Complete Working Project
Here is the finished tree. Every file appeared above, so nothing is hidden. Copy the structure, fill in the two password hashes, and the stack comes up with one command.
authelia-lab/
|-- compose.yaml
|-- authelia/
| |-- configuration.yml
| |-- users_database.yml
| |-- db.sqlite3 (created at first start)
| `-- notification.txt (created at first enrollment)
|-- nginx/
| |-- conf.d/lab.conf
| `-- snippets/
| |-- tls.conf
| |-- proxy.conf
| |-- authelia-location.conf
| `-- authelia-authrequest.conf
|-- certs/
| |-- lab.pem
| `-- lab-key.pem
`-- secrets/
|-- jwt_secret
|-- session_secret
`-- storage_key
A small bootstrap script handles the repeatable parts, which are steps 1 to 3 and the validation in step 6. Save it as bootstrap.sh, read it once, then run it. It refuses to overwrite secrets that already exist, because regenerating the storage key would orphan every enrolled device.
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p authelia nginx/conf.d nginx/snippets certs secrets
for n in session_secret storage_key jwt_secret; do
if [ ! -f "secrets/$n" ]; then
openssl rand -hex 32 > "secrets/$n"
echo "created secrets/$n"
else
echo "kept existing secrets/$n"
fi
done
chmod 600 secrets/*
if [ ! -f certs/lab.pem ]; then
mkcert -install
mkcert -cert-file certs/lab.pem -key-file certs/lab-key.pem \
auth.lab.test whoami.lab.test admin.lab.test
fi
docker run --rm \
-v "$PWD/authelia:/config" \
-v "$PWD/secrets:/run/secrets:ro" \
-e AUTHELIA_SESSION_SECRET_FILE=/run/secrets/session_secret \
-e AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/run/secrets/storage_key \
-e AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/run/secrets/jwt_secret \
authelia/authelia:4.39.28 \
authelia validate-config --config /config/configuration.yml
docker compose up -d
docker compose ps
To reset the lab without touching your secrets, run docker compose down and delete authelia/db.sqlite3. To remove it completely, add -v to drop the Redis volume, then delete the project folder and the three hosts-file entries.
Common Pitfalls That Break Authelia Setups
Most failed installs trace back to a short list of mistakes. Read it before you copy the lab to a real server.
- Mismatched cookie domain. The
session.cookies[].domainmust be a parent of every protected hostname and of the portal. Set it toauth.lab.testinstead oflab.testand the browser never sends the cookie to the apps, which produces an endless redirect loop. - Running over plain HTTP. Authelia sets secure cookies and browsers only allow WebAuthn in a secure context. HTTP appears to work for the first page, then every login silently fails.
- Using the latest tag. Configuration keys move between minor releases. Pin the exact version, read the release notes, run the validator, and only then bump the tag.
- Publishing the app port. If you map the whoami port to the host, anyone can reach the app without the proxy and without a login. Keep apps on the private Compose network and publish only nginx.
- Leaving the default policy open. A
default_policyofone_factorturns every forgotten hostname into a password-only app. Keep it atdenyand add explicit rules. - Losing the storage key. Regenerate it and every TOTP seed and passkey in the database becomes unreadable. Back it up offline with the SQLite file.
- Trusting client headers. If nginx appends to
X-Forwarded-Forinstead of overwriting it, a visitor can fake an address. The project reportedly has a trusted-proxy feature under development for its 4.40.0 milestone, but until it ships, your proxy config does this job. - Forgetting clock sync. TOTP fails when the server drifts. Run
timedatectland confirm that NTP synchronization shows as active.
The cookie domain mistake deserves extra attention because it looks like a bug in everything else. The browser follows the redirect, Authelia logs the user in, nginx asks again, no cookie arrives, and the cycle repeats until the browser gives up with a too-many-redirects page. Open the developer tools, find the Set-Cookie header on the portal response, and check that its Domain attribute is the shared parent. That single check resolves most first-time failures.
The published-port mistake is quieter and more dangerous. Nothing breaks, so nothing prompts you to look. Run a port scan against your own server from another machine after every compose change, and treat any port besides 80 and 443 as a finding until you can explain it.
Troubleshooting: 10 Problems and Fixes
Start every diagnosis with the same two commands: docker compose logs authelia --tail 50 and docker compose logs nginx --tail 50. Authelia explains most failures in plain language.
| Symptom | Likely cause | Fix |
|---|---|---|
| Endless redirect between app and portal | Cookie domain does not cover the app, or authelia_url is wrong | Set domain to the shared parent and authelia_url to the portal’s full HTTPS address |
| 502 Bad Gateway on the internal authz location | Authelia is down or the container name is wrong | Check docker compose ps, then confirm the proxy_pass host matches the service name |
| Authelia exits right after start | Config error or a missing secret file | Run the step 6 validator and read the first error line |
| Password accepted, TOTP code rejected | Clock drift between server and phone | Sync both clocks and keep skew: 1 |
| No email arrives during enrollment | The filesystem notifier writes a file, not mail | Read /config/notification.txt, or configure SMTP |
| Passkey registration fails | Plain HTTP, an IP address in the URL, or a hostname mismatch | Use the HTTPS hostname that matches your cookie domain |
| Users get logged out after a restart | Sessions live in memory because Redis is missing | Confirm the redis block and that the container is running |
| App loads but shows no user | Identity headers are not forwarded | Include the authrequest snippet inside the location that proxies to the app |
| 403 for a user who should have access | Rule order or a group typo | Rules match top to bottom, so move specific rules above general ones and check the group names in the users file |
| Account locked after a few attempts | Regulation ban is active | Wait out ban_time, then review the log for who triggered it |
Two extra tips help with the stubborn cases. When a browser keeps bouncing, test with curl -sI and read the location header, because browsers cache redirects and hide the real first response. And when nginx returns a 500 on a protected host, remember that a dead Authelia makes auth_request fail closed. Your apps stay unreachable instead of falling open, which is the right behavior even if it feels harsh at 2 a.m.
Advanced Tips for Production Use
The lab proves the pattern. The changes below make it fit for a public server.
Move from SQLite to PostgreSQL
SQLite is fine for one node and a handful of users. A shared database lets you run several Authelia instances and back up with standard tools. Replace the storage.local block and add the password through another _FILE variable:
storage:
postgres:
address: 'tcp://postgres:5432'
database: 'authelia'
username: 'authelia'
# password: set via AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE
Send real email and protect Redis
Swap the filesystem notifier for SMTP so identity checks reach users. Give Redis a password and keep it off published ports. Both changes stop a stolen container network position from becoming a stolen session.
notifier:
smtp:
address: 'submission://smtp.example.com:587'
username: '[email protected]'
sender: 'Authelia <[email protected]>'
# password: set via AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
session:
redis:
host: 'redis'
port: 6379
# password: set via AUTHELIA_SESSION_REDIS_PASSWORD_FILE
Other upgrades worth making
- Real certificates. Drop mkcert, point DNS at the server, and use Let’s Encrypt. Change the cookie domain to a domain you own.
- LDAP backend. Swap the file backend for LDAP when you already run a directory, so users keep one password across systems.
- OpenID Connect. Authelia can act as an OIDC provider. Apps that support OIDC, such as many dashboards and wikis, get proper single sign-on instead of header trust. The project reportedly retested its OIDC integration guides against version 4.39.22 in September 2026, across 72 client guides.
- Per-rule network limits. Add a
networkscondition so admin hosts only accept sign-ins from your VPN range. - Log shipping. Authelia logs failed logins and bans in a plain format. Forward them to a SIEM and alert on repeated bans. Our Wazuh SIEM Docker setup shows one way to collect container logs.
- IP-level blocking. Add Fail2ban or CrowdSec in front of the portal, since Authelia’s lockout is per username.
- Backups. Copy
users_database.yml,configuration.yml, the SQLite file (or a Postgres dump) and the three secrets. Test a restore on a spare machine. - Disciplined upgrades. Bump the tag, run the validator, read the release notes, and restart in a quiet hour. The project ships frequent point releases in the 4.39 line, including routine dependency and security-tool updates through October 2026, according to its repository activity.
Restrict the admin host to your VPN range
Two factors protect against stolen passwords. A network condition protects against stolen passwords and stolen second factors used from the wrong place. Authelia can name networks in the config and attach them to a rule. The example below lets admins reach the admin host only from a private VPN range, and sends everyone else to the default deny.
access_control:
default_policy: 'deny'
networks:
- name: 'vpn'
networks:
- '10.8.0.0/24'
rules:
- domain: 'admin.lab.test'
subject: 'group:admins'
networks:
- 'vpn'
policy: 'two_factor'
This only works if Authelia sees the real client address, which is why the nginx snippet overwrites X-Forwarded-For with $remote_addr. Behind a CDN or a second proxy, the address you see is the proxy’s, and the rule would match the wrong network. Test it from inside and outside the range before you trust it.
A pre-launch checklist
Run through this list the day before you point a public hostname at the portal. Each item takes a minute or two, and each one maps to a failure mode that shows up in real incident reports.
- Confirm
default_policyisdenyand that every hostname you serve has a rule. - Confirm only nginx publishes ports with
docker compose psandss -tlnpon the host. - Replace the lab hashes with unique passwords of at least 16 characters, and enroll two factors per admin so a lost phone is not a lockout.
- Switch the notifier to SMTP and run a real password-reset round trip.
- Set a Redis password and move the database off SQLite if you plan more than one Authelia instance.
- Copy secrets and the database to offline storage, then restore them on a spare box to prove the backup works.
- Pin every image tag, including nginx and Redis, and write the upgrade steps in a runbook.
- Point an uptime monitor at the bypassed health path and an alert at repeated lockouts in the logs.
Frequently Asked Questions
Is Authelia free to use?
Yes. The project is open source under the Apache 2.0 license, and no separately named paid edition or hosted service could be confirmed as of November 2026.
Does Authelia work with Traefik or Caddy instead of nginx?
Yes. Authelia supports forward-auth style integration for both. The portal, users file, access rules and session settings stay identical. Only the proxy snippets change.
Do I need a real domain?
For a lab, no. A reserved test domain with mkcert certificates works on one machine or one LAN. For anything public you need a domain you control, because cookies and certificates both depend on it.
What happens if Authelia goes down?
Protected apps become unreachable. The auth_request module treats an unreachable authorizer as a failure, so nginx refuses the request instead of passing it through. Run Authelia with a restart policy and monitor its health.
Can I use passkeys instead of TOTP?
Yes, as a second factor. Register a security key, phone or platform authenticator under WebAuthn. Passwordless first-factor login is newer, so confirm it in the configuration reference for your pinned release.
How is Authelia different from a VPN?
A VPN protects the network path. Authelia protects each web application. Many teams use both, with Authelia as a second gate inside the tunnel.
How do I upgrade safely?
Back up the config, secrets and database. Change the image tag, run the validator, read the release notes for removed keys, then run docker compose up -d. Keep the old tag handy for a quick rollback.





