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.
338 lines
11 KiB
Caddyfile
338 lines
11 KiB
Caddyfile
# Homelab Caddyfile — the single Caddy for both hosts.
|
|
#
|
|
# Caddy runs as a container on dankclaw (192.168.0.143, this repo's
|
|
# compose.yaml service 'caddy') and fronts every service on both hosts:
|
|
# - dankclaw services: by container name (compose network) or by the
|
|
# published LAN port (quadlet services on isolated networks)
|
|
# - dankserver services (192.168.0.235): by published LAN port
|
|
#
|
|
# Naming: flat franksdank.cloud scheme (see serverAppConfigs-docs/Infrastructure/LAN-DNS.md).
|
|
# DNS for these names is served by the Pi-hole quadlet on this host
|
|
# (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.
|
|
# 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
|
|
# dns-01 through the Porkbun module built into the franksdank/caddy-porkbun
|
|
# image (caddy/Containerfile). That needs no inbound ports and works from the
|
|
# LAN, where Pi-hole resolves these names to 192.168.0.143.
|
|
{
|
|
# 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-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
|
|
|
|
# Solve ACME challenges for every site via Porkbun DNS (no port 80/443
|
|
# reachability needed). Credentials come from the container environment
|
|
# (compose.yaml, from the host .env — never committed).
|
|
acme_dns porkbun {
|
|
api_key {env.PORKBUN_API_KEY}
|
|
api_secret_key {env.PORKBUN_API_SECRET_KEY}
|
|
}
|
|
}
|
|
|
|
# --- Bare-IP access (health checks, convenience) ---
|
|
:80 {
|
|
redir https://franksdank.cloud/ 302
|
|
}
|
|
|
|
# --- Homepage (Anubis → Authentik SSO) ---
|
|
franksdank.cloud www.franksdank.cloud {
|
|
reverse_proxy anubis:8923 {
|
|
header_up X-Real-Ip {remote_host}
|
|
}
|
|
}
|
|
|
|
# --- Legacy name: the old homepage name redirects to the naked domain ---
|
|
dankclaw.franksdank.cloud {
|
|
redir https://franksdank.cloud/ 302
|
|
}
|
|
|
|
# --- Legacy name: the media host has no Caddy of its own anymore ---
|
|
dankserver.franksdank.cloud {
|
|
redir https://franksdank.cloud/ 302
|
|
}
|
|
|
|
# --- 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
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Services on this host (dankclaw, 192.168.0.143)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# --- SearXNG (quadlet; behind the Gluetun tunnel, published 8081) ---
|
|
search.franksdank.cloud {
|
|
reverse_proxy anubis:8923 {
|
|
header_up X-Real-Ip {remote_host}
|
|
}
|
|
}
|
|
|
|
# --- ComfyUI ---
|
|
comfyui.franksdank.cloud {
|
|
reverse_proxy comfyui:8188
|
|
}
|
|
|
|
# --- OpenWebUI (quadlet on slirp4netns; published 3005) ---
|
|
chat.franksdank.cloud {
|
|
reverse_proxy 192.168.0.143:3005
|
|
}
|
|
|
|
# --- Hermes ---
|
|
hermes.franksdank.cloud {
|
|
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
|
|
}
|
|
|
|
# --- Grafana ---
|
|
grafana.franksdank.cloud {
|
|
basicauth {
|
|
dankuser REPLACE_WITH_HASH
|
|
}
|
|
reverse_proxy grafana:3000
|
|
}
|
|
|
|
# --- Prometheus ---
|
|
prometheus.franksdank.cloud {
|
|
reverse_proxy anubis:8923 {
|
|
header_up X-Real-Ip {remote_host}
|
|
}
|
|
}
|
|
|
|
# --- Loki ---
|
|
loki.franksdank.cloud {
|
|
reverse_proxy loki:3100
|
|
}
|
|
|
|
# --- Pi-hole (quadlet on slirp4netns; published 8083) ---
|
|
pihole.franksdank.cloud {
|
|
basicauth {
|
|
dankuser REPLACE_WITH_HASH
|
|
}
|
|
reverse_proxy 192.168.0.143:8083
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Services on dankserver (192.168.0.235) — published LAN ports
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# --- Plex (existing, kept) ---
|
|
# http:// on purpose: franksdank.casa is not hosted in the same Porkbun
|
|
# account as franksdank.cloud, so the DNS-01 provider cannot issue a
|
|
# Let's Encrypt cert for it. It stays plain-HTTP legacy; the canonical
|
|
# name is plex.franksdank.cloud (HTTPS, below).
|
|
http://plex.franksdank.casa {
|
|
reverse_proxy 192.168.0.235:32400 {
|
|
transport http {
|
|
tls_insecure_skip_verify
|
|
}
|
|
}
|
|
}
|
|
|
|
# PMS requires TLS from every non-loopback client: plain-HTTP off-loopback
|
|
# is read and silently closed (diagnosed 2026-08-26), which surfaced as an
|
|
# empty-body 502 here. Dial the upstream over TLS; PMS's self-signed cert is
|
|
# skipped on this pinned-LAN hop only (client-facing LE TLS is untouched).
|
|
# The web UI lives at /web/; / returns the XML MediaContainer index.
|
|
plex.franksdank.cloud {
|
|
reverse_proxy 192.168.0.235:32400 {
|
|
transport http {
|
|
tls_insecure_skip_verify
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Calibre-Web ---
|
|
books.franksdank.cloud {
|
|
reverse_proxy 192.168.0.235:8083
|
|
}
|
|
|
|
# --- RomM ---
|
|
romm.franksdank.cloud {
|
|
reverse_proxy anubis:8923 {
|
|
header_up X-Real-Ip {remote_host}
|
|
}
|
|
}
|
|
|
|
# --- Copyparty ---
|
|
files.franksdank.cloud {
|
|
reverse_proxy anubis:8923 {
|
|
header_up X-Real-Ip {remote_host}
|
|
}
|
|
}
|
|
|
|
# --- Jellyfin ---
|
|
jellyfin.franksdank.cloud {
|
|
reverse_proxy 192.168.0.235:8096 {
|
|
transport http {
|
|
versions h2c 1.1
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- *arr Stack ---
|
|
radarr.franksdank.cloud {
|
|
reverse_proxy 192.168.0.235:7878
|
|
}
|
|
|
|
sonarr.franksdank.cloud {
|
|
reverse_proxy 192.168.0.235:8989
|
|
}
|
|
|
|
lidarr.franksdank.cloud {
|
|
reverse_proxy 192.168.0.235:8686
|
|
}
|
|
|
|
readarr.franksdank.cloud {
|
|
reverse_proxy 192.168.0.235:8787
|
|
}
|
|
|
|
prowlarr.franksdank.cloud {
|
|
reverse_proxy 192.168.0.235:9696
|
|
}
|
|
|
|
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
|
|
# }
|
|
|
|
# --- Optional: Whisparr ---
|
|
# whisparr.franksdank.cloud {
|
|
# reverse_proxy 192.168.0.235:6969
|
|
# }
|