Add Authelia SSO to Frigate — disables its own login, not just a gate in front of it

Frigate has its own built-in login separate from Authelia's session, so
just adding `import authelia` in front of it (the pattern used for
no-built-in-auth services) would leave two independent logins stacked,
defeating the point of Authelia's "remember me" on mobile. Frigate has a
`proxy` auth mode built for exactly this — trust Remote-User/Remote-Groups
from an upstream forward_auth proxy and disable its own login entirely.

- Extend configure_caddy_for_service() with an optional 5th arg for
  sub-directives inside the reverse_proxy block itself (header_up), needed
  to pin an X-Proxy-Secret header so Frigate's proxy-auth trust can't be
  spoofed by a request reaching its published port directly, bypassing
  Caddy/Authelia. Backward compatible — every other caller is unaffected.
- services/frigate.sh: prompt to protect with Authelia when installed;
  wires import authelia + the X-Proxy-Secret header_up into Caddy, and
  only writes config.yml's auth.enabled: False + proxy block once Caddy
  actually confirms it's fronting the domain (never disables the native
  login with nothing else gating access). Reuses the secret across
  reinstalls instead of rotating it. Calls _authelia_scope_access() so
  access can be restricted to specific users instead of every Authelia
  account. Fixed a latent bug in the standalone-mode Caddy stub where the
  auth block was placed after reverse_proxy instead of before it (dead
  code — the same "Authelia never prompts" bug class CLAUDE.md documents
  for the real helper).
- CLAUDE.md: document the new configure_caddy_for_service parameter and
  Frigate's hybrid built-in-auth/forward_auth pattern.

Verified end-to-end against a local test harness (fake Authelia/Caddy
dirs): config.yml, .env, and the generated Caddyfile block all agree on
the shared secret and header names, auth is skipped cleanly when Caddy
isn't configured, and the secret is reused on a second run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
This commit is contained in:
Claude
2026-08-21 19:56:27 +00:00
parent afc59613fc
commit cd33b7ce71
3 changed files with 156 additions and 16 deletions
+46 -5
View File
@@ -191,13 +191,25 @@ pip_user_install PACKAGE... # pip3 --user with --break-system-packages o
### Caddy reverse proxy ### Caddy reverse proxy
```bash ```bash
configure_caddy_for_service "Display Name" "PORT" "default-subdomain" ["extra-block"] configure_caddy_for_service "Display Name" "PORT" "default-subdomain" ["extra-block"] ["reverse_proxy-extra"]
``` ```
Prompts the user for a domain, appends a site block to the Caddyfile, and Prompts the user for a domain, appends a site block to the Caddyfile, and
reloads Caddy. No-ops silently if Caddy isn't installed. The fourth argument reloads Caddy. No-ops silently if Caddy isn't installed. The fourth argument
is an optional string inserted verbatim inside the Caddy site block (use it is an optional string inserted verbatim inside the Caddy site block, before
for `import authelia` or custom matchers). `reverse_proxy` (use it for `import authelia` or custom matchers). The fifth
argument is a different thing — an optional string inserted **inside** the
`reverse_proxy` block itself, as sub-directives (e.g.
`" header_up X-Proxy-Secret abc123"`), for a backend that needs a
header only `reverse_proxy`'s own `header_up` can set — the fourth
argument's block runs *before* `reverse_proxy` and can't reach into it.
`services/frigate.sh` is the reference caller: Frigate's `proxy` auth mode
trusts `Remote-User`/`Remote-Groups` headers from Authelia's forward_auth,
but only if a matching `X-Proxy-Secret` header is also present — otherwise
those headers could be spoofed by a request that reaches Frigate's
published host port directly, bypassing Caddy/Authelia entirely. Omit the
fifth argument and the generated `reverse_proxy` line is the same bare form
as before — every other caller is unaffected.
The function places that block **before** `reverse_proxy` in the generated The function places that block **before** `reverse_proxy` in the generated
site block — don't reorder this. `forward_auth` (what `import authelia` site block — don't reorder this. `forward_auth` (what `import authelia`
@@ -245,14 +257,19 @@ forward_auth https://auth.example.com {
This only affects the remote-Authelia path — same-machine `authelia:9091` This only affects the remote-Authelia path — same-machine `authelia:9091`
snippets (`services/authelia.sh`) are a single hop and don't need it. snippets (`services/authelia.sh`) are a single hop and don't need it.
Sets two out-params (not `local` — read them after the call returns) so the Sets three out-params (not `local` — read them after the call returns) so
caller can tell whether Caddy actually ended up fronting the service: the caller can tell whether Caddy actually ended up fronting the service:
```bash ```bash
CADDY_SERVICE_CONFIGURED # true/false CADDY_SERVICE_CONFIGURED # true/false
CADDY_SERVICE_MODE # "local" or "remote" (only meaningful if configured) CADDY_SERVICE_MODE # "local" or "remote" (only meaningful if configured)
CADDY_SERVICE_DOMAIN # the domain actually configured (only meaningful if configured)
``` ```
`CADDY_SERVICE_DOMAIN` is what `_authelia_scope_access()` (see below) wants
as its `DOMAIN` argument — read it right after the call instead of
recomputing/guessing the domain a second time.
Use this to skip opening a host firewall port for a service Caddy already Use this to skip opening a host firewall port for a service Caddy already
fronts *locally* (it reaches the service over `host.docker.internal`, not fronts *locally* (it reaches the service over `host.docker.internal`, not
the network) — but still open it when `CADDY_SERVICE_MODE` is `"remote"`, the network) — but still open it when `CADDY_SERVICE_MODE` is `"remote"`,
@@ -461,6 +478,30 @@ for Authelia to protect. Removed from this list; if it grows a web UI in
the future, add it back and wire up the same prompt other services here the future, add it back and wire up the same prompt other services here
use. use.
**`frigate` — a third pattern, neither of the two above.** Frigate *does*
have built-in auth (username/password, `admin`/`viewer` roles, on by
default) so it isn't "no built-in auth" — but unlike the has-built-in-auth
list, that auth is designed to be handed off to an upstream proxy instead
of just living alongside it. Frigate has its own `proxy` auth mode built
specifically for Authelia/Authentik/oauth2_proxy/traefik-forward-auth:
given trusted `Remote-User`/`Remote-Groups` headers it can skip its own
login screen entirely (`auth.enabled: False`), rather than showing a
second, independently-expiring login *after* Authelia's. `services/frigate.sh`
wires this up: `import authelia` (fourth arg) plus a
`header_up X-Proxy-Secret <secret>` (fifth arg, see
`configure_caddy_for_service` above) into the reverse_proxy block, with
the matching `proxy.auth_secret`/`header_map`/`default_role: admin` block
written into `config/config.yml` — and only written at all once
`CADDY_SERVICE_CONFIGURED` confirms Caddy actually ended up fronting the
domain, so Frigate's own login is never disabled with nothing else in
front of it. `default_role: admin` (default in this repo's install) means
anyone who passes Authelia gets full access, same as the login it
replaces; use `proxy.role_map`/Authelia groups instead if some users
should be view-only. Reuses the same `FRIGATE_PROXY_AUTH_SECRET` on
reinstall (from `.env` via `ENV_MAP`, the same array `_frigate_parse_existing`
already builds) rather than rotating it and breaking the existing Caddy
pairing.
For services without built-in auth, prompt the user before calling For services without built-in auth, prompt the user before calling
`configure_caddy_for_service` and pass `import authelia` as the extra block `configure_caddy_for_service` and pass `import authelia` as the extra block
if Authelia is installed and the user wants SSO protection: if Authelia is installed and the user wants SSO protection:
+20 -3
View File
@@ -841,11 +841,19 @@ find_free_coturn_range() {
} }
# ── Caddy reverse-proxy wiring (shared by every web service) ───────────────── # ── Caddy reverse-proxy wiring (shared by every web service) ─────────────────
# Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"] # Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"] ["reverse_proxy-extra"]
# UPSTREAM: container:port for caddy_net routing (e.g. "filebrowser:80"), # UPSTREAM: container:port for caddy_net routing (e.g. "filebrowser:80"),
# or plain port number for localhost fallback (e.g. "8085"). # or plain port number for localhost fallback (e.g. "8085").
# The optional 5th arg is inserted as sub-directives *inside* the
# reverse_proxy block itself (e.g. " header_up X-Proxy-Secret abc123")
# — for the rare case a backend needs a header only reverse_proxy's own
# header_up can set, as opposed to EXTRA_CONFIG's auth-gate directives that
# run before reverse_proxy entirely. See services/frigate.sh's Authelia
# integration for the reference caller (pins X-Proxy-Secret so Frigate's
# proxy-auth trust can't be spoofed by a request that reaches it directly,
# bypassing Caddy/Authelia).
configure_caddy_for_service() { configure_caddy_for_service() {
local SERVICE_NAME="$1" SERVICE_UPSTREAM="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}" local SERVICE_NAME="$1" SERVICE_UPSTREAM="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}" REVERSE_PROXY_EXTRA="${5:-}"
# Out-params (not `local` — callers read these after the call returns) so # Out-params (not `local` — callers read these after the call returns) so
# a caller can tell whether Caddy actually ended up fronting the service # a caller can tell whether Caddy actually ended up fronting the service
@@ -941,6 +949,15 @@ configure_caddy_for_service() {
_BLOCK_UPSTREAM="${_THIS_IP}:${_DISPLAY_PORT}" _BLOCK_UPSTREAM="${_THIS_IP}:${_DISPLAY_PORT}"
fi fi
# Bare "reverse_proxy upstream" unless a caller needs sub-directives
# (header_up, etc.) inside it — see the REVERSE_PROXY_EXTRA comment above.
local _REVERSE_PROXY_LINE="reverse_proxy ${_BLOCK_UPSTREAM}"
if [ -n "$REVERSE_PROXY_EXTRA" ]; then
_REVERSE_PROXY_LINE="reverse_proxy ${_BLOCK_UPSTREAM} {
${REVERSE_PROXY_EXTRA}
}"
fi
local _SITE_BLOCK local _SITE_BLOCK
_SITE_BLOCK="$(cat << CADDY_BLOCK _SITE_BLOCK="$(cat << CADDY_BLOCK
@@ -954,7 +971,7 @@ ${SERVICE_DOMAIN} {
# after it would be dead code that never runs — full bypass regardless # after it would be dead code that never runs — full bypass regardless
# of what the auth server's own rules say. # of what the auth server's own rules say.
${EXTRA_CONFIG} ${EXTRA_CONFIG}
reverse_proxy ${_BLOCK_UPSTREAM} ${_REVERSE_PROXY_LINE}
# Security headers # Security headers
header { header {
+90 -8
View File
@@ -83,7 +83,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
} }
configure_caddy_for_service() { configure_caddy_for_service() {
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" _rp_extra="${5:-}"
local _caddy_dir="$DOCKER_DIR/caddy" local _caddy_dir="$DOCKER_DIR/caddy"
local _caddyfile="$_caddy_dir/Caddyfile" local _caddyfile="$_caddy_dir/Caddyfile"
local _display_port="${_upstream##*:}" local _display_port="${_upstream##*:}"
@@ -126,12 +126,23 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}" _block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
fi fi
local _rp_line="reverse_proxy ${_block_upstream}"
if [[ -n "$_rp_extra" ]]; then
_rp_line="reverse_proxy ${_block_upstream} {
${_rp_extra}
}"
fi
local _site_block local _site_block
_site_block="$(cat << CBLOCK _site_block="$(cat << CBLOCK
# $_name # $_name
${_domain} { ${_domain} {
reverse_proxy ${_block_upstream} # Auth (if any) must come before reverse_proxy — see lib/common.sh's
# configure_caddy_for_service for why (reverse_proxy first would answer
# every request itself, making an auth block after it dead code).
${_extra}
${_rp_line}
header { header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
@@ -144,7 +155,6 @@ ${_domain} {
output file /var/log/caddy/${_domain}.log output file /var/log/caddy/${_domain}.log
format json format json
} }
${_extra}
} }
CBLOCK CBLOCK
)" )"
@@ -526,6 +536,10 @@ install_frigate() {
echo " - Prompt to add cameras interactively (RTSP creds go in .env)" echo " - Prompt to add cameras interactively (RTSP creds go in .env)"
echo " or write a starter config.yml if none are added" echo " or write a starter config.yml if none are added"
echo " - Offer a Caddy reverse proxy and to start the container" echo " - Offer a Caddy reverse proxy and to start the container"
echo " - If Authelia is installed: offer to protect Frigate with it —"
echo " disables Frigate's own login (auth.enabled: False) and pins a"
echo " proxy.auth_secret/X-Proxy-Secret handshake so only Caddy can"
echo " satisfy Frigate's proxy-auth trust"
return 0 return 0
fi fi
@@ -646,6 +660,51 @@ FRIGATE_COMPOSE
mkdir -p config mkdir -p config
mkdir -p "$FRIGATE_MEDIA" mkdir -p "$FRIGATE_MEDIA"
# Authelia SSO — decided (and, if accepted, wired into Caddy) before
# config.yml is written, so the auth block baked into config.yml only
# ever reflects a gate that's actually in place (never "native login
# disabled, but nothing put in front of it instead"). Frigate has its
# own built-in login (username/password) separate from Authelia's —
# left alone it would show *after* Authelia's forward_auth already
# gated the domain: a redundant second login, and worse, a second
# session that can expire independently and force a re-login on its
# own schedule regardless of Authelia's "remember me" duration. The
# proxy.auth_secret/X-Proxy-Secret handshake (pinned into the Caddy
# reverse_proxy block) stops that trust from being spoofed by a
# request that reaches Frigate's published host port directly,
# bypassing Caddy/Authelia entirely.
local FRIGATE_USE_AUTHELIA="n" FRIGATE_PROXY_SECRET="" AUTH_CONFIG_BLOCK=""
if [ -d "$DOCKER_DIR/authelia" ]; then
echo ""
prompt_yn "Protect Frigate with Authelia SSO (disables Frigate's own login)? (y/n):" "y" FRIGATE_USE_AUTHELIA
fi
if [[ "$FRIGATE_USE_AUTHELIA" =~ ^[Yy]$ ]]; then
FRIGATE_PROXY_SECRET="${ENV_MAP[FRIGATE_PROXY_AUTH_SECRET]:-$(generate_password 32)}"
configure_caddy_for_service "Frigate" "frigate:5000" "frigate" \
" import authelia" \
" header_up X-Proxy-Secret ${FRIGATE_PROXY_SECRET}"
if [ "${CADDY_SERVICE_CONFIGURED:-false}" = true ]; then
AUTH_CONFIG_BLOCK="auth:
enabled: False # Authelia already gates the whole domain — its own login would be redundant
proxy:
auth_secret: \"{FRIGATE_PROXY_AUTH_SECRET}\" # must match the X-Proxy-Secret header Caddy sends
header_map:
user: remote-user
role: remote-groups
default_role: admin # anyone who passes Authelia gets full access, same as the disabled local login did
"
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "frigate" "$CADDY_SERVICE_DOMAIN"
else
log_warning "Caddy wasn't configured for Frigate — leaving Frigate's own login enabled (nothing else is gating access)."
FRIGATE_PROXY_SECRET=""
fi
else
configure_caddy_for_service "Frigate" "frigate:5000" "frigate"
fi
# Credentials/IPs go in .env as FRIGATE_* variables; Frigate substitutes # Credentials/IPs go in .env as FRIGATE_* variables; Frigate substitutes
# any {FRIGATE_VAR} placeholder in config.yml from its container env at # any {FRIGATE_VAR} placeholder in config.yml from its container env at
# startup, so RTSP secrets never need to be typed into the YAML directly. # startup, so RTSP secrets never need to be typed into the YAML directly.
@@ -654,12 +713,12 @@ FRIGATE_COMPOSE
if [ "${#CAM_NAME[@]}" -eq 0 ]; then if [ "${#CAM_NAME[@]}" -eq 0 ]; then
# No cameras entered — write a starter config the operator edits by hand. # No cameras entered — write a starter config the operator edits by hand.
cat > config/config.yml << 'FRIGATE_CONFIG' cat > config/config.yml << FRIGATE_CONFIG
# Frigate Configuration — Docs: https://docs.frigate.video # Frigate Configuration — Docs: https://docs.frigate.video
# #
# ⚠️ YOU MUST EDIT THIS FILE to add your cameras before starting Frigate. # ⚠️ YOU MUST EDIT THIS FILE to add your cameras before starting Frigate.
mqtt: ${AUTH_CONFIG_BLOCK}mqtt:
enabled: false # Set to true and configure if you use Home Assistant enabled: false # Set to true and configure if you use Home Assistant
cameras: cameras:
@@ -696,7 +755,7 @@ FRIGATE_CONFIG
# RTSP credentials/IPs come from .env — Frigate substitutes {FRIGATE_VAR} # RTSP credentials/IPs come from .env — Frigate substitutes {FRIGATE_VAR}
# placeholders below from the container's environment at startup. # placeholders below from the container's environment at startup.
mqtt: ${AUTH_CONFIG_BLOCK}mqtt:
enabled: false # Set to true and configure if you use Home Assistant enabled: false # Set to true and configure if you use Home Assistant
go2rtc: go2rtc:
@@ -724,6 +783,7 @@ FRIGATE_CONFIG
cat > .env << FRIGATE_ENV cat > .env << FRIGATE_ENV
FRIGATE_MEDIA=$FRIGATE_MEDIA FRIGATE_MEDIA=$FRIGATE_MEDIA
CADDY_NET=$SITE_CADDY_NET CADDY_NET=$SITE_CADDY_NET
FRIGATE_PROXY_AUTH_SECRET=$FRIGATE_PROXY_SECRET
${ENV_CAM_VARS} ${ENV_CAM_VARS}
FRIGATE_ENV FRIGATE_ENV
chmod 600 .env chmod 600 .env
@@ -732,7 +792,29 @@ FRIGATE_ENV
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true
log_success "Frigate configured at $FRIGATE_DIR" log_success "Frigate configured at $FRIGATE_DIR"
configure_caddy_for_service "Frigate" "frigate:5000" "frigate" local AUTH_README_SECTION=""
if [ -n "$AUTH_CONFIG_BLOCK" ]; then
AUTH_README_SECTION="
## Authelia SSO
Frigate's own login is disabled (\`auth.enabled: False\` in
\`config/config.yml\`) — Authelia gates the whole domain instead via Caddy's
\`import authelia\` plus a \`proxy.auth_secret\`/\`X-Proxy-Secret\` handshake
(the secret lives in \`.env\` as \`FRIGATE_PROXY_AUTH_SECRET\`) so that trust
can't be spoofed by a request that reaches Frigate's published port
directly, bypassing Caddy.
Everyone who passes Authelia gets full (admin) access to Frigate —
adjust \`config/config.yml\`'s \`proxy.role_map\`/\`default_role\` plus
Authelia's own group assignments if you want to give some users
view-only access instead.
To stop Authelia asking for a login again on repeat visits (e.g. from a
phone) for as long as possible, increase its \"remember me\" session
duration: \`sudo ./setup.sh authelia\` → \"Change 'remember me' session
duration\" (this affects every domain that instance protects, not just
Frigate).
"
fi
write_readme "$FRIGATE_DIR" << MD write_readme "$FRIGATE_DIR" << MD
# Frigate NVR # Frigate NVR
@@ -746,7 +828,7 @@ security cameras. Detects people, cars, animals, and more.
- Recordings: \`$FRIGATE_MEDIA\` - Recordings: \`$FRIGATE_MEDIA\`
- Config: \`config/config.yml\` — cameras configured during install (${#CAM_NAME[@]} total) - Config: \`config/config.yml\` — cameras configured during install (${#CAM_NAME[@]} total)
- Credentials: \`.env\` — RTSP user/pass/IP per camera as FRIGATE_* variables - Credentials: \`.env\` — RTSP user/pass/IP per camera as FRIGATE_* variables
${AUTH_README_SECTION}
## Manage ## Manage
\`\`\`bash \`\`\`bash
cd $FRIGATE_DIR cd $FRIGATE_DIR