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:
@@ -191,13 +191,25 @@ pip_user_install PACKAGE... # pip3 --user with --break-system-packages o
|
||||
### Caddy reverse proxy
|
||||
|
||||
```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
|
||||
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
|
||||
for `import authelia` or custom matchers).
|
||||
is an optional string inserted verbatim inside the Caddy site block, before
|
||||
`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
|
||||
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`
|
||||
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
|
||||
caller can tell whether Caddy actually ended up fronting the service:
|
||||
Sets three out-params (not `local` — read them after the call returns) so
|
||||
the caller can tell whether Caddy actually ended up fronting the service:
|
||||
|
||||
```bash
|
||||
CADDY_SERVICE_CONFIGURED # true/false
|
||||
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
|
||||
fronts *locally* (it reaches the service over `host.docker.internal`, not
|
||||
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
|
||||
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
|
||||
`configure_caddy_for_service` and pass `import authelia` as the extra block
|
||||
if Authelia is installed and the user wants SSO protection:
|
||||
|
||||
+20
-3
@@ -841,11 +841,19 @@ find_free_coturn_range() {
|
||||
}
|
||||
|
||||
# ── 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"),
|
||||
# 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() {
|
||||
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
|
||||
# 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}"
|
||||
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
|
||||
_SITE_BLOCK="$(cat << CADDY_BLOCK
|
||||
|
||||
@@ -954,7 +971,7 @@ ${SERVICE_DOMAIN} {
|
||||
# after it would be dead code that never runs — full bypass regardless
|
||||
# of what the auth server's own rules say.
|
||||
${EXTRA_CONFIG}
|
||||
reverse_proxy ${_BLOCK_UPSTREAM}
|
||||
${_REVERSE_PROXY_LINE}
|
||||
|
||||
# Security headers
|
||||
header {
|
||||
|
||||
+90
-8
@@ -83,7 +83,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
}
|
||||
|
||||
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 _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
@@ -126,12 +126,23 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
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
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_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 {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
@@ -144,7 +155,6 @@ ${_domain} {
|
||||
output file /var/log/caddy/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
@@ -526,6 +536,10 @@ install_frigate() {
|
||||
echo " - Prompt to add cameras interactively (RTSP creds go in .env)"
|
||||
echo " or write a starter config.yml if none are added"
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -646,6 +660,51 @@ FRIGATE_COMPOSE
|
||||
mkdir -p config
|
||||
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
|
||||
# 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.
|
||||
@@ -654,12 +713,12 @@ FRIGATE_COMPOSE
|
||||
|
||||
if [ "${#CAM_NAME[@]}" -eq 0 ]; then
|
||||
# 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
|
||||
#
|
||||
# ⚠️ 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
|
||||
|
||||
cameras:
|
||||
@@ -696,7 +755,7 @@ FRIGATE_CONFIG
|
||||
# RTSP credentials/IPs come from .env — Frigate substitutes {FRIGATE_VAR}
|
||||
# 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
|
||||
|
||||
go2rtc:
|
||||
@@ -724,6 +783,7 @@ FRIGATE_CONFIG
|
||||
cat > .env << FRIGATE_ENV
|
||||
FRIGATE_MEDIA=$FRIGATE_MEDIA
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
FRIGATE_PROXY_AUTH_SECRET=$FRIGATE_PROXY_SECRET
|
||||
${ENV_CAM_VARS}
|
||||
FRIGATE_ENV
|
||||
chmod 600 .env
|
||||
@@ -732,7 +792,29 @@ FRIGATE_ENV
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true
|
||||
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
|
||||
# Frigate NVR
|
||||
@@ -746,7 +828,7 @@ security cameras. Detects people, cars, animals, and more.
|
||||
- Recordings: \`$FRIGATE_MEDIA\`
|
||||
- Config: \`config/config.yml\` — cameras configured during install (${#CAM_NAME[@]} total)
|
||||
- Credentials: \`.env\` — RTSP user/pass/IP per camera as FRIGATE_* variables
|
||||
|
||||
${AUTH_README_SECTION}
|
||||
## Manage
|
||||
\`\`\`bash
|
||||
cd $FRIGATE_DIR
|
||||
|
||||
Reference in New Issue
Block a user