Covers Debian/Ubuntu apt install for git, adding GitHub's official apt repository (Linux tap equivalent) to install gh CLI, gh auth login flow, and using gh repo clone as the step-0 clone command so credentials are handled automatically. https://claude.ai/code/session_012eTokAaGiZo7aGt1T2W9BC
18 KiB
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-USERiptables chain: drops happen at the host edge before traffic reaches any docker-published port. - Caddy is not in this stack. Copy
caddy/Caddyfileinto 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, replacecaddy_neteverywhere in this repo. - Caddy v2.5.1 or newer (for the
forward_authdirective; 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. Usetwo_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, orRemote-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:
# 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.
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: addimport autheliato any site block.(accesslog)-- writes Caddy's JSON access log to/var/log/caddy/access.logso fail2ban'scaddy-4xxjail 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:
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
- Visit a protected subdomain in a private browser window.
- Caddy bounces you to
https://auth.example.com-- log in with your username and plaintext password. - If the rule is
two_factor, Authelia prompts you to register a second factor. Pick TOTP and scan the QR with your authenticator app. - Authelia writes a confirmation link to the filesystem notifier file:
Click the link to confirm TOTP registration.
docker compose exec authelia cat /config/notifications/notification.txt - Enter the TOTP code -- you're in. The
authelia_sessioncookie 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, andauthelia/db.sqlite3*are all gitignored. Rungit statusbefore every commit to confirm nothing sensitive is staged.- Authelia is not port-mapped to the host. Only containers on
caddy_netcan 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. regulationis 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
- Confirm
auth.enabled: Falseinfrigate_config/config.yml. - Confirm
trusted_proxiessubnet matches yourcaddy_netsubnet:docker network inspect caddy_net | grep -A2 '"Config"' - Restart Frigate:
docker compose restart frigate(in your Frigate stack).