# 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 `caddy/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/ └── Caddyfile # copy/merge into your Caddy setup ``` ## 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. | | **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 | ## Getting git and authenticating to GitHub 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 ```bash # 0) Clone the auth stack onto the server. # (The Frigate stack lives on the `main` branch and is cloned separately.) gh repo clone outis1one/frigate_w_audio -- \ --branch authelia ~/docker/authelia cd ~/docker/authelia # 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. 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 .env. cp .env.example .env $EDITOR .env # set TZ; pin AUTHELIA_VERSION if you want # 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. 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 $argon2id$... output into the password: field. # 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 # 7) Validate config before starting. docker compose run --rm authelia \ authelia validate-config --config /config/configuration.yml # Expect: "Configuration: validation complete" with no errors. # 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 fail2ban # expect "Jail authelia is now active" ``` ## Wire Caddy into Authelia Open `caddy/Caddyfile`. It defines: - `(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'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. **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: ```yaml auth: enabled: False trusted_proxies: - 172.18.0.0/16 # the caddy_net subnet -- find it with: # docker network inspect caddy_net | jq '.[0].IPAM.Config' proxy: header_map: user: remote-user # matches `copy_headers Remote-User` 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` in caddy/Caddyfile. # auth_secret: 'your-32-byte-hex' ``` Then uncomment `cam.example.com` in `authelia/configuration.yml`, restart both services: ```bash 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 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 and wire it up: ```bash echo 'your_smtp_password' > authelia/secrets/SMTP_PASSWORD chmod 600 authelia/secrets/SMTP_PASSWORD ``` Add to the authelia service environment in `docker-compose.yml`: ```yaml - AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/secrets/SMTP_PASSWORD ``` Restart and look for `"Notifier SMTP startup check successful"` in logs. ## 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` 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).