# 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.13.0 was # source-verified before pinning (policy parser is JSON-only at this tag, # flags are the config surface via flagenv: TARGET sets --target, # POLICY_FNAME sets --policy-fname). Upgrading = pull new tag, re-tag the # localhost alias, restart the unit; re-verify the policy schema first. # (the active Image= key lives in the [Container] section below) # # No secrets here: Anubis holds no credentials (memory store; challenge # cookies are client-side JWTs). A restart invalidates issued challenges — # users just re-challenge once. 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.13.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 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] Restart=always RestartSec=5 [Install] WantedBy=default.target