diff --git a/README.md b/README.md index 5ea91c1..c3d4706 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ authelia-stack/ │ └── caddy.local # 30 fails/2 min -> 30 min IP ban │ └── caddy/ - └── Caddyfile # copy/merge into your Caddy setup + └── snippets.caddyfile # per-service snippets to add to your existing Caddyfile ``` ## Prerequisites @@ -93,25 +93,59 @@ Concretely: 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: +Look for any of these in the app's docs: "Remote-User header", "trusted +upstream", "trusted proxies", "header-based auth", "SSO via reverse proxy". -- "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 | +| 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 @@ -249,15 +283,16 @@ docker compose logs -f fail2ban # expect "Jail authelia is now active" ## Wire Caddy into Authelia -Open `caddy/Caddyfile`. It defines: +Open `caddy/snippets.caddyfile`. It contains copy-paste blocks for your +existing Caddyfile, not a replacement for it: -- `(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.{env.DOMAIN}` -- the Authelia portal. -- Example site blocks for all four cases. +- `(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 relevant blocks into your real Caddyfile and replace `192.168.x.x` +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: diff --git a/caddy/Caddyfile b/caddy/Caddyfile deleted file mode 100644 index 8a3614a..0000000 --- a/caddy/Caddyfile +++ /dev/null @@ -1,210 +0,0 @@ -# ============================================================================= -# Caddyfile -- Authelia + fail2ban integration -# -# Copy this file into your Caddy setup (or merge the relevant blocks into -# your existing Caddyfile), then reload: -# -# # System Caddy: -# sudo caddy validate --config /etc/caddy/Caddyfile -# sudo systemctl reload caddy -# -# # Dockerized Caddy: -# docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile -# -# Domain is read from the DOMAIN environment variable -- no hardcoded -# domains in this file. Set it in your Caddy environment before reloading: -# -# # System Caddy (add to /etc/caddy/caddy.env or your systemd override): -# export DOMAIN=yourdomain.com -# -# # Dockerized Caddy (add to its .env or compose environment:): -# DOMAIN=yourdomain.com -# -# Only the upstream IPs need editing: replace 192.168.x.x with real LAN IPs. -# -# Requirements: -# - Caddy v2.5.1+ (for `forward_auth` directive; tested on v2.11.2) -# - Caddy must be on the `caddy_net` Docker network so it can resolve -# `authelia` by container name. In your Caddy compose: -# networks: [caddy_net] -# and at the bottom: -# networks: -# caddy_net: -# external: true -# -# ============================================================================= -# DECISION TREE -- which sites go behind Authelia? -# -# CASE 1 -- App has NO built-in auth (e.g. Pi doorbell PTT page). -# `import authelia` + rule in authelia/configuration.yml. -# Authelia is the ONLY login. Use two_factor for hardware-control pages. -# -# CASE 2 -- App has built-in auth AND supports trusted-header proxy auth -# (Frigate 0.14+, Grafana, Gitea, Nextcloud, Home Assistant, ...). -# `import authelia` + rule in Authelia + disable the app's own login form. -# Single Authelia login: Authelia authenticates, app reads Remote-User header. -# -# CASE 3 -- App has built-in auth and CANNOT switch to proxy auth, but you -# want a 2FA gate in front anyway (router admin, legacy apps, etc.). -# `import authelia` + rule in Authelia. App auth is untouched. -# User logs into Authelia (2FA), then the app's own login form appears. -# -# CASE 4 -- App handles its own auth; Authelia not involved. -# Plain `reverse_proxy`, no `import authelia`, no Authelia rule. -# Traffic skips Authelia entirely. -# -# fail2ban coverage: import (accesslog) in EVERY site block -- gated or not. -# The caddy-4xx jail watches /var/log/caddy/access.log and bans scanners -# spraying all your subdomains, not just the Authelia-gated ones. -# ============================================================================= - -# ----------------------------------------------------------------------------- -# (authelia) -- forward_auth gate. -# Import into any site block you want gated (cases 1, 2, 3). -# On success Authelia sets Remote-User, Remote-Groups, Remote-Email, -# Remote-Name headers that the upstream app can consume for role mapping. -# ----------------------------------------------------------------------------- -(authelia) { - forward_auth authelia:9091 { - uri /api/authz/forward-auth - copy_headers Remote-User Remote-Groups Remote-Email Remote-Name - } -} - -# ----------------------------------------------------------------------------- -# (accesslog) -- structured JSON access log consumed by fail2ban's caddy-4xx -# jail. Import into EVERY site block so fail2ban covers your whole stack. -# -# Pre-create the log directory before starting Caddy: -# sudo mkdir -p /var/log/caddy -# sudo chown caddy:caddy /var/log/caddy # system Caddy -# # Dockerized Caddy: add volumes: ["/var/log/caddy:/var/log/caddy"] to compose -# ----------------------------------------------------------------------------- -(accesslog) { - log { - output file /var/log/caddy/access.log { - roll_size 10MiB - roll_keep 5 - roll_keep_for 720h - } - format json - } -} - -# ============================================================================= -# Authelia login portal -# Never add `import authelia` here -- the `bypass` rule in -# access_control.rules handles the portal itself. Adding forward_auth here -# would cause a redirect loop. -# ============================================================================= -auth.{env.DOMAIN} { - import accesslog - reverse_proxy authelia:9091 -} - -# ============================================================================= -# CASE 2: Frigate UI -- Authelia replaces Frigate's own login form. -# -# Frigate 0.14+ supports trusted-header proxy auth. Authelia authenticates -# the user (optionally with TOTP 2FA), then passes Remote-User and -# Remote-Groups headers to Frigate which maps them to admin/viewer roles. -# -# To enable proxy auth in Frigate, edit frigate_config/config.yml: -# -# auth: -# enabled: False -# trusted_proxies: -# - 172.18.0.0/16 # caddy_net subnet; find it with: -# # docker network inspect caddy_net -# proxy: -# header_map: -# user: remote-user # matches copy_headers 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 below. -# # auth_secret: 'your-32-byte-hex' -# -# Then uncomment the cam rule in authelia/configuration.yml and restart: -# docker compose restart authelia (in the authelia stack) -# docker compose restart frigate (in the camera stack) -# ============================================================================= -cam.{env.DOMAIN} { - import accesslog - import authelia - - reverse_proxy 192.168.x.x:8971 { # CHANGE IP - transport http { - read_timeout 60s - write_timeout 60s - } - # Uncomment if you set auth_secret: in Frigate's proxy: block. - # header_up X-Proxy-Secret "your-32-byte-hex-here" - } -} - -# ============================================================================= -# CASE 1: Pi doorbell PTT page -- Authelia is the ONLY auth. -# -# The Pi's Flask server has no built-in authentication. Authelia gates it. -# two_factor is appropriate -- this URL controls a speaker in your house. -# Comment out until the Pi is deployed. -# Also uncomment the doorbell rule in authelia/configuration.yml. -# ============================================================================= -# doorbell.{env.DOMAIN} { -# import accesslog -# import authelia -# -# # Same-origin proxy to Frigate so WebRTC fetch works without CORS. -# handle_path /frigate/* { -# reverse_proxy 192.168.x.x:8971 { # CHANGE IP -# transport http { -# read_timeout 60s -# write_timeout 60s -# } -# } -# } -# -# handle { -# reverse_proxy 192.168.x.x:5555 # CHANGE IP -# } -# } - -# ============================================================================= -# CASE 3: App keeps its own login; Authelia adds a 2FA gate in front. -# -# Use when an app can't do proxy auth but you still want 2FA before it. -# The user authenticates with Authelia (2FA), then the app's own login -# form appears. Two separate logins -- the app's auth is untouched. -# -# Also add a rule in authelia/configuration.yml: -# - domain: 'nas.{{ env "DOMAIN" }}' -# policy: 'two_factor' -# ============================================================================= -# nas.{env.DOMAIN} { # CHANGE/REMOVE example -# import accesslog -# import authelia -# -# reverse_proxy 192.168.x.x:PORT { # CHANGE -# transport http { -# tls_insecure_skip_verify # only if self-signed TLS -# } -# } -# } - -# ============================================================================= -# CASE 4: App handles its own auth; Authelia not involved. -# -# No `import authelia`. No access_control rule in Authelia. -# Still import accesslog so fail2ban's caddy-4xx jail covers this site. -# ============================================================================= -# router.{env.DOMAIN} { # CHANGE/REMOVE example -# import accesslog -# reverse_proxy 192.168.x.x:PORT { # CHANGE -# transport http { -# tls_insecure_skip_verify -# } -# } -# } diff --git a/caddy/snippets.caddyfile b/caddy/snippets.caddyfile new file mode 100644 index 0000000..78e0d5a --- /dev/null +++ b/caddy/snippets.caddyfile @@ -0,0 +1,257 @@ +# ============================================================================= +# Authelia + fail2ban -- Caddy snippets +# +# These are SNIPPETS TO ADD to your existing Caddyfile, not a replacement +# for it. Copy the (authelia) and (accesslog) snippet definitions once at +# the top of your Caddyfile, then copy whichever site blocks apply. +# +# DOMAIN is read from the environment -- set it wherever your Caddy reads +# env vars (Caddy's own .env, systemd EnvironmentFile, or compose env:). +# Only the upstream IPs need manual editing. +# +# Caddy v2.5.1+ required; tested on v2.11.2. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Paste these two snippet definitions once, near the top of your Caddyfile. +# ----------------------------------------------------------------------------- + +# Gate any site block with Authelia by adding `import authelia` inside it. +(authelia) { + forward_auth authelia:9091 { + uri /api/authz/forward-auth + copy_headers Remote-User Remote-Groups Remote-Email Remote-Name + } +} + +# JSON access log that fail2ban's caddy-4xx jail reads. +# Add `import accesslog` to EVERY site block (gated or not) so fail2ban +# catches scanners hitting all your subdomains, not just the protected ones. +(accesslog) { + log { + output file /var/log/caddy/access.log { + roll_size 10MiB + roll_keep 5 + roll_keep_for 720h + } + format json + } +} + + +# ============================================================================= +# Authelia portal -- always required; never put `import authelia` here. +# ============================================================================= +auth.{env.DOMAIN} { + import accesslog + reverse_proxy authelia:9091 +} + + +# ============================================================================= +# CASE 1 -- App has NO built-in auth. Authelia is the only gate. +# +# Examples: Homer, Heimdall, Dozzle, phpIPAM, any internal tool with no +# login page. Use two_factor in authelia/configuration.yml for these. +# ============================================================================= + +# Homer / Heimdall dashboard (no auth whatsoever) +homer.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:8080 # CHANGE IP:PORT +} + +# Dozzle (Docker log viewer -- no auth by default) +dozzle.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:8888 # CHANGE IP:PORT +} + +# Pi doorbell PTT page (Flask server, no auth) +# Uncomment when the Pi is deployed. +# doorbell.{env.DOMAIN} { +# import accesslog +# import authelia +# +# handle_path /frigate/* { +# reverse_proxy 192.168.x.x:8971 # CHANGE: Frigate IP +# } +# handle { +# reverse_proxy 192.168.x.x:5555 # CHANGE: Pi IP +# } +# } + + +# ============================================================================= +# CASE 2 -- App supports trusted-header proxy auth. Authelia replaces its +# own login form. Single login, app reads Remote-User for roles. +# +# Requires per-app config changes -- see notes in each block. +# ============================================================================= + +# --- Frigate 0.14+ --- +# In frigate_config/config.yml: +# auth: +# enabled: False +# trusted_proxies: [172.18.0.0/16] # caddy_net subnet +# proxy: +# header_map: {user: remote-user, role: remote-groups} +# default_role: viewer +# separator: '|' +cam.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:8971 { # CHANGE IP + transport http { read_timeout 60s; write_timeout 60s } + # header_up X-Proxy-Secret "32-byte-hex" # if auth_secret: set in Frigate + } +} + +# --- Grafana --- +# In grafana.ini (or GF_* env vars): +# [auth.proxy] +# enabled = true +# header_name = Remote-User +# header_property = username +# auto_sign_up = true +grafana.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:3000 # CHANGE IP +} + +# --- Gitea / Forgejo --- +# In app.ini: +# [service] +# ENABLE_REVERSE_PROXY_AUTHENTICATION = true +# REVERSE_PROXY_TRUSTED_PROXIES = * +git.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:3000 # CHANGE IP +} + +# --- Nextcloud --- +# In config/config.php: +# 'trusted_proxies' => ['172.18.0.0/16'], +# 'overwriteprotocol' => 'https', +# Plus user_external app + HTTP header auth set to Remote-User. +cloud.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:80 { # CHANGE IP + header_up Host {upstream_hostport} + } +} + +# --- Paperless-ngx --- +# In compose env: +# PAPERLESS_ENABLE_HTTP_REMOTE_USER=true +# PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=HTTP_REMOTE_USER +paperless.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:8000 # CHANGE IP +} + +# --- Miniflux --- +# In compose env: +# AUTH_PROXY_HEADER=Remote-User +# AUTH_PROXY_USER_CREATION=true +miniflux.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:8080 # CHANGE IP +} + +# --- BookStack --- +# See BookStack docs for HTTP header auth (requires the SAML2 or HTTP auth method). +# bookstack.{env.DOMAIN} { +# import accesslog +# import authelia +# reverse_proxy 192.168.x.x:80 # CHANGE IP +# } + +# --- Immich --- +# Proxy auth headers not supported; use Authelia as OIDC provider instead. +# Configure Authelia's identity_providers.oidc block and point Immich at it. +# photos.{env.DOMAIN} { +# import accesslog +# import authelia +# reverse_proxy 192.168.x.x:2283 # CHANGE IP +# } + + +# ============================================================================= +# CASE 3 -- App keeps its own login. Authelia adds a 2FA gate in front. +# +# The user passes Authelia 2FA, then the app's own login appears. +# Use for apps that don't support proxy auth headers but you still want +# 2FA before they're even reachable. This is the only way to get TOTP +# in front of apps that don't natively support it. +# ============================================================================= + +# --- Uptime Kuma --- +# No proxy auth, no native TOTP -- Authelia is the only way to add 2FA. +uptime.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:3001 # CHANGE IP +} + +# --- Portainer --- +# No proxy auth header support (use OIDC for full SSO instead). +portainer.{env.DOMAIN} { + import accesslog + import authelia + reverse_proxy 192.168.x.x:9000 # CHANGE IP +} + +# --- Mealie --- +# Has OIDC support -- can do full case 2 with Authelia as OIDC provider. +# For simpler case-3 gate: +# mealie.{env.DOMAIN} { +# import accesslog +# import authelia +# reverse_proxy 192.168.x.x:9000 # CHANGE IP +# } + +# --- Home Assistant --- +# Can also do case 2 via the trusted_networks auth provider + header forwarding. +# homeassistant.{env.DOMAIN} { +# import accesslog +# import authelia +# reverse_proxy 192.168.x.x:8123 # CHANGE IP +# } + +# --- Vaultwarden --- +# Has its own strong auth + native TOTP. Many skip Authelia here entirely +# and rely on Vaultwarden's own 2FA (totally valid). Or use case 3 as an +# extra gate if you want 2FA even before the login page loads. +# vault.{env.DOMAIN} { +# import accesslog +# import authelia +# reverse_proxy 192.168.x.x:80 # CHANGE IP +# } + + +# ============================================================================= +# CASE 4 -- App handles its own auth. Authelia not involved. +# Still import accesslog so fail2ban covers this site. +# ============================================================================= + +# --- Syncthing --- +# sync.{env.DOMAIN} { +# import accesslog +# reverse_proxy 192.168.x.x:8384 # CHANGE IP +# } + +# --- Router / NAS admin UI --- +# router.{env.DOMAIN} { +# import accesslog +# reverse_proxy 192.168.x.x:443 { +# transport http { tls_insecure_skip_verify } +# } +# }