From 00b7eb75d441960053a0fca1ee85ddca5e7b0f3f Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 1 Sep 2026 17:13:51 -0700 Subject: [PATCH] =?UTF-8?q?docs:=20Anubis=20v1.27.0=20=E2=80=94=20domain-w?= =?UTF-8?q?ide=20cookie,=20persistent=20key,=20new=20gotchas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Facts table: image pin, COOKIE_DOMAIN, persistent ED25519 key via host env file; restarts no longer invalidate solved challenges. - Gotcha 8: X-Real-Ip mandatory at v1.27+ (500 without; chain sets it). - Gotcha 9: DENY renders error-styled page at HTTP 200 — probe bodies, not statuses. - Verify live: probe www (apex 308s), version-marker grep, body-based deny check. All commands executed live before commit. --- .../Infrastructure/Anubis.md | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/serverAppConfigs-docs/Infrastructure/Anubis.md b/serverAppConfigs-docs/Infrastructure/Anubis.md index 1ebd74f..c20a867 100644 --- a/serverAppConfigs-docs/Infrastructure/Anubis.md +++ b/serverAppConfigs-docs/Infrastructure/Anubis.md @@ -5,7 +5,11 @@ Dankclaw. Browsers get a proof-of-work challenge; AI scraper user-agents are denied outright. It sits in a two-tier chain: client → Caddy TLS listener → Anubis (`:8923`, internal-only) → Caddy internal listener (`:8016`) → app. Host headers survive both hops, so the Authentik embedded outpost can route -on the original `Host` behind it. See [[Caddy]] and [[Authentik]]. +on the original `Host` behind it. Since v1.27.0 the challenge cookie is +domain-wide (`COOKIE_DOMAIN=franksdank.cloud`): a browser solves the +proof-of-work once on any guarded vhost and every guarded +`*.franksdank.cloud` vhost honors it until the 168h expiry or an authentik +re-login. See [[Caddy]] and [[Authentik]]. ```mermaid graph LR @@ -21,11 +25,11 @@ graph LR | Item | Value | |------|-------| | Runtime | Quadlet unit `anubis.service` (`dankclaw/anubis/quadlet/anubis.container`) — boot-persistent, self-healing | -| Image | `ghcr.io/techarohq/anubis:v1.13.0`, run under a `localhost/` alias tag (see gotcha 1) | +| Image | `ghcr.io/techarohq/anubis:v1.27.0`, run under a `localhost/` alias tag (see gotcha 1) | | Ports | `:8923` internal (no published port); metrics on `:9090` reachable only inside `dankclaw_default` | -| Config surface | flags via flagenv — `TARGET=http://caddy:8016` sets `--target`, `POLICY_FNAME=/policy/policy.json` sets `--policy-fname` | +| Config surface | flags via flagenv — `TARGET=http://caddy:8016` sets `--target`, `POLICY_FNAME=/policy/policy.json` sets `--policy-fname`, `COOKIE_DOMAIN=franksdank.cloud` sets `--cookie-domain` | | Policy | `dankclaw/anubis/policy.json` mounted read-only into the container; parsed at process start only (no reload signal exists — every policy change needs a unit restart) | -| Store | memory (no signing keys needed); a restart invalidates issued challenge JWTs — users simply re-challenge once | +| Store | memory JWTs signed by a **persistent** ED25519 key (`ED25519_PRIVATE_KEY_HEX`, host file `/home/dank/Containers/anubis/env`, mode 0600, gitea-style `${VAR}` expansion) — restarts keep every solved challenge valid; with no key set Anubis mints an ephemeral one per boot | Policy content: an `ALLOW` rule for Anubis's own endpoints (the `/.within.website/x/cmd/anubis/*` API and static namespace — the challenge @@ -41,9 +45,11 @@ Valid action verbs at this tag are `ALLOW`, `CHALLENGE`, `DENY` — `ACCEPT` is not one of them (first deploy of that rule crash-looped the unit on exactly that typo; verified against the image binary's string table). The policy schema at this -tag is JSON-only — no `(data)/` imports and no CEL; first match wins. -Upgrading the pin = stop the unit, pull + re-tag the `localhost/` alias, -restart; re-verify the policy schema against the new tag first. +tag is JSON-only — no `(data)/` imports and no CEL; first match wins (JSON +still loads at v1.27.0 — scratch-verified 2026-09-02 before the pin bump). +Upgrading the pin = pull + re-tag the `localhost/` alias, restart; verify +the flag surface with `podman run --rm ... --help` and the policy against +the new tag first (done for v1.27.0). ## Exempt by construction @@ -52,7 +58,7 @@ These never route through Anubis (no policy bypass rule needed): plex ungated), and gitea (git smart-HTTP clients cannot solve JS challenges — `/info/refs` and `git-*-pack` paths pass unchallenged). -## Gotchas (all verified live; 1–6 on 2026-08-28, 7 on 2026-08-30) +## Gotchas (all verified live; 1–6 on 2026-08-28, 7 on 2026-08-30, 8–9 on 2026-09-02) 1. **Image ref casing.** Podman's reference parser rejects mixed-case repository refs before any network I/O (`repository name must be @@ -101,12 +107,32 @@ ungated), and gitea (git smart-HTTP clients cannot solve JS challenges — --service anubis` — and the deployer must therefore always restart it rather than trust the file contents on disk. +8. **X-Real-Ip is mandatory at v1.27+ (HTTP 500 without it).** Anubis now + answers any request lacking `X-Real-Ip` with a 500 whose log line reads + `[misconfiguration] X-Real-Ip header is not set`. The real chain always + sets it (every guarded Caddy listener carries `header_up X-Real-Ip + {remote_host}` — the bats seam enforces the line), so users never see + this. But any direct probe of `:8923` (scratch container, curl, a new + reverse-proxy hop) MUST set the header or it looks like a policy failure. + Scratch-verify with `-H "X-Real-Ip: 192.168.0.20"`. +9. **DENY renders as an error-styled page at HTTP 200.** At v1.27 the denied + client gets the "Oh noes!" template containing `Access Denied` and + `error code ` — with status **200**, not 403 (the boot log maps + rule names to error ids: `rule-error-ids`). Status-only probes cannot + distinguish challenge from deny at this release; grep the body for + `Access Denied` / `Making sure you` instead. + ## Verify live ``` ssh dankclaw 'systemctl --user is-active anubis.service' -curl -s -o /dev/null -w "%{http_code}\n" -A "GPTBot" https://franksdank.cloud/ # denied page -curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0" https://franksdank.cloud/ # challenge 200 +podman inspect anubis --format "{{.ImageName}} {{.State.Health.Status}}" +# the apex 308s to www; the guarded vhost is www — probe www, not the apex +curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" -A "Mozilla/5.0" https://franksdank.cloud/ +# challenge: body carries the version marker + PoW page +curl -s -A "Mozilla/5.0" https://www.franksdank.cloud/ | grep -oE "cachebuster=v[0-9.]+|Making sure you.re not a bot" +# deny: HTTP 200 at v1.27+ — grep the body, not the status (gotcha 9) +curl -s -A "GPTBot" https://www.franksdank.cloud/ | grep -q "Access Denied" && echo denied || echo BROKEN-challenge-served +# outpost transport exempt (ALLOW rule) curl -s https://search.franksdank.cloud/outpost.goauthentik.io/login | grep -q Making\ sure && echo BROKEN-still-challenged || echo outpost-transport-exempt - ```