Update authelia stack: 4 auth cases, full Caddyfile, improved docs

Keeps this as a standalone authelia+fail2ban stack (no Frigate services).

Changes:
- docker-compose.yml: fail2ban depends_on authelia with service_healthy
  condition so authelia.log exists before fail2ban tries to bind-mount it;
  add inline note about pre-creating the log file
- authelia/configuration.yml: expand access_control comment block to cover
  all 4 cases (added Case 3: app keeps own auth + Authelia as 2FA gate,
  and Case 4: app handles auth alone); clearer per-case commented rules
- caddy/Caddyfile (replaces snippet.example.caddyfile): complete Caddyfile
  with all 4 auth-case examples; (accesslog) imported in every block so
  fail2ban caddy-4xx jail covers all subdomains, not just gated ones;
  full inline docs for enabling Frigate proxy auth
- README.md: expand "Which sites" from 3 to 4 cases; add proxy-auth service
  compatibility table (Frigate, Grafana, Gitea, Nextcloud, HA, Portainer
  etc.); clarify fail2ban covers all sites via single caddy-4xx jail;
  add touch authelia/authelia.log to first-run; add troubleshooting entries
  for authelia.log bind-mount directory bug and fail2ban chain verification

https://claude.ai/code/session_012eTokAaGiZo7aGt1T2W9BC
This commit is contained in:
Claude
2026-04-26 02:46:42 +00:00
parent 3c2bb275ee
commit 1b4c9298e1
5 changed files with 529 additions and 455 deletions
+261 -270
View File
@@ -1,9 +1,8 @@
# 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`.
(fail2ban). Sits next to your dockerized Caddy and gates every public
subdomain behind a single sign-on portal at `auth.example.com`.
```
Internet
@@ -15,168 +14,195 @@ etc.) behind a single sign-on portal at `auth.example.com`.
| reverse_proxy +--------+-----------+
| |
v v
Frigate (IoT VLAN), ntfy, etc. ./authelia/db.sqlite3
Frigate (LAN), NAS, Pi, etc. ./authelia/db.sqlite3
./authelia/authelia.log
^
| tail
+------+--------+
| fail2ban | host net
| DOCKER-USER | +iptables
| 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`).
- File-backed users database, SQLite storage, no Redis, no external DB.
- Filesystem notifier for password reset (swap to SMTP later, one block change).
- fail2ban bans via the `DOCKER-USER` iptables chain: drops happen at the host
edge before traffic reaches any docker-published port.
- Caddy is not in this stack. Copy `caddy/Caddyfile` into your Caddy setup.
## 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
authelia-stack/
├── docker-compose.yml
├── .env.example # copy to .env
├── .gitignore
├── README.md
├── authelia/
├── configuration.yml # main config -- edit your domain here
├── users_database.yml.example # copy to users_database.yml (gitignored)
├── secrets/ # gitignored; secret files mounted as /secrets
└── notifications/ # filesystem notifier writes here (gitignored)
├── fail2ban/
└── data/ # mounted as /data in the container
├── filter.d/
├── authelia.local # matches Authelia text-log auth failures
└── caddy-4xx.local # matches Caddy JSON 4xx responses
└── jail.d/
├── authelia.local # 3 fails/10 min -> 1 hr IP ban
└── caddy.local # 30 fails/2 min -> 30 min IP ban
└── caddy/
└── Caddyfile # copy/merge into your Caddy setup
```
## 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).
- Docker + docker compose v2.
- Caddy already running, in Docker, joined to an external network named
`caddy_net`. If your network is named differently, replace `caddy_net`
everywhere in this repo.
- Caddy v2.5.1 or newer (needed for the `forward_auth` directive).
- A root domain you control. DNS A records for `auth.<root>` and every
protected subdomain must point at the Caddy host's public IP.
## Which sites go behind Authelia?
There are four ways a site can relate to Authelia. Pick one per site.
| Case | App has built-in auth? | Supports proxy auth? | What to do |
|------|------------------------|----------------------|------------|
| **1** | No | n/a | `import authelia` in Caddy + rule in Authelia. Authelia is the only login. |
| **2** | Yes | Yes | `import authelia` in Caddy + rule in Authelia + disable app's own login form. Single login. |
| **3** | Yes | No | `import authelia` in Caddy + rule in Authelia. App auth is unchanged. User logs into Authelia then the app. Two logins. |
| **4** | Yes | — | Plain `reverse_proxy`. No `import authelia`, no rule. App handles auth. |
Concretely:
- **`doorbell.example.com`** (Pi PTT page) -- **case 1**. No app auth at all.
Authelia is the only gate. Use `two_factor` -- this URL controls a speaker.
- **`cam.example.com`** (Frigate UI) -- **case 2**. Frigate 0.14+ supports
proxy auth. Disable Frigate's login form and let Authelia drive both the
access gate and the role mapping (admin vs. viewer) via headers.
- **Router admin / NAS UI** -- **case 3** if you want a 2FA gate in front,
**case 4** if you just leave it to the app.
Default policy in `configuration.yml` is `deny`, so a domain with no rule
AND no `import authelia` in Caddy never reaches Authelia at all.
### How to tell if an app supports proxy auth (case 2)
Look for any of these in the app's docs:
- "Remote-User header", "trusted upstream", "trusted proxies"
- "Header-based authentication", "SSO via reverse proxy"
- Support for `X-Forwarded-User`, `X-Remote-User`, or `Remote-User`
| App | Proxy auth? | Notes |
|-----|-------------|-------|
| Frigate 0.14+ | Yes | `auth.enabled: False` + `proxy:` block in config.yml |
| Grafana | Yes | `[auth.proxy]` section in grafana.ini |
| Gitea / Forgejo | Yes | `REVERSE_PROXY_AUTHENTICATION_USER` in app.ini |
| Nextcloud | Yes | `TRUSTED_PROXIES` env + `overwriteprotocol = https` |
| Home Assistant | Yes | `trusted_networks` auth provider + `use_x_forwarded_for` |
| Jellyfin | Partial | Community plugin required |
| Portainer | No | Use Authelia OIDC integration instead |
| Vaultwarden | No | Use Authelia OIDC integration instead |
| Router/NAS admin | Rarely | Use case 3 (2FA gate) or case 4 |
## 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.
# 1) Create the external docker network (Caddy must also be on this).
docker network create caddy_net 2>/dev/null || true
# 2) Bootstrap the secrets directory
# 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
# 3) Copy and edit .env.
cp .env.example .env
$EDITOR .env # set TZ; pin AUTHELIA_VERSION if you want
$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)
# 4) Edit authelia/configuration.yml.
# Replace every `example.com` with your real root domain.
# Look for the four CHANGE comments: totp.issuer, session.cookies[].domain,
# session.cookies[].authelia_url, session.cookies[].default_redirection_url.
# Also uncomment access_control.rules entries for the sites you want to gate.
$EDITOR authelia/configuration.yml
# 5) Create your first user
# 5) Create your first user.
cp authelia/users_database.yml.example authelia/users_database.yml
$EDITOR authelia/users_database.yml # set username, email, displayname
$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 $argon2id$... output into the password: field.
# Paste the resulting `$argon2id$v=19$m=...` into the user's `password:`.
# 6) Pre-create the Authelia log file.
# Docker creates a DIRECTORY at the bind-mount path if the file doesn't
# exist, which breaks fail2ban's mount. Create it as an empty file first.
touch authelia/authelia.log
# 6) Validate the config before starting (catches typos / schema issues)
# 7) Validate config before starting.
docker compose run --rm authelia \
authelia validate-config --config /config/configuration.yml
# Expect: "Configuration: validation complete" with no errors.
# 7) Bring it up
# 8) Wire up Caddy (see "Wire Caddy into Authelia" below).
# 9) Bring it up.
docker compose up -d
docker compose logs -f authelia # expect "Authelia is listening on ..."
docker compose logs -f authelia # expect "Authelia is listening on ..."
docker compose logs -f fail2ban # expect "Jail authelia is now active"
```
## 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:
Open `caddy/Caddyfile`. It defines:
- `(authelia)` -- reusable snippet: `import authelia` in any site block
you want gated.
- `(authelia)` -- reusable snippet: add `import authelia` to any site block.
- `(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.
so fail2ban's `caddy-4xx` jail can watch it.
- `auth.example.com` -- the Authelia portal.
- Example site blocks for all four cases (cases 1-3 active, case 4 commented).
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:
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
**Every** site block should have `import accesslog` -- even case 4 sites.
fail2ban's caddy-4xx jail watches the one log file and covers all your
subdomains automatically. Scanners spray everything, not just gated sites.
### Caddy access log path
fail2ban mounts `/var/log/caddy` from the host as read-only. Your Caddy
service must write to the same path. In your Caddy compose:
```yaml
services:
caddy:
volumes:
- /var/log/caddy:/var/log/caddy
```
For each case-1 or case-2 domain, also add a rule under `access_control.rules`
in `authelia/configuration.yml`. Restart Authelia after editing:
Create the directory before starting:
```bash
docker compose restart authelia
sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy # adjust to your Caddy UID
```
### Switching Frigate to Authelia (case 2)
## 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:
Edit `frigate_config/config.yml` in your Frigate stack:
```yaml
auth:
@@ -187,92 +213,83 @@ auth:
proxy:
header_map:
user: remote-user # what Authelia sends; matches `copy_headers` in Caddy
user: remote-user # matches `copy_headers Remote-User` in (authelia) snippet
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'
# Optional shared secret -- prevents LAN header spoofing.
# Generate: openssl rand -hex 32
# Set the same value as `header_up X-Proxy-Secret` in caddy/Caddyfile.
# auth_secret: 'your-32-byte-hex'
```
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:
Then uncomment `cam.example.com` in `authelia/configuration.yml`, restart
both services:
```bash
sudo mkdir -p /var/log/caddy
sudo chown -R 1000:1000 /var/log/caddy # adjust UID to match your Caddy
docker compose restart authelia
docker compose restart frigate # in your Frigate stack
```
Verify: `https://cam.example.com` in a private window goes to Authelia and
back without a Frigate login screen.
## 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:
1. Visit a protected subdomain in a private browser window.
2. Caddy bounces you to `https://auth.example.com` -- log in with your
username and plaintext password.
3. If the rule is `two_factor`, Authelia prompts you to register a second
factor. Pick **TOTP** and scan the QR with your authenticator app.
4. Authelia writes a confirmation link to the filesystem notifier file:
```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.
Click the link to confirm TOTP registration.
5. Enter the TOTP code -- you're in. The `authelia_session` cookie is scoped
to your root domain and covers every protected subdomain automatically.
## 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.
Append to `authelia/users_database.yml`, generate a hash:
```bash
docker compose run --rm authelia \
authelia crypto hash generate argon2 --password 'new-password'
```
Paste the hash as `password:`. Restart or wait 5 minutes for auto-reload.
### Disable a user
Set `disabled: true` on their entry and restart Authelia.
Set `disabled: true` on their entry. Takes effect at next refresh.
### Reset their TOTP
### Reset TOTP (force re-enrollment)
```bash
docker compose exec authelia \
authelia storage user totp delete --username yourname \
authelia storage user totp delete --username USERNAME \
--config /config/configuration.yml
```
They will be prompted to re-enroll on next login.
## fail2ban
### Verify it's running and watching the right files
fail2ban does **not** need its own separate stack or compose file. It lives
alongside Authelia in this same `docker-compose.yml`. It uses host networking
(no docker network needed) and watches two log sources:
| Jail | Log | Trigger | Ban |
|------|-----|---------|-----|
| `authelia` | `./authelia/authelia.log` | 3 failed logins in 10 min | 1 hour |
| `caddy-4xx` | `/var/log/caddy/access.log` | 30 HTTP 4xx in 2 min | 30 min |
The **caddy-4xx jail covers every site** on your Caddyfile as long as each
block has `import accesslog`. You don't need per-site jails.
Defense in depth: Authelia's `regulation` block locks the *user account*
after 3 bad passwords. fail2ban bans the *source IP* independently.
### Verify jails are active
```bash
docker compose exec fail2ban fail2ban-client status
@@ -280,27 +297,20 @@ 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
### Test filters against 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.
If nothing matches: confirm `log.format: 'text'` in `authelia/configuration.yml`
and `format json` in the `(accesslog)` snippet in your Caddyfile.
### Manually unban an IP
@@ -309,91 +319,28 @@ 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:
### Tune thresholds
Edit `fail2ban/data/jail.d/*.local`, then:
```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
## Day-to-day
```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
docker compose ps # services running?
docker compose logs -f authelia # follow Authelia
docker compose logs -f fail2ban # follow fail2ban
docker compose pull && docker compose up -d # upgrade images
```
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`.
Bump `AUTHELIA_VERSION` in `.env` when upgrading Authelia. After any
upgrade, re-run `validate-config` -- the schema evolves between releases.
## 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`:
Replace `notifier:` in `authelia/configuration.yml`:
```yaml
notifier:
@@ -403,46 +350,90 @@ notifier:
username: 'authelia@example.com'
sender: 'Authelia <authelia@example.com>'
subject: '[Authelia] {title}'
# Password loaded via AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
# password loaded via AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
```
Add the password file:
Add the secret and wire it up:
```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:`:
Add to the authelia service environment in `docker-compose.yml`:
```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.
Restart and look for `"Notifier SMTP startup check successful"` in logs.
## 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.
- `.env`, `authelia/secrets/*`, `authelia/users_database.yml`, and
`authelia/db.sqlite3*` are all gitignored. Run `git status` before every
commit to confirm nothing sensitive is staged.
- Authelia is not port-mapped to the host. Only containers on `caddy_net`
can reach it; only Caddy is configured to forward_auth there.
- TOTP secrets in the SQLite DB are encrypted at rest with
`STORAGE_ENCRYPTION_KEY`. Back up both the DB and the key file -- losing
either means every user must re-enroll TOTP.
- `regulation` is per-user account lockout; fail2ban is per-IP. Both are on.
## What's next
## Troubleshooting
Once this is steady-state:
### Redirect loop between a site and `auth.example.com`
- 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.
Cookie domain mismatch. The `domain:` under `session.cookies[]` must be the
bare root domain (`example.com`), and every protected site must be a subdomain
of it served over HTTPS. Mixed HTTP/HTTPS won't work; the session cookie is
`Secure`.
### "access denied" with no login prompt
`default_policy: deny` and no `access_control` rule for this domain. Add a
rule in `authelia/configuration.yml` and restart Authelia.
### Authelia container restarts forever
```bash
docker compose logs authelia | head -50
```
Most often: missing/empty secret files in `authelia/secrets/`, bad YAML in
`configuration.yml`, or an invalid argon2 hash in `users_database.yml`.
### 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:
```bash
docker compose up -d caddy
```
### fail2ban bans don't block traffic
fail2ban is writing to the wrong 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
# Should show f2b-* jump rules.
```
### fail2ban: authelia jail missing / "No such file" on authelia.log
Docker created a directory at `./authelia/authelia.log` instead of a file
because the file didn't exist when the container started:
```bash
docker compose down fail2ban
rm -rf authelia/authelia.log # remove the directory Docker created
touch authelia/authelia.log # create as an empty file
docker compose up -d fail2ban
```
### Frigate still shows its own login after switching to proxy auth
1. Confirm `auth.enabled: False` in `frigate_config/config.yml`.
2. Confirm `trusted_proxies` subnet matches your `caddy_net` subnet:
```bash
docker network inspect caddy_net | grep -A2 '"Config"'
```
3. Restart Frigate: `docker compose restart frigate` (in your Frigate stack).