default_policy: deny means no rule = 403 even for logged-in users. Step 4 now explains the two-sided requirement: Caddy controls what traffic reaches Authelia; the rule controls what Authelia does with it. https://claude.ai/code/session_012eTokAaGiZo7aGt1T2W9BC
632 lines
25 KiB
Markdown
632 lines
25 KiB
Markdown
# 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:
|
||
|
||
```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) For each site you added `import authelia` to in Caddy, uncomment the
|
||
# matching rule in authelia/configuration.yml under access_control.rules.
|
||
#
|
||
# WHY: default_policy is 'deny'. If a domain reaches Authelia with no
|
||
# matching rule, Authelia returns 403 -- even to a logged-in user. The
|
||
# rule tells Authelia the domain is valid and what auth level to require.
|
||
#
|
||
# Each rule is two lines -- just uncomment and pick one_factor or two_factor:
|
||
# - domain: 'cam.{{ env "DOMAIN" }}'
|
||
# policy: '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).
|