Files
serverAppConfigs/serverAppConfigs-docs/Infrastructure/Anubis.md
T
Frank 21fa38e6a8 anubis: fix image ref typo in deploy pull, health probe exec form, unit alias tag
Three compounding deploy-handler bugs found by live deployment:
- podman image exists/pull used a mixed-case ghcr.io ref that podman's
  reference parser rejects before any network I/O (repository name must be
  lowercase); all refs now lowercase — ghcr resolves case-insensitively.
- the unit runs the image under a localhost/ alias tag: this host's podman
  5.7 quadlet path fails container create with 'image not known' for
  registry-prefixed refs even when present in storage (verified live).
- the health probe was shell-form and the distroless ko-build image has no
  shell; HealthCmd is now a JSON exec-form array, which passes through the
  quadlet generator's argv assembly verbatim.

Tests updated to assert the alias-tag reality plus the pull guard.
2026-08-28 11:18:41 -07:00

4.2 KiB

Anubis — bot challenge proxy

Anubis is the bot-challenge proxy that fronts the guarded public vhosts on 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 per-vhost routing (and Authentik forward_auth for the wrapped apps) resumes unchanged behind it. See Caddy and Authentik.

graph LR
    C[Client] --> T[Caddy TLS listener]
    T --> A[Anubis verify :8923]
    A -->|accepted| I[Caddy internal listener :8016]
    A -->|challenge or deny| C
    I --> P[per-vhost routing and forward_auth]

Facts (live inventory)

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)
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
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.

Exempt by construction

These never route through Anubis (no policy bypass rule needed): plex (plain-HTTP legacy), auth.franksdank.cloud (the IdP login itself must stay ungated), and gitea (git smart-HTTP clients cannot solve JS challenges — /info/refs and git-*-pack paths pass unchallenged).

Gotchas (all verified live 2026-08-28)

  1. Image ref casing. Podman's reference parser rejects mixed-case repository refs before any network I/O (repository name must be lowercase). All refs use the lowercase ghcr.io/techarohq/anubis form — ghcr resolves case-insensitively at pull time.
  2. Quadlet + registry-prefixed refs. On this host's podman 5.7 quadlet path, a container created from a registry-prefixed image ref fails with image not known even when the image is present in storage; the identical argv works from a shell or a transient unit. The unit therefore runs the image under a localhost/techarohq/anubis:v1.13.0 alias tag — deploy pulls the canonical ref, then podman tags the alias (idempotent).
  3. Health probe exec form. The image is a distroless ko-build (no shell; entrypoint /ko-app/anubis). This quadlet generator joins HealthCmd into one \x20-escaped argv element and podman then runs it shell-form — exec of a missing shell exits 1 and the container sits unhealthy. A JSON exec-form array (HealthCmd=["/ko-app/anubis","--healthcheck"]) passes through the generator verbatim and executes the binary directly.
  4. Rollback nuance. systemctl --user stop anubis.service makes every guarded vhost return 502 (the chain is reverse-proxy-THROUGH Anubis, not forward_auth — Caddy's public listeners proxy to :8923). A true rollback is a Caddyfile revert + redeploy of the caddy service.
  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.

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