# Authelia + fail2ban Self-hosted authentication portal (Authelia) with an IP-banning sidecar (fail2ban). Sits next to your dockerized Caddy and gates every public subdomain 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 (LAN), NAS, Pi, 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, 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 blocks from `caddy/snippets.caddyfile` into your Caddy setup. ## Repo layout ``` 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/ └── snippets.caddyfile # per-service snippets to add to your existing Caddyfile ``` ## Prerequisites - 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 (for the `forward_auth` directive; tested on v2.11.2). - 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. | | **2a** | Yes | Yes (Remote-User header) | `import authelia` + disable app's own login form. Single login via headers. | | **2b** | Yes | Yes (OIDC) | `import authelia` + configure Authelia as OIDC provider in the app. Single login via token exchange. | | **3** | Yes | No | `import authelia` in Caddy. 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 2a**. Frigate 0.14+ supports proxy auth via `Remote-User` header. Disable Frigate's login form and let Authelia drive both the access gate and the role mapping (admin vs. viewer). - **`books.example.com`** (Audiobookshelf) -- **case 2b**. App redirects to Authelia, Authelia issues a JWT token, app accepts it. No password set in the app itself. - **Router admin / NAS UI** -- **case 3** if you want a 2FA gate in front, **case 4** if you just leave it to the app. ### What is OIDC? OpenID Connect (OIDC) is an identity protocol layered on top of OAuth 2.0. The short version: instead of an app checking your password itself, it redirects you to Authelia, Authelia authenticates you and issues a signed token (JWT), and the app trusts that token. The app never handles your password — it only ever sees the token. Authelia becomes the **identity provider** (IdP). Apps like Audiobookshelf, Immich, Jellyfin, and Mealie become **relying parties** — they trust Authelia's tokens and use them to identify users. The practical difference from proxy-header auth (case 2a): - **Headers**: Caddy adds `Remote-User` to every request and the app reads it. Works silently. Requires the app to support header-based auth. - **OIDC**: The browser does a full redirect dance (app → Authelia → app). Users see the Authelia login page. Requires the app to support OIDC/OAuth. Both result in the same thing: one Authelia credential covers the app. OIDC requires additional setup in `authelia/configuration.yml` — an `identity_providers.oidc` block with a client entry per app, each with its own `client_id` and `client_secret`. See Authelia's OIDC docs for the full config. The Caddy side is identical to case 2a: `import authelia`. Default policy in `configuration.yml` is `deny`, so a domain with no rule AND no `import authelia` in Caddy never reaches Authelia at all. ### Should you use Authelia at all? A password manager with per-service credentials and per-service TOTP is a solid security posture. Authelia improves on it in specific situations: | Situation | Password manager alone | Authelia | |-----------|----------------------|----------| | App has **no auth at all** (Homer, Dozzle, doorbell page) | Can't help | Gates it with 2FA, zero app changes | | App has auth but **no native TOTP** (Uptime Kuma, Syncthing, phpIPAM) | Password-only | Adds 2FA in front for free | | Multiple people need access | Change credentials in N places | Disable one account in Authelia | | True SSO -- log in once, reach 10 services | Still authenticates 10× (autofilled) | One session covers all gated services | | Consistent rate-limiting / brute-force protection | Wildly varies per app | fail2ban + regulation applied uniformly | **Where Authelia is marginal for a single user:** - All your services already have native TOTP support → Authelia adds mostly friction. You're right that 2FA doesn't require Authelia: Vaultwarden, Nextcloud, Grafana, and Gitea all support TOTP natively. If you've already set that up in a manager like Bitwarden, Authelia's 2FA argument is weaker. - The Authelia password itself becomes keys-to-the-kingdom for everything gated behind it, which is why the TOTP requirement on Authelia matters more than on any individual service. **The real sweet spot:** - You have services with zero auth (case 1: Homer, Prometheus, NUT web UI, Gatus) -- something has to gate them. - You have services with auth but no TOTP -- Authelia gives them 2FA without touching the app at all. - You manage access for more than one person. ### 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 auth", "SSO via reverse proxy". | App | Case | Notes | |-----|------|-------| | **Proxy-header auth (case 2a)** | | | | Frigate 0.14+ | 2a | `auth.enabled: False` + `proxy:` block in config.yml | | Grafana | 2a | `[auth.proxy]` in grafana.ini; `GF_AUTH_PROXY_ENABLED=true` | | Gitea / Forgejo | 2a | `ENABLE_REVERSE_PROXY_AUTHENTICATION=true` in app.ini | | Nextcloud | 2a | `trusted_proxies` + `user_external` app + HTTP header auth | | Paperless-ngx | 2a | `PAPERLESS_ENABLE_HTTP_REMOTE_USER=true` | | Miniflux | 2a | `AUTH_PROXY_HEADER=Remote-User` env var | | Home Assistant | 2a | `trusted_networks` auth provider + header forwarding | | BookStack | 2a | `AUTH_METHOD=http` + `HTTP_AUTH_HEADER=Remote-User` in .env | | **OIDC auth (case 2b)** | | | | Audiobookshelf | 2b | Native OIDC; configure in Settings > Authentication | | Jellyfin | 2b | Requires `Jellyfin.Plugin.SSO` from Plugin Catalogue | | Immich | 2b | No header auth; OIDC only. Admin > OAuth settings | | Mealie | 2b | OIDC supported; or use case 3 as a simpler gate | | Portainer | 2b | OIDC in Settings > Authentication; or case 3 as simple gate | | **No built-in auth (case 1)** | | | | Homer / Heimdall | 1 | No auth at all -- Authelia is the only gate | | Dozzle | 1 | No auth by default -- Authelia is the only gate | | Prometheus | 1 | No auth built in; always gate, metrics expose internals | | Alertmanager | 1 | No auth built in | | Gatus | 1 | Status page; optional built-in OIDC but simpler to gate here | | WatchYourLAN | 1 | Network ARP scanner, no built-in auth | | NUT web UI | 1 | NUT daemon has no web UI; frontend web apps vary -- most have no auth | | **App keeps own auth (case 3)** | | | | Uptime Kuma | 3 | No proxy auth, no native TOTP -- Authelia is the only way to add 2FA | | qBittorrent | 3 | Web UI has own auth; no proxy headers | | Plex | 4 ⚠ | Do NOT use `import authelia` -- native clients go through Caddy but cannot complete browser-redirect auth; use Case 4, Plex handles its own auth | | Emby | 4 ⚠ | Same as Plex | | Gotify | 3 | Notification server; own auth | | ntfy | 3 | Notification server; token-based auth | | wg-easy | 3 | WireGuard web UI; WireGuard clients bypass Caddy (UDP 51820) | | Umami | 3 | Analytics; own auth | | phpIPAM | 3 | IP address management; own auth | | Checkmk | 3 | Monitoring; own auth (LDAP in enterprise edition) | | Snipe-IT | 3 | Asset management; own auth | | Zammad | 3 | Help desk / ticketing; own auth | | Lubelog | 3 | Vehicle maintenance; own auth | | UniFi | 3 | Network controller; own auth | | MeshCentral | 3 | Remote management; own auth; OIDC in enterprise builds | | Vaultwarden | 3 or 4 | Strong native auth + TOTP; many skip Authelia here entirely | | Router / NAS admin | 3 or 4 | Depends on firmware; case 4 is usually fine | | **App handles own auth (case 4)** | | | | Syncthing | 4 | Decent native auth; proxy auth not supported | ## Getting git and authenticating to GitHub ### Set your preferred editor first The setup steps below use `$EDITOR` to open config files. It's a standard shell variable that points to whichever editor you like. If it isn't set, running `$EDITOR somefile` fails because the shell tries to execute an empty string. Check whether it's already set: ```bash echo $EDITOR # blank = not set ``` Set it for the current session, or make it permanent by adding the export to `~/.bashrc` (bash) or `~/.zshrc` (zsh): ```bash # Pick one -- whatever you actually have installed: export EDITOR=nano export EDITOR=vim export EDITOR=micro export EDITOR="code --wait" # VS Code (the --wait keeps the terminal paused until you close the file) # Make it permanent: echo 'export EDITOR=nano' >> ~/.bashrc && source ~/.bashrc # or for zsh: echo 'export EDITOR=nano' >> ~/.zshrc && source ~/.zshrc ``` Once set, `$EDITOR somefile` opens that file in your chosen editor every time. ### Install git If git isn't installed on the server yet: ```bash # Debian / Ubuntu / Raspberry Pi OS sudo apt update && sudo apt install -y git # Fedora / RHEL / Rocky / AlmaLinux sudo dnf install -y git ``` The easiest way to authenticate is the **GitHub CLI** (`gh`). Install it by adding GitHub's official apt repository (their Linux equivalent of a Homebrew tap), then run `gh auth login` to authenticate interactively: ```bash # Add the GitHub CLI apt repository sudo apt install -y curl curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) \ signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] \ https://cli.github.com/packages stable main" \ | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null sudo apt update && sudo apt install -y gh # Authenticate -- follow the prompts (browser or paste a token) gh auth login ``` When prompted: choose **GitHub.com**, **HTTPS**, and **Login with a web browser** (or paste a personal access token if the server has no browser). Once done, `gh` passes credentials to `git` automatically -- no extra config needed. On macOS the whole thing is two lines: ```bash brew install git gh gh auth login ``` ## First-run setup ### 0. Clone the repo The Frigate stack lives on `main`. This auth stack is on the `authelia` branch -- clone it separately into its own directory. ```bash git clone -b authelia \ https://github.com/outis1one/frigate_w_audio.git \ ~/docker/authelia ``` ```bash cd ~/docker/authelia ``` ### 1. Create the external Docker network Caddy must join this same network so it can reach Authelia by container name. Skip if `caddy_net` already exists. ```bash docker network create caddy_net 2>/dev/null || true ``` ### 2. Generate secrets Authelia loads these from files so they never appear in `docker inspect` or process listings. ```bash 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. Set your domain `DOMAIN` is the only value you set here. It flows into `authelia/configuration.yml` via Go template substitution and into your Caddyfile via `{env.DOMAIN}` -- no find-and-replace needed anywhere else. ```bash cp .env.example .env ``` ```bash $EDITOR .env ``` Set `DOMAIN=yourdomain.com` and `TZ=Your/Timezone`. Save and close. ### 4. Add access control rules **This step and step 8 (Caddy wiring) must be done together for every site you want to gate. Both are required -- neither alone is enough.** ```bash $EDITOR authelia/configuration.yml ``` Scroll to `access_control.rules`. Uncomment the rule for each site you want to protect and choose a policy: ```yaml - domain: 'cam.{{ env "DOMAIN" }}' policy: 'two_factor' ``` #### Why both sides are required Caddy and Authelia each control one half of the gate: | What you configure | What it does | |--------------------|-------------| | `import authelia` in a Caddy site block | Sends that site's requests to Authelia for a decision | | Rule in `access_control.rules` | Tells Authelia what decision to make | The default policy is `deny`. If a request reaches Authelia with no matching rule, it gets a **403 Forbidden -- no login prompt, no redirect, just blocked**. This is true even for an already-logged-in user. Miss either side and here is what happens: | Caddy `import authelia` | Rule in `configuration.yml` | Result | |------------------------|----------------------------|--------| | Missing | Present | Site is open -- Authelia is never consulted | | Present | Missing | 403 Forbidden, no login prompt | | Both missing | | Site is open -- Authelia is never consulted | | Both present | | Works correctly | #### Which policy to use | Policy | Requires | |--------|---------| | `bypass` | Nothing -- Authelia waves the request through. Used for the portal itself only. | | `one_factor` | Password only | | `two_factor` | Password + TOTP. Use this for everything. | ### 5. Create your first user ```bash cp authelia/users_database.yml.example authelia/users_database.yml ``` ```bash $EDITOR authelia/users_database.yml ``` Fill in `username`, `email`, and `displayname`. Then generate the password hash: ```bash docker compose run --rm authelia \ authelia crypto hash generate argon2 --password 'your-real-password' ``` Copy the `$argon2id$...` line and paste it as the `password:` value in `users_database.yml`. ### 6. Pre-create the Authelia log file Docker creates a **directory** at a bind-mount path if the source file does not exist yet. That breaks fail2ban's read-only mount. Create it as an empty file first: ```bash touch authelia/authelia.log ``` ### 7. Validate the config ```bash docker compose run --rm authelia \ authelia validate-config --config /config/configuration.yml ``` Expect: `Configuration: validation complete` with no errors. Fix any YAML issues before continuing. ### 8. Wire Caddy See [Wire Caddy into Authelia](#wire-caddy-into-authelia) below. Add the Caddy site block for each site alongside the rule you added in step 4. ### 9. Bring it up ```bash docker compose up -d ``` Confirm both services started cleanly: ```bash docker compose logs -f authelia ``` Expect: `Authelia is listening on ...` ```bash docker compose logs -f fail2ban ``` Expect: `Jail authelia is now active` ## Wire Caddy into Authelia Open `caddy/snippets.caddyfile`. It contains copy-paste blocks for your existing Caddyfile, not a replacement for it: - `(authelia)` and `(accesslog)` snippet definitions -- paste once near the top of your Caddyfile. - `auth.{env.DOMAIN}` -- the Authelia portal block. - Per-service examples for all four cases (Frigate, Grafana, Gitea, Uptime Kuma, Homer, etc.) with the required per-app config notes inline. Copy the blocks you need into your real Caddyfile and replace `192.168.x.x` with real upstream IPs. The domain is already templated as `{env.DOMAIN}` -- just make sure the `DOMAIN` environment variable is available to Caddy: ```bash # Dockerized Caddy -- add to its .env or compose environment: DOMAIN=yourdomain.com # System Caddy -- add to /etc/caddy/caddy.env (or wherever systemd reads env): echo 'DOMAIN=yourdomain.com' | sudo tee -a /etc/caddy/caddy.env # Then make sure the systemd unit loads it: # EnvironmentFile=/etc/caddy/caddy.env (in the [Service] section) sudo systemctl daemon-reload && sudo systemctl reload caddy ``` **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 ``` Create the directory before starting: ```bash sudo mkdir -p /var/log/caddy sudo chown caddy:caddy /var/log/caddy # adjust to your Caddy UID ``` ## Switching Frigate to Authelia (case 2) Edit `frigate_config/config.yml` in your Frigate stack: First, find your `caddy_net` subnet -- you need this for `trusted_proxies`: ```bash docker network inspect caddy_net | jq '.[0].IPAM.Config' ``` Then edit `frigate_config/config.yml`: ```yaml auth: enabled: False trusted_proxies: - 172.18.0.0/16 # replace with your caddy_net subnet from above proxy: header_map: user: remote-user # matches `copy_headers Remote-User` in (authelia) snippet role: remote-groups default_role: viewer separator: '|' # Optional shared secret -- prevents LAN header spoofing. # auth_secret: 'your-32-byte-hex' ``` To generate the optional `auth_secret`: ```bash openssl rand -hex 32 ``` Set the same value in the `header_up X-Proxy-Secret` line in your Caddy site block. Add the rule to `authelia/configuration.yml` (step 4 of first-run), then restart both services: ```bash docker compose restart authelia ``` ```bash docker compose restart frigate # in your Frigate stack ``` Verify in a private browser window: `https://cam.yourdomain.com` should go to Authelia and back without a Frigate login screen. ## First login + TOTP enrollment 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 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: ```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. Takes effect at next refresh. ### Reset TOTP (force re-enrollment) ```bash docker compose exec authelia \ authelia storage user totp delete --username USERNAME \ --config /config/configuration.yml ``` ## fail2ban 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 docker compose exec fail2ban fail2ban-client status authelia docker compose exec fail2ban fail2ban-client status caddy-4xx ``` ### Test filters against real logs ```bash docker compose exec fail2ban fail2ban-regex \ /var/log/authelia/authelia.log \ /data/filter.d/authelia.local docker compose exec fail2ban fail2ban-regex \ /var/log/caddy/access.log \ /data/filter.d/caddy-4xx.local ``` 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 ```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 thresholds Edit `fail2ban/data/jail.d/*.local`, then: ```bash docker compose restart fail2ban ``` ## Day-to-day ```bash 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 upgrading Authelia. After any upgrade, re-run `validate-config` -- the schema evolves between releases. ## Switching the notifier to SMTP Replace `notifier:` 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 secret file: ```bash echo 'your_smtp_password' > authelia/secrets/SMTP_PASSWORD chmod 600 authelia/secrets/SMTP_PASSWORD ``` Add to the `authelia` service `environment:` block in `docker-compose.yml`: ```yaml - AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/secrets/SMTP_PASSWORD ``` Restart and check for a successful startup message: ```bash docker compose restart authelia && docker compose logs -f authelia ``` Expect: `Notifier SMTP startup check successful` ## Security notes - `.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. ## Troubleshooting ### Redirect loop between a site and `auth.example.com` 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` -- a request reached Authelia with no matching rule for that domain. Add a rule under `access_control.rules` in `authelia/configuration.yml`: ```yaml - domain: 'yoursite.{{ env "DOMAIN" }}' policy: 'two_factor' ``` Then restart Authelia: ```bash docker compose 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 this to your Caddy service in its compose file: ```yaml services: caddy: networks: - caddy_net networks: caddy_net: external: true ``` Then recreate the Caddy container: ```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).