Files
frigate_w_audio/README.md
T
Claude b797602cbc Fix Plex/Emby Authelia caveat: Case 4, not Case 3
Native clients (mobile, TV, desktop) DO route through Caddy via FQDN,
but cannot complete Authelia's browser-redirect login flow -- they get a
302 to auth.DOMAIN and fail. `import authelia` breaks all native clients.

Correct setup is Case 4: Caddy terminates TLS, import accesslog for
fail2ban, NO import authelia. Plex/Emby token auth handles access control.

https://claude.ai/code/session_012eTokAaGiZo7aGt1T2W9BC
2026-04-26 15:36:32 +00:00

24 KiB
Raw Blame History

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.<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.
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:

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):

# 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:

# 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:

# 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:

brew install git gh
gh auth login

First-run setup

# 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:

# 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:

services:
  caddy:
    volumes:
      - /var/log/caddy:/var/log/caddy

Create the directory before starting:

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:

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:

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:
    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:

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)

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

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

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

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:

docker compose restart fail2ban

Day-to-day

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:

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:

echo 'your_smtp_password' > authelia/secrets/SMTP_PASSWORD
chmod 600 authelia/secrets/SMTP_PASSWORD

Add to the authelia service environment in docker-compose.yml:

- 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

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:

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:

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:

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:
    docker network inspect caddy_net | grep -A2 '"Config"'
    
  3. Restart Frigate: docker compose restart frigate (in your Frigate stack).