From e006ad1f0fd5fdbadada2074f9b5a0e800602d36 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 29 Aug 2026 01:57:39 -0700 Subject: [PATCH] docs(anubis): record the challenge-flow contract and the JS-canary gotcha The policy must ALLOW Anubis's own endpoints before the challenge-all rule or the challenge challenges itself: browsers POST-solve via make-challenge then GET-navigate pass-challenge, and a challenged answer renders as the browser's own fetch-failure error page. Non-JS clients never reach the verify step, which is why every curl probe looked green while every human hit a wall. Also records that v1.13.0's valid action verbs are ALLOW/CHALLENGE/DENY (ACCEPT crash-looped the unit on first deploy). --- .../Infrastructure/Anubis.md | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/serverAppConfigs-docs/Infrastructure/Anubis.md b/serverAppConfigs-docs/Infrastructure/Anubis.md index 06cdb3e..ad13f03 100644 --- a/serverAppConfigs-docs/Infrastructure/Anubis.md +++ b/serverAppConfigs-docs/Infrastructure/Anubis.md @@ -28,12 +28,19 @@ graph LR | 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 | -Policy content: DENY for AI-scaper user-agent regexes (GPTBot, Claude-*, -Perplexity*, CCBot, Bytespider, Amazonbot, meta-externalagent, and friends), -CHALLENGE for everything else (`path_regex: "."` catch-all), DNSBL off. -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. +Policy content: an `ALLOW` rule for Anubis's own endpoints (the +`/.within.website/x/cmd/anubis/*` API and static namespace — the challenge +JS calls `make-challenge`/`pass-challenge` under that prefix, and challenging +those endpoints makes the challenge challenge itself), then DENY for +AI-scraper user-agent regexes (GPTBot, Claude-*, Perplexity*, CCBot, +Bytespider, Amazonbot, meta-externalagent, and friends), then CHALLENGE for +everything else (`path_regex: "."` catch-all). DNSBL off. 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. ## Exempt by construction @@ -67,6 +74,19 @@ ungated), and gitea (git smart-HTTP clients cannot solve JS challenges — 5. **Deploy handler pull guard.** `deploy_anubis` guards the pull with `podman image exists`; a cold anonymous ghcr token request can 403 on the first fetch — retry once, the auth cache makes the second attempt local. +6. **JS-capable clients are the canary.** Non-JS clients (curl, bots, + scrapers) never solve challenges — they just receive the challenge page, + so every curl probe looks green while the verification step is broken. + The first JS-capable client to traverse a freshly deployed chain is the + first real test of it: browsers POST-solve via `make-challenge` and + GET-navigate `pass-challenge?response=&nonce=N + &elapsedTime=T&redir=`; if any own-endpoint path is challenged + instead of allowed, the browser's fetch throws and it renders its own + error page (iOS/macOS browsers phrase it as "the request failed and the + interceptors did not return an alternative response") instead of the + login flow. Verify with a JS-capable client end-to-end — mint via + `make-challenge`, solve, submit with browser-exact query params — before + declaring a challenge chain healthy. ## Verify live