Files
frigate_w_audio/README.md
T
Claude 6cf6a2aeb8 refactor: Caddyfile -> snippets.caddyfile; expand service table; add "should you use Authelia" section
caddy/:
- Rename Caddyfile to snippets.caddyfile -- these are per-service snippets
  to add to an existing Caddyfile, not a replacement for one
- Add per-service example blocks for Homer/Heimdall, Dozzle, Frigate,
  Grafana, Gitea, Nextcloud, Paperless-ngx, Miniflux, BookStack, Immich,
  Uptime Kuma, Portainer, Mealie, Home Assistant, Vaultwarden, Syncthing
  with required per-app config notes inline (DotheEvo style)
- All site blocks use {env.DOMAIN}; only IPs need editing

README.md:
- Add "Should you use Authelia?" section that honestly answers when it
  improves on password-manager + per-service credentials/TOTP, and when
  it's marginal (single user, all services already have native TOTP)
- Expand service compatibility table from 9 to 17 entries with case
  classification (1/2/3/4) and notes for each
- Fix Wire Caddy section to reference snippets.caddyfile
- Fix repo layout tree

https://claude.ai/code/session_012eTokAaGiZo7aGt1T2W9BC
2026-04-26 14:49:06 +00:00

570 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/
└── 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.<root>` and every
protected subdomain must point at the Caddy host's public IP.
## Which sites go behind Authelia?
There are four ways a site can relate to Authelia. Pick one per site.
| Case | App has built-in auth? | Supports proxy auth? | What to do |
|------|------------------------|----------------------|------------|
| **1** | No | n/a | `import authelia` in Caddy + rule in Authelia. Authelia is the only login. |
| **2** | Yes | Yes | `import authelia` in Caddy + rule in Authelia + disable app's own login form. Single login. |
| **3** | Yes | No | `import authelia` in Caddy + rule in Authelia. App auth is unchanged. User logs into Authelia then the app. Two logins. |
| **4** | Yes | — | Plain `reverse_proxy`. No `import authelia`, no rule. App handles auth. |
Concretely:
- **`doorbell.example.com`** (Pi PTT page) -- **case 1**. No app auth at all.
Authelia is the only gate. Use `two_factor` -- this URL controls a speaker.
- **`cam.example.com`** (Frigate UI) -- **case 2**. Frigate 0.14+ supports
proxy auth. Disable Frigate's login form and let Authelia drive both the
access gate and the role mapping (admin vs. viewer) via headers.
- **Router admin / NAS UI** -- **case 3** if you want a 2FA gate in front,
**case 4** if you just leave it to the app.
Default policy in `configuration.yml` is `deny`, so a domain with no rule
AND no `import authelia` in Caddy never reaches Authelia at all.
### 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) -- 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 |
|-----|------|-------|
| Frigate 0.14+ | 2 | `auth.enabled: False` + `proxy:` block in config.yml |
| Grafana | 2 | `[auth.proxy]` in grafana.ini; `GF_AUTH_PROXY_ENABLED=true` |
| Gitea / Forgejo | 2 | `ENABLE_REVERSE_PROXY_AUTHENTICATION=true` in app.ini |
| Nextcloud | 2 | `trusted_proxies` + `user_external` app + HTTP header auth |
| Paperless-ngx | 2 | `PAPERLESS_ENABLE_HTTP_REMOTE_USER=true` |
| Miniflux | 2 | `AUTH_PROXY_HEADER=Remote-User` env var |
| Home Assistant | 2 | `trusted_networks` auth provider |
| BookStack | 2 (partial) | HTTP header auth via SAML2 or custom auth method |
| Immich | 2 (OIDC) | No header auth; configure Authelia as OIDC provider |
| Mealie | 2 (OIDC) | OIDC support; configure Authelia as OIDC provider |
| Homer / Heimdall | 1 | No auth at all -- Authelia is the only gate |
| Dozzle | 1 | No auth by default -- Authelia is the only gate |
| Uptime Kuma | 3 | No proxy auth, no native TOTP -- case 3 is the only way to add 2FA |
| Portainer | 3 | No proxy auth headers; has OIDC for full SSO |
| Jellyfin | 3 | Community plugin for header auth; otherwise case 3 |
| Syncthing | 4 | Decent native auth; proxy auth not supported |
| Vaultwarden | 3 or 4 | Strong native auth + TOTP; many skip Authelia here entirely |
| Router / NAS admin | 3 or 4 | Depends on firmware; usually case 4 is fine |
## 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
```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 -- this is the only place you set your domain.
# DOMAIN flows into authelia/configuration.yml and caddy/Caddyfile
# automatically via each tool's env-var substitution; no find-and-replace.
cp .env.example .env
$EDITOR .env # set DOMAIN, TZ; pin image versions if you want
# 4) Uncomment access_control.rules in authelia/configuration.yml for the
# sites you want to gate. The domain is already templated -- just
# uncomment the right case block and choose one_factor or two_factor.
$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/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:
```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 <authelia@example.com>'
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).