- 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
449 lines
16 KiB
Markdown
449 lines
16 KiB
Markdown
# 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-USER` chain
|
|
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,
|
|
change `caddy_net` everywhere in this repo.
|
|
- A root domain you control (the examples use `example.com`). DNS records
|
|
for `auth.<root>` and every protected subdomain should point at the
|
|
Caddy host's public IP.
|
|
- Caddy v2.5.1 or newer (for `forward_auth` directive).
|
|
|
|
## First-run setup
|
|
|
|
```bash
|
|
# 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`, no `import 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 authelia` in any site block
|
|
you want gated.
|
|
- `(accesslog)` -- writes Caddy's JSON access log to `/var/log/caddy/access.log`
|
|
so 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
services:
|
|
caddy:
|
|
volumes:
|
|
- /var/log/caddy:/var/log/caddy
|
|
```
|
|
|
|
Make sure the host directory exists and is writable by Caddy's UID:
|
|
|
|
```bash
|
|
sudo mkdir -p /var/log/caddy
|
|
sudo chown -R 1000:1000 /var/log/caddy # adjust UID to match your Caddy
|
|
```
|
|
|
|
## First login + TOTP enrollment
|
|
|
|
1. Visit any protected subdomain in a private browser window.
|
|
2. Caddy bounces you to `https://auth.<your-domain>/` -- log in with the
|
|
username and plaintext password you set above.
|
|
3. 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.
|
|
4. On first registration Authelia tries to email you a confirmation link.
|
|
The filesystem notifier writes it to a file -- grab it with:
|
|
```bash
|
|
docker compose exec authelia cat /config/notifications/notification.txt
|
|
```
|
|
Click that link to confirm registration.
|
|
5. 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
```bash
|
|
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`:
|
|
|
|
```yaml
|
|
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:
|
|
```bash
|
|
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:`:
|
|
```yaml
|
|
- 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 with `git status` before every commit.
|
|
- Authelia is *not* port-mapped to the host. Only containers on
|
|
`caddy_net` can reach it, and only Caddy is configured to forward
|
|
unauthenticated traffic to it via `forward_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.
|
|
- `regulation` is per-user; fail2ban is per-IP. Both are on by default.
|
|
- The shipped `caddy-4xx` filter ignores `favicon.ico`, `robots.txt`, and
|
|
Apple touch icons so accidentally-missing static assets don't ban your
|
|
own browser. Add to `ignoreregex` if other false-positives show up in
|
|
`fail2ban-regex` testing.
|
|
|
|
## 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.sqlite3` and `authelia/secrets/`
|
|
-- losing either is a recovery mess.
|