From 1b4c9298e1bc2a3a8452bca2c6ca6a375660ec60 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 02:46:42 +0000 Subject: [PATCH] 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 --- README.md | 531 ++++++++++++++++---------------- authelia/configuration.yml | 90 +++--- caddy/Caddyfile | 203 ++++++++++++ caddy/snippet.example.caddyfile | 144 --------- docker-compose.yml | 16 +- 5 files changed, 529 insertions(+), 455 deletions(-) create mode 100644 caddy/Caddyfile delete mode 100644 caddy/snippet.example.caddyfile diff --git a/README.md b/README.md index d19e6e8..9ab0e85 100644 --- a/README.md +++ b/README.md @@ -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.` 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.` 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 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./` -- 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 ` 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.` - -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 ' 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). diff --git a/authelia/configuration.yml b/authelia/configuration.yml index 3f20b9a..605ac5c 100644 --- a/authelia/configuration.yml +++ b/authelia/configuration.yml @@ -25,7 +25,7 @@ server: log: level: 'info' - format: 'text' # fail2ban filter expects text format + format: 'text' # fail2ban filter expects text format -- don't change to json file_path: '/config/authelia.log' keep_stdout: true # also log to stdout for `docker logs` @@ -63,60 +63,76 @@ authentication_backend: # --------------------------------------------------------------------------- # Access control # -# default_policy: deny means every domain that Caddy forwards here must -# have an explicit allow rule. Caddy only forwards when a site block has -# `import authelia`, so domains where you keep the app's own auth (no -# `import authelia` in Caddy) bypass Authelia entirely and don't need -# rules here. +# default_policy: deny -- every domain Caddy forward_auths here must have +# an explicit rule. Domains with no `import authelia` in Caddy never reach +# Authelia at all, so the deny doesn't apply to them. # # Policies: # bypass Authelia waves the request through (used for the portal) # one_factor password only # two_factor password + TOTP # -# Decision tree for whether a site needs an Authelia rule at all: +# --------------------------------------------------------------------------- +# WHICH SITES NEED A RULE HERE? # -# - App has NO built-in auth (doorbell PTT page) -> rule here + -# `import authelia` in Caddy. Use two_factor for anything that -# controls hardware in the house. +# There are four ways a site can relate to Authelia: # -# - App has built-in auth but supports trusted-header proxy auth -# (Frigate 0.14+, Grafana, Gitea, Jellyfin, ...) -> rule here + -# `import authelia` in Caddy AND switch the app to proxy auth in -# its own config. Single login (Authelia), 2FA, app keeps its own -# user/role mapping driven from Authelia headers. +# CASE 1 -- App has NO built-in auth (e.g. Pi doorbell PTT page). +# -> Rule required + `import authelia` in Caddy. +# -> Authelia is the ONLY login. Use two_factor for hardware-control pages. # -# - App has built-in auth and CAN'T switch (router admin, legacy -# things) -> NO rule here, NO `import authelia` in Caddy. The -# traffic skips Authelia entirely; the app handles its own login. +# CASE 2 -- App has built-in auth AND supports trusted-header proxy auth +# (Frigate 0.14+, Grafana, Gitea, Nextcloud, Home Assistant ...). +# -> Rule required + `import authelia` in Caddy + disable the app's login +# form in its own config (see README.md per-app instructions). +# -> Single Authelia login covers both access AND the app's user/role +# mapping via Remote-User / Remote-Groups headers. +# +# CASE 3 -- App has built-in auth and CANNOT switch to proxy auth, but you +# still want a 2FA gate in front of it (extra security layer). +# -> Rule required + `import authelia` in Caddy. +# -> User logs into Authelia (2FA) THEN into the app's own login form. +# Two separate logins -- the app auth is unchanged. +# -> Useful for: router admin pages, NAS UIs, any legacy app. +# +# CASE 4 -- App handles its own auth and you don't want Authelia involved. +# -> NO rule here + NO `import authelia` in Caddy. +# -> Traffic skips Authelia entirely; the app handles everything. # --------------------------------------------------------------------------- access_control: default_policy: 'deny' rules: + # The Authelia portal itself is always bypass. - domain: 'auth.example.com' # CHANGE policy: 'bypass' - # ----- Apps with NO built-in auth: Authelia is the only gate ----- - # The Pi doorbell PTT page has no app-level auth, so Authelia is it. - # 2FA is appropriate -- this URL controls a speaker in your house. - # - domain: 'doorbell.example.com' + # ------------------------------------------------------------------- + # CASE 1: No app auth -- Authelia is the only gate. + # The Pi doorbell PTT page has no built-in authentication. + # two_factor is appropriate -- this URL controls a speaker in your house. + # ------------------------------------------------------------------- + # - domain: 'doorbell.example.com' # CHANGE # policy: 'two_factor' - # ----- Apps that switched FROM their own auth TO Authelia ----- - # Frigate 0.14+ supports trusted-header proxy auth. To use this: - # 1. In frigate_config/config.yml, set `auth.enabled: False` - # and add a `proxy:` block with `header_map.user: remote-user` - # and `header_map.role: remote-groups` (see README). - # 2. `import authelia` in the cam.* Caddy block. - # 3. Add the rule below. - # - domain: 'cam.example.com' + # ------------------------------------------------------------------- + # CASE 2: App supports trusted-header proxy auth -- replace app login. + # Frigate 0.14+: set `auth.enabled: False` and configure `proxy:` in + # frigate_config/config.yml (see README.md "Switching Frigate to Authelia"). + # Single login: Authelia authenticates, Frigate reads Remote-User/Groups. + # ------------------------------------------------------------------- + # - domain: 'cam.example.com' # CHANGE # policy: 'two_factor' - # ----- Apps that KEEP their own auth ----- - # Don't add a rule and don't `import authelia` in their Caddy block. - # Example: a router admin page on `router.example.com` -- no rule - # appears here, the request never reaches Authelia. + # ------------------------------------------------------------------- + # CASE 3: App keeps its own auth; Authelia adds a 2FA gate in front. + # The app's login form is still shown after Authelia passes the request. + # User logs into Authelia (2FA) then into the app separately. + # ------------------------------------------------------------------- + # - domain: 'nas.example.com' # CHANGE/REMOVE example + # policy: 'two_factor' + + # CASE 4: No rule here, no `import authelia` in Caddy. App handles auth. session: # secret loaded via AUTHELIA_SESSION_SECRET_FILE @@ -130,7 +146,9 @@ session: remember_me: '1 month' same_site: 'lax' -# In-app rate limiting. First line of defense; fail2ban is the second. +# In-app rate limiting. Locks the user account after repeated failures. +# fail2ban is the second line of defense: it bans the source IP. +# Together: Authelia locks the *user*, fail2ban bans the *IP*. regulation: max_retries: 3 find_time: '2 minutes' @@ -143,7 +161,7 @@ storage: # Filesystem notifier -- password reset / new device emails get written to # a file you can `tail -f`. Swap to `smtp:` when you wire up a real -# transactional sender. +# transactional sender (see README.md "Switching the notifier to SMTP"). notifier: disable_startup_check: false filesystem: diff --git a/caddy/Caddyfile b/caddy/Caddyfile new file mode 100644 index 0000000..887c034 --- /dev/null +++ b/caddy/Caddyfile @@ -0,0 +1,203 @@ +# ============================================================================= +# Caddyfile -- Authelia + fail2ban integration +# +# Copy this file into your Caddy setup (or merge the relevant blocks into +# your existing Caddyfile), edit all placeholders, then reload: +# +# # System Caddy: +# sudo caddy validate --config /etc/caddy/Caddyfile +# sudo systemctl reload caddy +# +# # Dockerized Caddy: +# docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile +# +# Placeholders to replace: +# example.com -> your real root domain +# 192.168.x.x -> real upstream LAN IPs +# +# Requirements: +# - Caddy v2.5.1+ (for `forward_auth` directive) +# - Caddy must be on the `caddy_net` Docker network so it can resolve +# `authelia` by container name. In your Caddy compose: +# networks: [caddy_net] +# and at the bottom: +# networks: +# caddy_net: +# external: true +# +# ============================================================================= +# DECISION TREE -- which sites go behind Authelia? +# +# CASE 1 -- App has NO built-in auth (e.g. Pi doorbell PTT page). +# `import authelia` + rule in authelia/configuration.yml. +# Authelia is the ONLY login. Use two_factor for hardware-control pages. +# +# CASE 2 -- App has built-in auth AND supports trusted-header proxy auth +# (Frigate 0.14+, Grafana, Gitea, Nextcloud, Home Assistant, ...). +# `import authelia` + rule in Authelia + disable the app's own login form. +# Single Authelia login: Authelia authenticates, app reads Remote-User header. +# +# CASE 3 -- App has built-in auth and CANNOT switch to proxy auth, but you +# want a 2FA gate in front anyway (router admin, legacy apps, etc.). +# `import authelia` + rule in Authelia. App auth is untouched. +# User logs into Authelia (2FA), then the app's own login form appears. +# +# CASE 4 -- App handles its own auth; Authelia not involved. +# Plain `reverse_proxy`, no `import authelia`, no Authelia rule. +# Traffic skips Authelia entirely. +# +# fail2ban coverage: import (accesslog) in EVERY site block -- gated or not. +# The caddy-4xx jail watches /var/log/caddy/access.log and bans scanners +# spraying all your subdomains, not just the Authelia-gated ones. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# (authelia) -- forward_auth gate. +# Import into any site block you want gated (cases 1, 2, 3). +# On success Authelia sets Remote-User, Remote-Groups, Remote-Email, +# Remote-Name headers that the upstream app can consume for role mapping. +# ----------------------------------------------------------------------------- +(authelia) { + forward_auth authelia:9091 { + uri /api/authz/forward-auth + copy_headers Remote-User Remote-Groups Remote-Email Remote-Name + } +} + +# ----------------------------------------------------------------------------- +# (accesslog) -- structured JSON access log consumed by fail2ban's caddy-4xx +# jail. Import into EVERY site block so fail2ban covers your whole stack. +# +# Pre-create the log directory before starting Caddy: +# sudo mkdir -p /var/log/caddy +# sudo chown caddy:caddy /var/log/caddy # system Caddy +# # Dockerized Caddy: add volumes: ["/var/log/caddy:/var/log/caddy"] to compose +# ----------------------------------------------------------------------------- +(accesslog) { + log { + output file /var/log/caddy/access.log { + roll_size 10MiB + roll_keep 5 + roll_keep_for 720h + } + format json + } +} + +# ============================================================================= +# Authelia login portal +# Never add `import authelia` here -- the `bypass` rule in +# access_control.rules handles the portal itself. Adding forward_auth here +# would cause a redirect loop. +# ============================================================================= +auth.example.com { # CHANGE + import accesslog + reverse_proxy authelia:9091 +} + +# ============================================================================= +# CASE 1: Pi doorbell PTT page -- Authelia is the ONLY auth. +# +# The Pi's Flask server has no built-in authentication. Authelia gates it. +# two_factor is appropriate -- this URL controls a speaker in your house. +# Comment out until the Pi is deployed. +# Also add (or uncomment) the doorbell.example.com rule in configuration.yml. +# ============================================================================= +# doorbell.example.com { # CHANGE +# import accesslog +# import authelia +# +# # Same-origin proxy to Frigate so WebRTC fetch works without CORS. +# handle_path /frigate/* { +# reverse_proxy 192.168.x.x:8971 { +# transport http { +# read_timeout 60s +# write_timeout 60s +# } +# } +# } +# +# handle { +# reverse_proxy 192.168.x.x:5555 +# } +# } + +# ============================================================================= +# CASE 2: Frigate UI -- Authelia replaces Frigate's own login form. +# +# Frigate 0.14+ supports trusted-header proxy auth. Authelia authenticates +# the user (optionally with TOTP 2FA), then passes Remote-User and +# Remote-Groups headers to Frigate which maps them to admin/viewer roles. +# +# To enable proxy auth in Frigate, edit frigate_config/config.yml: +# +# auth: +# enabled: False +# trusted_proxies: +# - 172.18.0.0/16 # caddy_net subnet; find it with: +# # docker network inspect caddy_net +# proxy: +# header_map: +# user: remote-user # matches copy_headers in (authelia) snippet +# role: remote-groups +# default_role: viewer +# separator: '|' +# # Optional shared secret -- prevents LAN header spoofing. +# # Generate: openssl rand -hex 32 +# # Set the same value as header_up X-Proxy-Secret below. +# # auth_secret: 'your-32-byte-hex' +# +# Then uncomment the cam.example.com rule in authelia/configuration.yml +# and restart: docker compose restart authelia (in the authelia stack) +# docker compose restart frigate (in the camera stack) +# ============================================================================= +cam.example.com { # CHANGE + import accesslog + import authelia + + reverse_proxy 192.168.x.x:8971 { # CHANGE IP + transport http { + read_timeout 60s + write_timeout 60s + } + # Uncomment if you set auth_secret: in Frigate's proxy: block. + # header_up X-Proxy-Secret "your-32-byte-hex-here" + } +} + +# ============================================================================= +# CASE 3: App keeps its own login; Authelia adds a 2FA gate in front. +# +# Use when an app can't do proxy auth but you still want 2FA before it. +# The user authenticates with Authelia (2FA), then the app's own login +# form appears. Two separate logins -- the app's auth is untouched. +# +# Also add a rule in authelia/configuration.yml: +# - domain: 'nas.example.com' +# policy: 'two_factor' +# ============================================================================= +# nas.example.com { # CHANGE/REMOVE example +# import accesslog +# import authelia +# +# reverse_proxy 192.168.x.x:PORT { # CHANGE +# transport http { +# tls_insecure_skip_verify # only if self-signed TLS +# } +# } +# } + +# ============================================================================= +# CASE 4: App handles its own auth; Authelia not involved. +# +# No `import authelia`. No access_control rule in Authelia. +# Still import accesslog so fail2ban's caddy-4xx jail covers this site. +# ============================================================================= +# router.example.com { # CHANGE/REMOVE example +# import accesslog +# reverse_proxy 192.168.x.x:PORT { # CHANGE +# transport http { +# tls_insecure_skip_verify +# } +# } +# } diff --git a/caddy/snippet.example.caddyfile b/caddy/snippet.example.caddyfile deleted file mode 100644 index f3fe3cf..0000000 --- a/caddy/snippet.example.caddyfile +++ /dev/null @@ -1,144 +0,0 @@ -# ============================================================================= -# Authelia + Caddy integration snippets -# -# Merge these blocks into your real Caddyfile (typically the one your -# dockerized Caddy mounts from its own ~/docker/caddy/ folder). Reload Caddy -# after editing: -# docker compose -f ~/docker/caddy/docker-compose.yml exec caddy \ -# caddy reload --config /etc/caddy/Caddyfile -# -# Requires: -# - Caddy v2.5.1 or newer -# - Caddy joined to the external `caddy_net` docker network so it can -# resolve `authelia` by container name -# -# ----------------------------------------------------------------------------- -# DECISION TREE: which sites go behind Authelia? -# -# 1. App has NO built-in auth (e.g. the Pi doorbell PTT page). -# -> `import authelia` here AND add a rule in -# authelia/configuration.yml access_control.rules. Use -# two_factor for anything that controls hardware. -# -# 2. App has built-in auth AND supports trusted-header proxy auth -# (Frigate 0.14+, Grafana, Gitea, Jellyfin, Portainer, ...). -# -> `import authelia` here, add a rule in Authelia, AND switch -# the app's own config to consume Remote-User from upstream -# (disable its built-in login form). One login, 2FA, app -# still owns its user/role mapping. See the cam.* example -# and the README for Frigate specifics. -# -# 3. App has built-in auth and CAN'T switch (router admin pages, -# odd legacy things). -# -> Plain `reverse_proxy` block. NO `import authelia`, NO -# Authelia rule. The traffic skips Authelia entirely. -# ============================================================================= - -# ----------------------------------------------------------------------------- -# Reusable forward_auth snippet for cases (1) and (2). Import into any site -# block you want gated by Authelia. -# ----------------------------------------------------------------------------- -(authelia) { - forward_auth authelia:9091 { - uri /api/authz/forward-auth - copy_headers Remote-User Remote-Groups Remote-Email Remote-Name - } -} - -# ----------------------------------------------------------------------------- -# Caddy access logging -- fail2ban needs JSON access logs at a host path -# both Caddy and fail2ban can see. Mount /var/log/caddy in BOTH compose -# files (Caddy as rw, fail2ban as ro). The roll directives keep it bounded. -# ----------------------------------------------------------------------------- -(accesslog) { - log { - output file /var/log/caddy/access.log { - roll_size 10MiB - roll_keep 5 - roll_keep_for 720h - } - format json - } -} - -# ============================================================================= -# Site blocks -# ============================================================================= - -# ----------------------------------------------------------------------------- -# Authelia login portal -- always bypass in access_control.rules. -# ----------------------------------------------------------------------------- -auth.example.com { - import accesslog - reverse_proxy authelia:9091 -} - -# ----------------------------------------------------------------------------- -# CASE 1: app has NO built-in auth. -# Pi doorbell PTT page -- Authelia is the only gate. two_factor in Authelia. -# ----------------------------------------------------------------------------- -doorbell.example.com { - import accesslog - import authelia - - handle_path /frigate/* { - reverse_proxy 192.168.x.x:8971 { - transport http { - read_timeout 60s - write_timeout 60s - } - } - } - - handle { - reverse_proxy 192.168.x.x:5555 - } -} - -# ----------------------------------------------------------------------------- -# CASE 2: app supports trusted-header proxy auth. -# Frigate UI -- Authelia gates access AND Frigate consumes Remote-User from -# upstream so its own role mapping (admin/viewer) still works. To use this, -# also edit frigate_config/config.yml: -# -# auth: -# enabled: False -# trusted_proxies: -# - 172.18.0.0/16 # your caddy_net subnet, see README -# proxy: -# header_map: -# user: remote-user -# role: remote-groups -# default_role: viewer -# separator: '|' -# # Optional but recommended when Caddy and Frigate are on different -# # hosts/VLANs. Generate with `openssl rand -hex 32` and add the -# # matching `header_up X-Proxy-Secret ` below. -# # auth_secret: 'paste-32-byte-hex-here' -# ----------------------------------------------------------------------------- -cam.example.com { - import accesslog - import authelia - reverse_proxy 192.168.x.x:8971 { - transport http { - read_timeout 60s - write_timeout 60s - } - # Uncomment and match Frigate's auth_secret if you set one above. - # header_up X-Proxy-Secret "paste-same-32-byte-hex-here" - } -} - -# ----------------------------------------------------------------------------- -# CASE 3: app keeps its own auth (no Authelia involvement). -# Example: a router admin page or a service that can't do proxy auth. -# Do NOT add `import authelia` and do NOT add an access_control rule for it. -# ----------------------------------------------------------------------------- -# router.example.com { -# import accesslog -# reverse_proxy 192.168.1.1:443 { -# transport http { -# tls_insecure_skip_verify -# } -# } -# } diff --git a/docker-compose.yml b/docker-compose.yml index 0855c58..57e03cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,14 +60,20 @@ services: - F2B_DB_PURGE_AGE=7d volumes: - ./fail2ban/data:/data - # Authelia log -- read-only mount so fail2ban can parse 1FA/TOTP - # failures. Authelia writes this under /config which is ./authelia. + # Authelia text log -- fail2ban watches this for 1FA/TOTP failures. + # Authelia writes it to /config/authelia.log = ./authelia/authelia.log. + # IMPORTANT: `touch authelia/authelia.log` before first `docker compose up` + # so Docker creates it as a file, not a directory (see README.md). - ./authelia/authelia.log:/var/log/authelia/authelia.log:ro - # Caddy access log -- you must configure your Caddyfile to write - # JSON access logs to this host path. See README.md. + # Caddy JSON access log -- covers ALL sites that import (accesslog), + # not just Authelia-gated ones. Mount the host directory read-only. + # Your Caddy must write here; see README.md "Caddy access log path". - /var/log/caddy:/var/log/caddy:ro + # Wait for Authelia to pass its healthcheck before starting, so the + # authelia.log file exists before fail2ban tries to bind-mount it. depends_on: - - authelia + authelia: + condition: service_healthy networks: caddy_net: