# Quadlet unit: Anubis bot-challenge proxy (rootless, boot-persistent). # # Install path on dankclaw: ~/.config/containers/systemd/anubis.container # Unit name: anubis.service (container name: anubis) # # Two-tier chain: the public Caddy listeners for the guarded vhosts proxy # everything to this container (port 8923, internal only — no published port, # so metrics on :9090 stay reachable only inside dankclaw_default); Anubis # verifies the client (challenge/DENY per policy) and forwards accepted # traffic back to Caddy's internal listener (TARGET below), where the normal # per-vhost routing — including Authentik forward_auth — resumes. Host headers # survive both hops, so vhost matching on the internal listener still works. # # Image pin: the image is pulled from ghcr.io/techarohq/anubis (source of # truth), but the unit runs it under a localhost/-aliased tag: on this host's # podman 5.7 quadlet path, registry-prefixed refs fail to resolve from local # storage at container create time ("image not known") even when present; the # localhost alias resolves (verified live 2026-08-28). v1.27.0 pinned 2026-09-02: --help # verified on the image itself (cookie-domain, ed25519-private-key-hex, # cookie-expiration-time default 168h, difficulty default 4); JSON policy # schema unchanged; X-Real-Ip is now REQUIRED (missing header => HTTP 500) — # the Caddy chain always sets it (bats seam enforces the header_up line). # Upgrading = pull new tag, re-tag the localhost alias, restart the unit. # # No secrets in this repo. COOKIE_DOMAIN makes the challenge cookie valid for # all *.franksdank.cloud vhosts: solve the proof-of-work once (e.g. on www) # and every guarded vhost honors it until expiry (default 168h, aligned with # authentik's 7-day session) or re-login. ED25519_PRIVATE_KEY_HEX pins the # JWT signing key (host file /home/dank/Containers/anubis/env, mode 0600, # gitea-style ${VAR} expansion via the [Service] EnvironmentFile): without # it every restart invalidates everyone's solved challenges. See # serverAppConfigs-docs/Infrastructure/Anubis.md. [Unit] Description=Anubis bot-challenge proxy container After=network-online.target Wants=network-online.target [Container] ContainerName=anubis Image=localhost/techarohq/anubis:v1.27.0 Pull=never Network=dankclaw_default # flagenv maps env names to flag names (TARGET -> --target, POLICY_FNAME -> # --policy-fname). TARGET points back at Caddy's internal listener; the # container DNS name resolves on dankclaw_default. Environment=TARGET=http://caddy:8016 Environment=POLICY_FNAME=/policy/policy.json Environment=COOKIE_DOMAIN=franksdank.cloud EnvironmentFile=/home/dank/Containers/anubis/env Environment=ED25519_PRIVATE_KEY_HEX=${ED25519_PRIVATE_KEY_HEX} Volume=/home/dank/serverAppConfigs/dankclaw/anubis/policy.json:/policy/policy.json:ro # The image ships no HEALTHCHECK (distroless ko-build: entrypoint is # /ko-app/anubis, no shell in the image), so the probe is declared here. # JSON exec form on purpose: this quadlet generator joins HealthCmd into a # single \x20-escaped argv element and podman then runs it as shell-form — # which needs a shell the distroless image does not have. A JSON array is # passed through verbatim and executes /ko-app/anubis directly. HealthCmd=["/ko-app/anubis","--healthcheck"] HealthInterval=15s HealthTimeout=5s HealthRetries=3 [Service] EnvironmentFile=/home/dank/Containers/anubis/env Restart=always RestartSec=5 [Install] WantedBy=default.target