# 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.` 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 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 ..." ``` ## Wire Caddy into Authelia Open `caddy/snippet.example.caddyfile`. It defines: - `(authelia)` -- a reusable snippet: `import authelia` in any site block to gate it. - `(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 protected blocks for `cam.example.com` and `doorbell.example.com`. 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 protected domain, also add a rule under `access_control.rules` in `authelia/configuration.yml` (Authelia's default policy is `deny` -- a domain with no rule will not authenticate). Restart Authelia after editing: ```bash docker compose restart authelia ``` ### 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./` -- 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 ` 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.` 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 ' 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.