diff --git a/dankclaw/Caddyfile b/dankclaw/Caddyfile
index fa2d90c..b6db27e 100644
--- a/dankclaw/Caddyfile
+++ b/dankclaw/Caddyfile
@@ -11,12 +11,17 @@
# (dankclaw/pihole/addn-hosts — every name resolves to 192.168.0.143)
# and handed to LAN clients by the SURFboard G36 router.
#
-# Authentication chain (Anubis rollout, 2026-08-28): the guarded vhosts
-# (homepage, SearXNG, Hermes, Prometheus, RomM, Copyparty) proxy EVERYTHING
-# through the Anubis bot-challenge proxy first. Chain: client → Caddy (TLS,
-# public listener) → Anubis :8923 (verify: DENY known AI scrapers, CHALLENGE
-# everyone else) → Caddy's internal listener http://:8016 (Authentik
-# forward_auth + app routing resumes; Host headers survive both hops) → app.
+# Authentication chain (2026-08-29): the guarded vhosts (homepage, SearXNG,
+# Hermes, Prometheus, RomM, Copyparty) proxy EVERYTHING through the Anubis
+# bot-challenge proxy first, then the whole request is proxied THROUGH
+# Authentik (2026.8 proxy-provider mode: this build serves no forward_auth
+# endpoint; the authentication flow runs inline — unauthenticated clients get
+# the login UI served in place, and once a session exists Authentik proxies
+# the request on to the app via the provider's internal_host). Chain: client →
+# Caddy (TLS, public listener) → Anubis :8923 (verify: DENY known AI scrapers,
+# CHALLENGE everyone else) → Caddy's internal listener http://:8016 (auth
+# decision; Host headers survive every hop) → authenticated traffic is
+# forwarded back to http://:8017 and routed straight to the app.
# Apps with their own auth (Grafana, Gitea, OpenWebUI, ComfyUI, Jellyfin,
# *arr, Pi-hole) and the Authentik IdP itself (auth.franksdank.cloud — its
# login flow must never sit behind a challenge) are exempt by construction:
@@ -224,104 +229,115 @@ prowlarr.franksdank.cloud {
qbittorrent.franksdank.cloud {
reverse_proxy 192.168.0.235:8080
}
-
-# --- Internal listener: where Anubis forwards accepted traffic ---
+# --- 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: outpost paths go straight to the
-# Authentik outpost, everything else runs the forward_auth check and then
-# dials the app. This block is the second half of every guarded vhost.
+# 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.
http://:8016 {
@home host franksdank.cloud www.franksdank.cloud
handle @home {
- root * /srv/homepage
- route {
- # Authentik outpost (login UI + session management).
- reverse_proxy /outpost.goauthentik.io/* authentik-server:9000
-
- # Forward auth: check the Authentik session before serving the page.
- forward_auth authentik-server:9000 {
- uri /outpost.goauthentik.io/auth/caddy
- copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
- trusted_proxies private_ranges
- }
-
- # Operator-only admin page. Auth is checked on the original path,
- # before any rewrite, so the path matcher is reliable. 'handle'
- # preserves order.
- handle /admin* {
- try_files /admin.html
- file_server
- }
-
- # Shared data + assets: fetched by both the user page and the admin
- # page. With forward_auth, any authenticated user can access them
- # (the browser's Authentik session covers these fetches).
- @data path /services.json /status.json /app.js /style.css /deployed.js
- handle @data {
- file_server
- }
-
- # Everything else (the user page).
- handle {
- try_files /index.html
- file_server
- }
+ reverse_proxy authentik-server:9000 {
+ header_up X-authentik-meta-outpost 7e39655b-58d3-4fe6-a80a-513f31051217
+ header_up X-authentik-meta-provider 1
+ header_up X-authentik-meta-app name=caddy
}
}
@search host search.franksdank.cloud
handle @search {
- reverse_proxy /outpost.goauthentik.io/* authentik-server:9000
- forward_auth authentik-server:9000 {
- uri /outpost.goauthentik.io/auth/caddy
- copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
- trusted_proxies private_ranges
+ reverse_proxy authentik-server:9000 {
+ header_up X-authentik-meta-outpost 7e39655b-58d3-4fe6-a80a-513f31051217
+ header_up X-authentik-meta-provider 1
+ header_up X-authentik-meta-app name=caddy
}
+ }
+
+ @hermes host hermes.franksdank.cloud
+ handle @hermes {
+ reverse_proxy authentik-server:9000 {
+ header_up X-authentik-meta-outpost 7e39655b-58d3-4fe6-a80a-513f31051217
+ header_up X-authentik-meta-provider 1
+ header_up X-authentik-meta-app name=caddy
+ }
+ }
+
+ @prometheus host prometheus.franksdank.cloud
+ handle @prometheus {
+ reverse_proxy authentik-server:9000 {
+ header_up X-authentik-meta-outpost 7e39655b-58d3-4fe6-a80a-513f31051217
+ header_up X-authentik-meta-provider 1
+ header_up X-authentik-meta-app name=caddy
+ }
+ }
+
+ @romm host romm.franksdank.cloud
+ handle @romm {
+ reverse_proxy authentik-server:9000 {
+ header_up X-authentik-meta-outpost 7e39655b-58d3-4fe6-a80a-513f31051217
+ header_up X-authentik-meta-provider 1
+ header_up X-authentik-meta-app name=caddy
+ }
+ }
+
+ @files host files.franksdank.cloud
+ handle @files {
+ reverse_proxy authentik-server:9000 {
+ header_up X-authentik-meta-outpost 7e39655b-58d3-4fe6-a80a-513f31051217
+ header_up X-authentik-meta-provider 1
+ header_up X-authentik-meta-app name=caddy
+ }
+ }
+}
+
+# --- Post-auth listener: where Authentik forwards authenticated traffic ----
+# The proxy provider's internal_host is http://caddy:8017, so every request
+# that passes authentication lands here (Host header preserved) and routes
+# straight to its app. This is the end of the chain — nothing dials back
+# into Authentik from here, so the chain cannot loop.
+http://:8017 {
+ @home host franksdank.cloud www.franksdank.cloud
+ handle @home {
+ root * /srv/homepage
+ handle /admin* {
+ try_files /admin.html
+ file_server
+ }
+ @data path /services.json /status.json /app.js /style.css /deployed.js
+ handle @data {
+ file_server
+ }
+ handle {
+ try_files /index.html
+ file_server
+ }
+ }
+
+ @search host search.franksdank.cloud
+ handle @search {
reverse_proxy 192.168.0.143:8081
}
@hermes host hermes.franksdank.cloud
handle @hermes {
- reverse_proxy /outpost.goauthentik.io/* authentik-server:9000
- forward_auth authentik-server:9000 {
- uri /outpost.goauthentik.io/auth/caddy
- copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
- trusted_proxies private_ranges
- }
reverse_proxy hermes:8090
}
@prometheus host prometheus.franksdank.cloud
handle @prometheus {
- reverse_proxy /outpost.goauthentik.io/* authentik-server:9000
- forward_auth authentik-server:9000 {
- uri /outpost.goauthentik.io/auth/caddy
- copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
- trusted_proxies private_ranges
- }
reverse_proxy prometheus:9090
}
@romm host romm.franksdank.cloud
handle @romm {
- reverse_proxy /outpost.goauthentik.io/* authentik-server:9000
- forward_auth authentik-server:9000 {
- uri /outpost.goauthentik.io/auth/caddy
- copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
- trusted_proxies private_ranges
- }
reverse_proxy 192.168.0.235:8085
}
@files host files.franksdank.cloud
handle @files {
- reverse_proxy /outpost.goauthentik.io/* authentik-server:9000
- forward_auth authentik-server:9000 {
- uri /outpost.goauthentik.io/auth/caddy
- copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
- trusted_proxies private_ranges
- }
reverse_proxy 192.168.0.235:3923
}
}
diff --git a/serverAppConfigs-docs/Infrastructure/Authentik.md b/serverAppConfigs-docs/Infrastructure/Authentik.md
index fd366c0..8f95033 100644
--- a/serverAppConfigs-docs/Infrastructure/Authentik.md
+++ b/serverAppConfigs-docs/Infrastructure/Authentik.md
@@ -24,41 +24,27 @@ so the stack is 3 services, not 4.
```mermaid
graph LR
- subgraph Browser
- U[User]
- end
- subgraph Caddy [Caddy - auth gate]
- FA[forward_auth]
- end
- subgraph AK [Authentik]
- SRV[server :9000]
- WKR[worker]
- PG[(postgres)]
- OUT[outpost /auth/caddy]
- end
- subgraph App [Target app]
- A1[SearXNG / Hermes / Prometheus
RomM / Copyparty / homepage]
- end
-
- U -->|1. GET /| FA
- FA -->|2. GET /outpost.../auth/caddy| OUT
- OUT -->|3. no session: 302 login| U
- U -->|4. login at auth.franksdank.cloud| SRV
- SRV --> PG
- WKR --> PG
- SRV -->|5. 2xx: session valid| FA
- FA -->|6. proxy with headers| A1
+ U[Browser] -->|1. GET /search?q=x| A[Anubis :8923
bot challenge]
+ A -->|2. verified, dials internal listener| C[Caddy :8016
auth decision]
+ C -->|3. whole request + meta headers| S[authentik-server :9000
proxy provider 1]
+ S -->|4. no session: login flow served in place| U
+ S -->|5. session valid: proxies on to internal_host| P[Caddy :8017
post-auth routing]
+ P -->|6. app response back up the chain| U
+ S --> PG[(postgres)]
```
-1. The browser hits a protected vhost (e.g. `search.franksdank.cloud`).
-2. Caddy `forward_auth` clones the request to the Authentik outpost
- (`/outpost.goauthentik.io/auth/caddy`).
-3. With no valid session, the outpost returns a `302` to the Authentik login
- page (`auth.franksdank.cloud`).
-4. The user logs in at Authentik (email + password, TOTP/WebAuthn MFA, or an
- upstream provider).
-5. On the next request the outpost returns `2xx`; Caddy copies the
- `X-Authentik-*` headers and proxies to the app.
+In authentik 2026.8 the proxy provider is an OAuth2 provider and the old
+`/outpost.goauthentik.io/auth/caddy` forward-auth endpoint no longer exists —
+every request that reaches it returns 404 (verified live, 2026-08-29). The
+chain therefore does not forward-auth *to* a path: guarded vhosts proxy the
+**whole request through authentik** with `X-authentik-meta-outpost`,
+`X-authentik-meta-provider` and `X-authentik-meta-app` headers. The provider's
+`authentication_flow` (`default-authentication-flow`) decides: no session →
+the login flow is served in place; session valid → the policy passes and
+authentik proxies the request on to the provider's `internal_host`
+(`http://caddy:8017`, Caddy's post-auth internal listener), which routes it
+straight to the app. The chain terminates — nothing dials back into
+Authentik from `:8017`.
## The three compose services
@@ -79,30 +65,39 @@ graph LR
## Caddy integration
-The Caddyfile wraps the no-auth vhosts in a `route` block. The pattern (one
-example; the others are identical):
+Guarded vhosts proxy the **whole request through Authentik** on Caddy's
+internal listener — there is no `forward_auth` anywhere: in 2026.8 the auth
+endpoint the old config pointed at (`/outpost.goauthentik.io/auth/caddy`)
+does not exist in the URL tree, so every unauthenticated subrequest got a
+404 and browsers were served authentik's own Not-Found page (verified live,
+2026-08-29). Pattern (one example; the other guarded vhosts are identical):
```caddyfile
-search.franksdank.cloud {
- route {
- # 1. The outpost path is proxied to Authentik (login UI + session).
- reverse_proxy /outpost.goauthentik.io/* authentik-server:9000
- # 2. forward_auth checks the session before serving the app.
- forward_auth authentik-server:9000 {
- uri /outpost.goauthentik.io/auth/caddy
- copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
- trusted_proxies private_ranges
+http://:8016 {
+ @search host search.franksdank.cloud
+ handle @search {
+ # Unauthenticated: the login flow is served in place.
+ # Authenticated: Authentik proxies on to internal_host (:8017).
+ reverse_proxy authentik-server:9000 {
+ header_up X-authentik-meta-outpost 7e39655b-58d3-4fe6-a80a-513f31051217
+ header_up X-authentik-meta-provider 1
+ header_up X-authentik-meta-app name=caddy
}
- # 3. The app.
- reverse_proxy 192.168.0.143:8081
}
}
```
-Vhosts protected by forward_auth: the homepage (`franksdank.cloud`,
-`www.franksdank.cloud`), SearXNG, Hermes, Prometheus, RomM, Copyparty. The
-`auth.franksdank.cloud` vhost proxies to the server directly (no forward_auth —
-it *is* the login).
+The meta headers tell authentik which outpost, provider and application are
+answering (the values come from the DB: outpost pk = the UUID embedded in
+`AUTHENTIK_OUTPOST_TOKEN`, provider pk 1 = `embedded-outpost`, app slug
+`caddy`). The provider's `internal_host` is `http://caddy:8017`, so
+authenticated traffic is handed back to Caddy's post-auth listener, which
+routes it straight to the app — the chain terminates there.
+
+Guarded vhosts: the homepage (`franksdank.cloud`, `www.franksdank.cloud`),
+SearXNG, Hermes, Prometheus, RomM, Copyparty. The `auth.franksdank.cloud`
+vhost proxies to the server directly — it *is* the login, and it must never
+sit behind a challenge.
## Configuration (secrets)
diff --git a/tests/authentik.bats b/tests/authentik.bats
index e72e97a..49763ee 100644
--- a/tests/authentik.bats
+++ b/tests/authentik.bats
@@ -95,22 +95,20 @@ UNIT_DIR="$REPO_ROOT/dankclaw/authentik/quadlet"
echo "$block" | grep -q 'reverse_proxy authentik-server:9000'
}
-# --- Caddyfile: forward_auth on vhosts without their own auth ---
+# --- Caddyfile: guarded vhosts proxy the whole request through Authentik ----
-@test "the Caddyfile uses forward_auth" {
- grep -q 'forward_auth' "$CADDYFILE"
+@test "guarded vhosts proxy the whole request through authentik on the internal listener" {
+ awk '/^http:\/\/:8016/,/^\}/' "$CADDYFILE" | grep -q 'reverse_proxy authentik-server:9000'
}
-@test "forward_auth targets the authentik outpost caddy endpoint" {
- grep -q '/outpost.goauthentik.io/auth/caddy' "$CADDYFILE"
+@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 "forward_auth copies the X-Authentik-Username header" {
- grep -q 'X-Authentik-Username' "$CADDYFILE"
-}
-
-@test "the outpost path is proxied to authentik in forward_auth vhosts" {
- grep -q '/outpost.goauthentik.io/\*' "$CADDYFILE"
+@test "authenticated traffic returns to Caddy on the post-auth listener" {
+ grep -qE '^http://:8017 \{' "$CADDYFILE"
}
# --- Homepage: moved to naked domain + www ---
diff --git a/tests/caddyfile.bats b/tests/caddyfile.bats
index 464a6fb..8498972 100644
--- a/tests/caddyfile.bats
+++ b/tests/caddyfile.bats
@@ -125,10 +125,11 @@ CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile"
# franksdank.casa is not in the same Porkbun account, so it cannot get
# a DNS-01 Let's Encrypt cert; it stays explicitly http. Every other
# client-facing site address must carry no http:// prefix. The internal
- # listener (http://:8016) is not client-facing — Anubis dials it over
- # container DNS after verifying a client, so plain HTTP is intended.
+ # listeners (http://:8016, http://:8017) are not client-facing — Anubis
+ # and Authentik dial them over container DNS after verifying a client,
+ # so plain HTTP is intended.
local offenders
- offenders=$(grep -E '^http://' "$CADDYFILE" | grep -v 'plex\.franksdank\.casa' | grep -v '^http://:8016' || true)
+ offenders=$(grep -E '^http://' "$CADDYFILE" | grep -v 'plex\.franksdank\.casa' | grep -vE '^http://:801[67]' || true)
[ -z "$offenders" ] || { echo "non-.casa http:// vhosts: $offenders"; return 1; }
}
diff --git a/tests/homepage_caddy.bats b/tests/homepage_caddy.bats
index c02d8e7..a848a01 100644
--- a/tests/homepage_caddy.bats
+++ b/tests/homepage_caddy.bats
@@ -37,9 +37,11 @@ setup() {
local block
block=$(awk '/^franksdank\.cloud[[:space:]]/,/^}/' "$CADDY")
echo "$block" | grep -q 'reverse_proxy anubis:8923'
- # The SSO check itself now runs on the internal listener that Anubis
+ # The SSO check itself runs on the internal listener that Anubis
# forwards accepted traffic to; Host survives the hop, so per-vhost
- # routing (and with it forward_auth) resumes there.
+ # routing resumes there — guarded vhosts proxy the whole request
+ # through Authentik, which forwards authenticated traffic back on the
+ # post-auth listener (http://:8017) that routes straight to the app.
grep -q 'http://:8016' "$CADDY"
- awk '/^http:\/\/:8016/,/^\}/' "$CADDY" | grep -q 'forward_auth authentik-server:9000'
+ awk '/^http:\/\/:8016/,/^\}/' "$CADDY" | grep -q 'reverse_proxy authentik-server:9000'
}