From 29f9a86884067908f1d0d914f142664e05c41c0d Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 29 Aug 2026 23:49:39 -0700 Subject: [PATCH] caddy+authentik tests: uniform Host-preserving :8016 proxy through embedded outpost The redirect-to-flow design with an absolute cross-host next= was rejected by the flow's open-redirect guard at completion ("Request has been denied: Invalid next URL"), and meta-header routing does not exist in this build (providers route on the ORIGINAL Host). :8016 now proxies every request whole to authentik-server:9000 preserving Host; the embedded outpost serves login in place and forwards authenticated traffic back to :8017. Requires one proxy provider per guarded host (provisioned live: search/hermes/prometheus/romm/ files + www, flows backfilled). Tests flipped from the meta-header claim to the upheld contract: Host preserved, no absolute next= redirects. --- dankclaw/Caddyfile | 58 +++++++++++--------------------------------- tests/authentik.bats | 27 ++++++++++++--------- 2 files changed, 30 insertions(+), 55 deletions(-) diff --git a/dankclaw/Caddyfile b/dankclaw/Caddyfile index db177da..3d46d08 100644 --- a/dankclaw/Caddyfile +++ b/dankclaw/Caddyfile @@ -234,53 +234,23 @@ prowlarr.franksdank.cloud { qbittorrent.franksdank.cloud { reverse_proxy 192.168.0.235:8080 } -# --- Internal listener: auth decision layer ------------------------------- -# Anubis dials http://caddy:8016 after verifying a client (TARGET in -# dankclaw/anubis/quadlet/anubis.container). Host headers survive the hop, so -# per-vhost routing resumes here: guarded vhosts proxy the WHOLE request -# through Authentik (the meta headers tell Authentik which outpost, provider -# and app are answering); unauthenticated clients get the login flow served -# in place, authenticated traffic is forwarded back to http://:8017 and -# routed straight to the app. Exempt vhosts never enter this block. # --- Internal listener: auth decision layer --------------------------------- # Anubis dials http://caddy:8016 after verifying a client (TARGET in -# dankclaw/anubis/quadlet/anubis.container). Host headers survive the hop, so -# per-vhost routing resumes here. This 2026.x build does not mount the outpost -# URL space for proxy-provider meta-header routing (probed live: /ping answers -# 204, every app path with meta headers 404s), so guarded vhosts route -# whole-request to Authentik's flow endpoint instead: unauthenticated clients -# get the login flow served in place; authenticated sessions complete instantly -# and bounce back via next= straight to the app at http://:8017. Exempt vhosts -# never enter this block. +# dankclaw/anubis/quadlet/anubis.container). Host headers survive the hop. +# Every request here is proxied whole-request to Authentik with Host +# preserved. This build's embedded outpost routes ONLY on the ORIGINAL Host +# against each proxy provider's external_host — meta headers are ignored and +# cross-host absolute next= redirects are rejected by the flow's open-redirect +# guard ("Request has been denied: Invalid next URL"), verified live. +# Unauthenticated clients get the login flow served in place; authenticated +# sessions are forwarded by the provider's internal_host back to +# http://:8017 and routed straight to the app. Every host that may enter this +# listener MUST have a proxy provider registered with the embedded outpost — +# the Anubis guard set, the Caddy TLS edge, and the provider list stay in +# lockstep. Exempt vhosts never enter this block. http://:8016 { - @home host www.franksdank.cloud - handle @home { - redir https://auth.franksdank.cloud/if/flow/default-authentication-flow/?next=https://www.franksdank.cloud/ 302 - } - - @search host search.franksdank.cloud - handle @search { - redir https://auth.franksdank.cloud/if/flow/default-authentication-flow/?next=https://search.franksdank.cloud/ 302 - } - - @hermes host hermes.franksdank.cloud - handle @hermes { - redir https://auth.franksdank.cloud/if/flow/default-authentication-flow/?next=https://hermes.franksdank.cloud/ 302 - } - - @prometheus host prometheus.franksdank.cloud - handle @prometheus { - redir https://auth.franksdank.cloud/if/flow/default-authentication-flow/?next=https://prometheus.franksdank.cloud/ 302 - } - - @romm host romm.franksdank.cloud - handle @romm { - redir https://auth.franksdank.cloud/if/flow/default-authentication-flow/?next=https://romm.franksdank.cloud/ 302 - } - - @files host files.franksdank.cloud - handle @files { - redir https://auth.franksdank.cloud/if/flow/default-authentication-flow/?next=https://files.franksdank.cloud/ 302 + reverse_proxy authentik-server:9000 { + header_up X-Forwarded-Proto https } } diff --git a/tests/authentik.bats b/tests/authentik.bats index 42ccb28..9681d23 100644 --- a/tests/authentik.bats +++ b/tests/authentik.bats @@ -2,14 +2,17 @@ # Seam: Authentik (SSO / OIDC provider) — the homelab's auth layer. # # Authentik is a 3-service stack (PostgreSQL + server + worker) on dankclaw. -# It provides OIDC, SAML, and Caddy forward_auth for the vhosts that do not -# have their own application-level authentication (SearXNG, Hermes, -# Prometheus, RomM, Copyparty, the homepage). +# It protects the vhosts that have no application-level authentication +# (SearXNG, Hermes, Prometheus, RomM, Copyparty, the homepage) with its +# embedded proxy outpost. # -# The Caddyfile carries an `auth.franksdank.cloud` vhost that proxies to the -# Authentik server. Vhosts without their own auth are wrapped in a `route` -# block that (1) proxies the outpost path to Authentik, (2) runs forward_auth, -# (3) serves the app. +# The Caddyfile carries an `auth.franksdank.cloud` vhost for the admin UI, and +# an internal `:8016` listener that Anubis dials after verifying a client. +# :8016 proxies every request whole to the Authentik server with the Host +# header preserved; the embedded outpost routes on the ORIGINAL Host against +# each proxy provider's external_host (meta headers do NOT route it — +# disproved live, 2026-08-29) and forwards authenticated traffic back to the +# post-auth listener `:8017`. # # See ../docs/Infrastructure/Authentik.md. @@ -101,10 +104,12 @@ UNIT_DIR="$REPO_ROOT/dankclaw/authentik/quadlet" awk '/^http:\/\/:8016/,/^\}/' "$CADDYFILE" | grep -q 'reverse_proxy authentik-server:9000' } -@test "the auth decision carries the outpost meta headers" { - grep -q 'X-authentik-meta-outpost' "$CADDYFILE" - grep -q 'X-authentik-meta-provider' "$CADDYFILE" - grep -q 'X-authentik-meta-app' "$CADDYFILE" +@test "the internal listener preserves Host and never emits cross-host next=" { + # The outpost routes on the original Host: no header_up Host override... + ! grep -q 'header_up Host' "$CADDYFILE" + # ...and the rejected design (absolute cross-host next= into the flow UI) + # must never return — it produces "Invalid next URL" at flow completion. + ! grep -qE 'if/flow/[^ ]*next=https' "$CADDYFILE" } @test "authenticated traffic returns to Caddy on the post-auth listener" {