anubis: bot-challenge proxy in front of the guarded vhosts (two-tier chain)
Bots and AI scrapers hit the SSO-fronted vhosts and eat Authentik auth work for 403s. Anubis verifies clients first: DENY for known AI scraper UAs, JS challenge for everyone else; verified traffic resumes the normal Authentik forward_auth chain on Caddy's internal listener (:8016). Chain: client -> Caddy (TLS) -> Anubis :8923 (verify) -> Caddy :8016 (auth + routing resumes; Host survives both hops) -> app. Reverse-proxy through Anubis, not forward_auth: at the pinned tag a CHALLENGE answers HTTP 200 + HTML, and forward_auth treats 200 as "authenticated" — the page would pass through unchallenged. Exempt by construction (never routed through Anubis): plex (plain HTTP legacy), auth.* (IdP login must be reachable ungated), gitea (git smart-HTTP clients cannot solve JS challenges), and every app with its own login. Pinned ghcr.io/TecharoHQ/anubis:v1.13.0 — the tag was source-verified before pinning: at this tag the policy parser is JSON-only with a minimal bots[] schema (no imports, no CEL expressions), flags are the config surface via flagenv (TARGET/POLICY_FNAME map to --target/ --policy-fname), and the image's exec-form HEALTHCHECK is inherited by the quadlet unit (dodges the v1 generator's HealthCmd space-split quirk). Rollback nuance: stopping anubis 502s everything behind the chain (the public listeners proxy through it); a true rollback is reverting the Caddyfile and redeploying, not just systemctl --user stop anubis.
This commit is contained in:
1 parent
7b38ea63ef
commit
0ae3650414
10 files changed
+328
-94
No files matched your search
+134
-83
@@ -11,13 +11,19 @@
|
||||
# (dankclaw/pihole/addn-hosts — every name resolves to 192.168.0.143)
|
||||
# and handed to LAN clients by the SURFboard G36 router.
|
||||
#
|
||||
# Authentication: Caddy forward_auth (Authentik SSO) protects the vhosts that
|
||||
# do not have their own application-level auth (homepage, SearXNG, Hermes,
|
||||
# Prometheus, RomM, Copyparty). The basicauth hash is a placeholder; deploy.sh
|
||||
# renders the real hash (CADDY_BASICAUTH_HASH from the host .env, git-ignored)
|
||||
# into /home/dank/Containers/caddy/Caddyfile, which the container mounts.
|
||||
# Apps with their own auth (Grafana, Gitea, OpenWebUI, Jellyfin, *arr, Pi-hole)
|
||||
# are not wrapped in forward_auth — they handle their own login.
|
||||
# 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.
|
||||
# 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:
|
||||
# their vhosts never route through Anubis. The basicauth hash is a
|
||||
# placeholder; deploy.sh renders the real hash (CADDY_BASICAUTH_HASH from the
|
||||
# host .env, git-ignored) into /home/dank/Containers/caddy/Caddyfile, which
|
||||
# the container mounts.
|
||||
#
|
||||
# TLS: real Let's Encrypt certs. Site addresses carry no http:// prefix,
|
||||
# so Caddy serves HTTPS (and auto-redirects HTTP). Certs are issued via
|
||||
@@ -28,7 +34,7 @@
|
||||
{
|
||||
# Admin stays ON: it binds to 127.0.0.1:2019 inside the container and the
|
||||
# port is not published, so nothing outside the container can reach it.
|
||||
# It powers the config file watcher, so a rendered Caddyfile change
|
||||
# It powers the config file watcher, so a rendered-config change
|
||||
# hot-reloads without a container restart. 'admin off' once left a broken
|
||||
# basicauth hash sitting on disk unapplied for hours (2026-08-22).
|
||||
email admin@franksdank.cloud
|
||||
@@ -47,42 +53,10 @@
|
||||
redir https://franksdank.cloud/ 302
|
||||
}
|
||||
|
||||
# --- Homepage (Authentik SSO) ---
|
||||
# --- Homepage (Anubis → Authentik SSO) ---
|
||||
franksdank.cloud www.franksdank.cloud {
|
||||
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 anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +71,8 @@ dankserver.franksdank.cloud {
|
||||
}
|
||||
|
||||
# --- Authentik (SSO / OIDC provider) ---
|
||||
# Exempt by construction: the IdP login flow must be reachable without a
|
||||
# challenge, so this vhost never routes through Anubis.
|
||||
auth.franksdank.cloud {
|
||||
reverse_proxy authentik-server:9000
|
||||
}
|
||||
@@ -107,14 +83,8 @@ auth.franksdank.cloud {
|
||||
|
||||
# --- SearXNG (quadlet; behind the Gluetun tunnel, published 8081) ---
|
||||
search.franksdank.cloud {
|
||||
route {
|
||||
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.143:8081
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,18 +100,16 @@ chat.franksdank.cloud {
|
||||
|
||||
# --- Hermes ---
|
||||
hermes.franksdank.cloud {
|
||||
route {
|
||||
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
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Gitea (quadlet; published 3000) ---
|
||||
# Exempt by construction: Gitea has its own login, and git smart-HTTP
|
||||
# clients (git push/fetch over HTTPS) cannot solve JS challenges. The
|
||||
# policy would need a bypass for /info/refs + /git-*-pack paths if this
|
||||
# ever moves behind Anubis.
|
||||
gitea.franksdank.cloud {
|
||||
reverse_proxy 192.168.0.143:3000
|
||||
}
|
||||
@@ -156,14 +124,8 @@ grafana.franksdank.cloud {
|
||||
|
||||
# --- Prometheus ---
|
||||
prometheus.franksdank.cloud {
|
||||
route {
|
||||
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
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,27 +179,15 @@ books.franksdank.cloud {
|
||||
|
||||
# --- RomM ---
|
||||
romm.franksdank.cloud {
|
||||
route {
|
||||
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
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Copyparty ---
|
||||
files.franksdank.cloud {
|
||||
route {
|
||||
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
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +225,107 @@ qbittorrent.franksdank.cloud {
|
||||
reverse_proxy 192.168.0.235:8080
|
||||
}
|
||||
|
||||
# --- Internal listener: where Anubis forwards accepted traffic ---
|
||||
# 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.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 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
|
||||
}
|
||||
}
|
||||
|
||||
# --- Optional: Jackett ---
|
||||
# jackett.franksdank.cloud {
|
||||
# reverse_proxy 192.168.0.235:9117
|
||||
|
||||
Reference in new issue
Block a user