From 0764be44dac6ff3fd7cdc5d93a6b404f20940abf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 20:09:37 +0000 Subject: [PATCH] Fix Authelia bypass on remote-Authelia forward_auth (asterisk-digital-ocean) The remote-Authelia forward_auth block dialed a scheme-qualified upstream (https://auth.example.com), which is a second Caddy hop. Caddy rewrites the outgoing Host header to the upstream host for routing, and without an explicit override X-Forwarded-Host picked up that rewritten value instead of the original site's host. Authelia was evaluating every protected domain as auth.example.com itself (bypass policy), so 2FA never triggered for any domain behind the remote instance. Pin the forwarded headers to the original request explicitly to fix it. --- CLAUDE.md | 31 ++++++++++++++++++++++++++++++ services/asterisk-digital-ocean.sh | 18 +++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 0692e30..5869174 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,6 +173,37 @@ service builds its own site block instead of using this helper (e.g. `services/asterisk-digital-ocean.sh` does, deliberately, see its own comment for why), put its auth block first there too. +**`forward_auth` to a remote Authelia over a scheme-qualified URL needs +explicit `header_up` pins.** A bare `forward_auth authelia:9091` (Authelia on +the same Docker network, one hop) is fine relying on Caddy's default +`X-Forwarded-*` headers. But `forward_auth https://auth.example.com { ... }` +(Authelia on a *different* machine, reached over its own public domain+TLS — +see `services/asterisk-digital-ocean.sh`'s remote-Authelia prompt) is a +second Caddy hop: Caddy rewrites the outgoing request's `Host` header to +`auth.example.com` so the remote Caddy can route/SNI-match it, and without an +override `X-Forwarded-Host` picks up that rewritten value instead of the +original site's host. Confirmed live: Authelia evaluated *every* protected +domain as if the request were for `auth.example.com` itself (which typically +has `policy: bypass` in `access_control.rules` so its own login portal isn't +gated behind itself) — so every domain behind the remote instance silently +passed through with no 2FA prompt, regardless of that domain's own policy. +Fix: pin the forwarded headers to the original request explicitly instead of +trusting Caddy's default derivation: + +``` +forward_auth https://auth.example.com { + uri /api/authz/forward-auth + copy_headers Remote-User Remote-Groups Remote-Name Remote-Email + header_up X-Forwarded-Method {method} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Host {host} + header_up X-Forwarded-Uri {uri} +} +``` + +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: diff --git a/services/asterisk-digital-ocean.sh b/services/asterisk-digital-ocean.sh index 480fb23..235c85d 100755 --- a/services/asterisk-digital-ocean.sh +++ b/services/asterisk-digital-ocean.sh @@ -584,9 +584,27 @@ ENV local _remote_authelia="" prompt_text " Remote Authelia address — a bare host:port over a private network (e.g. a NetBird mesh IP:9091), or a full https:// URL if it's on its own public domain+TLS:" "" _remote_authelia if [[ -n "$_remote_authelia" ]]; then + # header_up lines are required here (unlike the local + # "authelia:9091" snippet in services/authelia.sh) because + # this upstream is reached over a second Caddy hop when + # given as a scheme-qualified URL (https://auth.example.com). + # Caddy rewrites the outgoing request's Host header to that + # upstream host so the remote Caddy can route/SNI-match it — + # and without an explicit override, X-Forwarded-Host picks up + # that rewritten value instead of the original site's host. + # Confirmed live: Authelia was evaluating every request as + # if it were for auth.example.com itself (which has + # policy: bypass in access_control.rules), so every domain + # silently passed through with no 2FA prompt regardless of + # its own policy. Pinning these to the original request's + # values fixes it regardless of hop count. EXTRA_BLOCK=" forward_auth ${_remote_authelia} { uri /api/authz/forward-auth copy_headers Remote-User Remote-Groups Remote-Name Remote-Email + header_up X-Forwarded-Method {method} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Host {host} + header_up X-Forwarded-Uri {uri} }" sed -i "s/^WEB_ADMIN_AUTH_DISABLED=.*/WEB_ADMIN_AUTH_DISABLED=true/" .env log_info "Using remote Authelia at ${_remote_authelia}."