- Pin AUTHELIA_VERSION=4.39.19 (current stable, released 2026-04-12) and FAIL2BAN_VERSION=1.1.0-r0 in .env.example + docker-compose.yml. - Reshape access_control.rules and the Caddyfile snippet around a three-case decision tree: no app auth (Authelia is the gate), app with proxy-auth support (switch FROM app login TO Authelia headers), and apps that keep their own login (skip Authelia entirely). - Document Frigate 0.14+ proxy auth specifically: auth.enabled: False, proxy.header_map (remote-user / remote-groups), trusted_proxies for the caddy_net subnet, optional X-Proxy-Secret for cross-VLAN trust. https://claude.ai/code/session_013XZ1vmgk78k2PEQ5DmJhF3
16 KiB
Authelia + fail2ban
Self-hosted authentication portal (Authelia) with an IP-banning sidecar
(fail2ban). Designed to sit next to a dockerized Caddy on the main server
and gate every public subdomain (cam.example.com, doorbell.example.com,
etc.) behind a single sign-on portal at auth.example.com.
Internet
|
v
+-------+ caddy_net (docker) +--------------------+
| Caddy |--- forward_auth -------------->| Authelia |
+---+---+ | /api/authz/... |
| reverse_proxy +--------+-----------+
| |
v v
Frigate (IoT VLAN), ntfy, etc. ./authelia/db.sqlite3
./authelia/authelia.log
^
| tail
+------+--------+
| fail2ban | host net
| DOCKER-USER | +iptables
+---------------+
- One docker-compose file, two services, one external network (
caddy_net). - File-backed users database, SQLite storage, in-memory sessions (no Redis).
- Filesystem notifier -- password reset / new-device emails get written to
a file you
tail -f. Swap to SMTP later by editing one block. - fail2ban runs in host network mode and bans via the
DOCKER-USERchain so drops happen at the host edge, before traffic reaches the Caddy container's published ports. - Caddy is not in this repo. You merge a snippet into your real Caddyfile
(see
caddy/snippet.example.caddyfile).
Repo layout
authelia/
|-- docker-compose.yml
|-- .env.example # copy to .env
|-- .gitignore
|-- README.md # this file
|
|-- authelia/
| |-- configuration.yml # main config -- edit your domain in here
| |-- users_database.yml.example # copy to users_database.yml (gitignored)
| |-- secrets/ # gitignored; secret files mounted as /secrets
| `-- notifications/ # filesystem notifier writes here
|
|-- fail2ban/
| `-- data/ # mounted as /data in the container
| |-- jail.d/
| | |-- authelia.local
| | `-- caddy.local
| `-- filter.d/
| |-- authelia.local
| `-- caddy-4xx.local
|
`-- caddy/
`-- snippet.example.caddyfile # merge into YOUR Caddyfile
Prerequisites
- Docker + docker compose v2 on the main server.
- Caddy already running on the main server, in Docker, joined to an
external network named
caddy_net. If your network is named differently, changecaddy_neteverywhere in this repo. - A root domain you control (the examples use
example.com). DNS records forauth.<root>and every protected subdomain should point at the Caddy host's public IP. - Caddy v2.5.1 or newer (for
forward_authdirective).
First-run setup
# 0) From wherever you keep ~/docker stacks
cd ~/docker
git clone <this repo's url> authelia
cd authelia
# 1) External docker network -- Caddy must already be on this. If it isn't,
# create it now and make sure your Caddy compose joins it.
docker network create caddy_net 2>/dev/null || true
# 2) Bootstrap the secrets directory
mkdir -p authelia/secrets
openssl rand -hex 32 > authelia/secrets/JWT_SECRET
openssl rand -hex 32 > authelia/secrets/SESSION_SECRET
openssl rand -hex 32 > authelia/secrets/STORAGE_ENCRYPTION_KEY
chmod 600 authelia/secrets/*
# 3) Copy and edit the .env
cp .env.example .env
$EDITOR .env # set TZ; pin AUTHELIA_VERSION if you want
# 4) Edit configuration.yml -- replace EVERY `example.com` with your real
# root domain (look for the CHANGE comments)
$EDITOR authelia/configuration.yml
# 5) Create your first user
cp authelia/users_database.yml.example authelia/users_database.yml
$EDITOR authelia/users_database.yml # set username, email, displayname
# Generate the password hash:
docker compose run --rm authelia \
authelia crypto hash generate argon2 --password 'your-real-password'
# Paste the resulting `$argon2id$v=19$m=...` into the user's `password:`.
# 6) Validate the config before starting (catches typos / schema issues)
docker compose run --rm authelia \
authelia validate-config --config /config/configuration.yml
# 7) Bring it up
docker compose up -d
docker compose logs -f authelia # expect "Authelia is listening on ..."
Which sites go behind Authelia?
Authelia is opt-in per site. The goal is one login (Authelia, with 2FA) across everything that can use it -- and zero double-prompts for things that already authenticate themselves and can't be retrofitted.
| Case | App has built-in auth? | Switchable to proxy auth? | What to do |
|---|---|---|---|
| 1 | No | n/a | Gate with Authelia. Use two_factor for anything that controls hardware. |
| 2 | Yes | Yes (Authelia, Authentik, oauth2_proxy headers) | Disable the app's login form, point it at Authelia headers, gate with Authelia. Single login. |
| 3 | Yes | No | Don't involve Authelia. Plain reverse_proxy in Caddy. The app handles its own login. |
Concretely, in this household:
doorbell.example.com(Pi PTT page) -- case 1. No app auth. Authelia is the only gate.two_factor.cam.example.com(Frigate UI) -- case 2. Frigate 0.14+ supports proxy auth, so disable Frigate's login form and let Authelia drive both the auth and the role mapping. Single login covers Frigate too.- router admin / NAS UI / odd one-offs -- case 3 territory. Plain
reverse_proxy, noimport authelia, no Authelia rule.
Default policy in configuration.yml is deny, so a domain with no rule
and no import authelia in Caddy never reaches Authelia at all -- the
deny doesn't apply.
Wire Caddy into Authelia
Open caddy/snippet.example.caddyfile. It defines:
(authelia)-- reusable snippet:import autheliain any site block you want gated.(accesslog)-- writes Caddy's JSON access log to/var/log/caddy/access.logso fail2ban can watch it.auth.example.com-- the Authelia portal subdomain.- Example blocks for the three cases above.
Copy the relevant blocks into your real Caddyfile, replace example.com
with your domain and 192.168.x.x with real upstream IPs, then reload Caddy:
docker compose -f ~/docker/caddy/docker-compose.yml exec caddy \
caddy reload --config /etc/caddy/Caddyfile
For each case-1 or case-2 domain, also add a rule under access_control.rules
in authelia/configuration.yml. Restart Authelia after editing:
docker compose restart authelia
Switching Frigate to Authelia (case 2)
Frigate 0.14+ has a proxy: config block that consumes a username header
from the upstream and skips its own login form. Edit
frigate_config/config.yml in your Frigate repo:
auth:
enabled: False
trusted_proxies:
- 172.18.0.0/16 # the caddy_net subnet -- find it with:
# docker network inspect caddy_net | jq '.[0].IPAM.Config'
proxy:
header_map:
user: remote-user # what Authelia sends; matches `copy_headers` in Caddy
role: remote-groups
default_role: viewer
separator: '|'
# Optional but recommended when Caddy crosses VLANs to reach Frigate.
# Generate with `openssl rand -hex 32`. Caddy must send the same value
# as `X-Proxy-Secret` -- see the cam.* block in caddy/snippet.example.caddyfile.
# auth_secret: 'paste-32-byte-hex-here'
Restart Frigate (docker compose restart frigate in the Frigate repo).
Confirm the Frigate UI now jumps straight to Authelia and back without
a Frigate login screen.
If you want Authelia groups to drive Frigate roles (admin vs. viewer),
add a role_map: under proxy: (see Frigate docs).
Caddy access log path
fail2ban mounts /var/log/caddy from the host as read-only. Your Caddy
compose needs to mount the same host path read-write so Caddy can write to
it. In your Caddy compose:
services:
caddy:
volumes:
- /var/log/caddy:/var/log/caddy
Make sure the host directory exists and is writable by Caddy's UID:
sudo mkdir -p /var/log/caddy
sudo chown -R 1000:1000 /var/log/caddy # adjust UID to match your Caddy
First login + TOTP enrollment
- Visit any protected subdomain in a private browser window.
- Caddy bounces you to
https://auth.<your-domain>/-- log in with the username and plaintext password you set above. - If the access rule is
two_factor, Authelia asks you to register a second factor. Pick TOTP and scan the QR with Authy / 1Password / Google Authenticator / Bitwarden / etc. - On first registration Authelia tries to email you a confirmation link.
The filesystem notifier writes it to a file -- grab it with:
Click that link to confirm registration.
docker compose exec authelia cat /config/notifications/notification.txt - Re-enter the TOTP code -- you're in.
A successful login sets the authelia_session cookie scoped to your root
domain, so it covers every subdomain protected by the same Authelia.
User management
Add a user
Append to authelia/users_database.yml, generate a hash with
docker compose run --rm authelia authelia crypto hash generate argon2 --password '...',
paste it as password:, then docker compose restart authelia (or wait
five minutes for the file refresh interval).
Change a password
Same as above -- re-generate the hash and replace the password: field.
Disable a user
Set disabled: true on their entry and restart Authelia.
Reset their TOTP
docker compose exec authelia \
authelia storage user totp delete --username yourname \
--config /config/configuration.yml
They will be prompted to re-enroll on next login.
fail2ban
Verify it's running and watching the right files
docker compose exec fail2ban fail2ban-client status
docker compose exec fail2ban fail2ban-client status authelia
docker compose exec fail2ban fail2ban-client status caddy-4xx
Each status <jail> shows the active failures, banned IPs, and the log
file it's tailing.
Test a filter against your real logs
# Authelia
docker compose exec fail2ban fail2ban-regex \
/var/log/authelia/authelia.log \
/data/filter.d/authelia.local
# Caddy
docker compose exec fail2ban fail2ban-regex \
/var/log/caddy/access.log \
/data/filter.d/caddy-4xx.local
If the failregex doesn't match anything, your log format probably differs
from what the filter expects. For Authelia: confirm log.format: 'text'
in configuration.yml. For Caddy: confirm the (accesslog) snippet is
imported into the site you're testing and that format json is set.
Manually unban an IP
docker compose exec fail2ban fail2ban-client set authelia unbanip 1.2.3.4
docker compose exec fail2ban fail2ban-client set caddy-4xx unbanip 1.2.3.4
Tune
Per-jail maxretry, findtime, bantime live in
fail2ban/data/jail.d/*.local. Edit and restart fail2ban:
docker compose restart fail2ban
The caddy-4xx jail's defaults (30 fails / 2 minutes -> 30 minute ban) are
intentionally loose -- a single failed request shouldn't ban you, but a
scanner spraying /wp-admin, /.env, /admin.php etc. will hit it fast.
The authelia jail is tighter (3 fails / 10 minutes -> 1 hour ban) on top
of Authelia's own in-app regulation (3 fails / 2 minutes -> 5 minute
account lockout), giving you defense in depth: Authelia locks the user,
fail2ban bans the IP.
Day-to-day operation
docker compose ps # everything up?
docker compose logs -f authelia # follow Authelia
docker compose logs -f fail2ban # follow fail2ban
docker compose pull && docker compose up -d # upgrade
Bump AUTHELIA_VERSION in .env when you upgrade Authelia. After any
Authelia upgrade, re-run validate-config -- the schema does evolve.
Troubleshooting
Redirect loop between site and auth.<domain>
Cookie domain mismatch. The domain under session.cookies[] must be the
root domain (e.g. example.com), and every protected site must be a
subdomain of that root, served over HTTPS. Mixed http:// and https://
won't work; the cookie is Secure.
"Configuration: session: option 'domain' and option 'cookies' can't be specified at the same time"
Old-style session.domain: ... left over from pre-4.38 config. Remove it;
this repo's configuration.yml already uses the new session.cookies[]
form.
"access denied" with no login prompt
Default policy is deny. Add a rule under access_control.rules for the
domain you're hitting and restart Authelia.
Authelia container restarts forever
docker compose logs authelia | head -50
Most often: missing/empty secret files, bad YAML in configuration.yml,
or a users_database.yml with an invalid hash.
Caddy can't resolve authelia
Caddy isn't on caddy_net. Add networks: [caddy_net] to your Caddy
service and caddy_net: external: true at the bottom of its compose,
then docker compose up -d caddy.
fail2ban bans don't actually block
Almost always: fail2ban isn't writing to the right iptables chain. With
dockerized Caddy, you need chain = DOCKER-USER (already set in the
shipped jail files). Verify:
sudo iptables -L DOCKER-USER -n
You should see jump rules pointing at f2b-* chains.
Authelia logs show nothing
log.file_path is /config/authelia.log (i.e. ./authelia/authelia.log
on the host). If the file isn't appearing, Authelia probably isn't
writing logs because it failed to start -- check docker compose logs authelia.
Switching the notifier to SMTP
When you have a transactional sender (Mailgun, Postmark, Amazon SES, your
own postfix), replace the notifier: block in authelia/configuration.yml:
notifier:
disable_startup_check: false
smtp:
address: 'smtps://smtp.example.com:465'
username: 'authelia@example.com'
sender: 'Authelia <authelia@example.com>'
subject: '[Authelia] {title}'
# Password loaded via AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
Add the password file:
echo 'your_smtp_password' > authelia/secrets/SMTP_PASSWORD
chmod 600 authelia/secrets/SMTP_PASSWORD
And add to docker-compose.yml under the authelia service environment::
- AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/secrets/SMTP_PASSWORD
Restart and verify with docker compose logs authelia -- expect a
"Notifier SMTP startup check successful" line.
Security notes
.env,authelia/secrets/*,authelia/users_database.yml,authelia/db.sqlite3*, and the notifications file are all gitignored. Verify withgit statusbefore every commit.- Authelia is not port-mapped to the host. Only containers on
caddy_netcan reach it, and only Caddy is configured to forward unauthenticated traffic to it viaforward_auth. - The TOTP secrets in the SQLite DB are encrypted at rest with
STORAGE_ENCRYPTION_KEY. Lose that file and you lose every user's TOTP -- back it up alongside the DB. regulationis per-user; fail2ban is per-IP. Both are on by default.- The shipped
caddy-4xxfilter ignoresfavicon.ico,robots.txt, and Apple touch icons so accidentally-missing static assets don't ban your own browser. Add toignoreregexif other false-positives show up infail2ban-regextesting.
What's next
Once this is steady-state:
- Add OIDC clients in Authelia for apps that speak OIDC natively (Grafana, Gitea, etc.) -- they'll do real SSO without forward-auth headers.
- Switch the filesystem notifier to SMTP (see above).
- Consider a backup job for
authelia/db.sqlite3andauthelia/secrets/-- losing either is a recovery mess.