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.
This commit is contained in:
1 parent
799441a8be
commit
21fa38e6a8
4 files changed
+86
-2
No files matched your search
@@ -555,7 +555,7 @@ deploy_anubis() {
|
|||||||
# The image lives on ghcr.io; pull once, then the unit runs it from
|
# The image lives on ghcr.io; pull once, then the unit runs it from
|
||||||
# local storage (Pull=never keeps boots off the registry).
|
# local storage (Pull=never keeps boots off the registry).
|
||||||
if ! podman image exists ghcr.io/techarohq/anubis:v1.13.0; then
|
if ! podman image exists ghcr.io/techarohq/anubis:v1.13.0; then
|
||||||
podman pull ghcr.io/techaroq/anubis:v1.13.0
|
podman pull ghcr.io/techarohq/anubis:v1.13.0
|
||||||
fi
|
fi
|
||||||
# The unit runs the localhost/ alias (see the unit file comment): this
|
# The unit runs the localhost/ alias (see the unit file comment): this
|
||||||
# host's podman quadlet path cannot resolve registry-prefixed refs from
|
# host's podman quadlet path cannot resolve registry-prefixed refs from
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# 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]].
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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 tag`s 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
|
||||||
|
```
|
||||||
@@ -71,6 +71,7 @@ covers both workloads).
|
|||||||
- [[LAN-DNS]] — name resolution, Pi-hole, router DNS hand-off
|
- [[LAN-DNS]] — name resolution, Pi-hole, router DNS hand-off
|
||||||
- [[Pi-hole]] — the LAN DNS server (quadlet, records, web UI)
|
- [[Pi-hole]] — the LAN DNS server (quadlet, records, web UI)
|
||||||
- [[Caddy]] — reverse proxy and TLS
|
- [[Caddy]] — reverse proxy and TLS
|
||||||
|
- [[Anubis]] — bot challenge proxy fronting the guarded vhosts (chain, policy, gotchas)
|
||||||
- [[Authentik]] — the homelab's SSO / OIDC provider (forward_auth)
|
- [[Authentik]] — the homelab's SSO / OIDC provider (forward_auth)
|
||||||
- [[authentik-postgres]] — Authentik database service
|
- [[authentik-postgres]] — Authentik database service
|
||||||
- [[authentik-server]] — Authentik server + Caddy outpost service
|
- [[authentik-server]] — Authentik server + Caddy outpost service
|
||||||
|
|||||||
+7
-1
@@ -23,8 +23,14 @@ setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@test "the image is pinned to an exact upstream tag and never pulled at boot" {
|
@test "the image is pinned to an exact upstream tag and never pulled at boot" {
|
||||||
grep -qE '^Image=ghcr\.io/techaroq/anubis:v[0-9]+\.[0-9]+\.[0-9]+$' "$UNIT"
|
# The unit runs a localhost/ alias tag of the canonical upstream ref
|
||||||
|
# (podman 5.7 quadlet quirk: registry-prefixed refs fail container
|
||||||
|
# create on this host). The pin is still an exact upstream tag — the
|
||||||
|
# deploy handler pulls the canonical ghcr.io ref and re-tags the alias;
|
||||||
|
# nothing is pulled at boot (Pull=never).
|
||||||
|
grep -qE '^Image=localhost/techarohq/anubis:v[0-9]+\.[0-9]+\.[0-9]+$' "$UNIT"
|
||||||
grep -q '^Pull=never$' "$UNIT"
|
grep -q '^Pull=never$' "$UNIT"
|
||||||
|
grep -q 'podman pull ghcr\.io/techarohq/anubis:v[0-9]*\.[0-9]*\.[0-9]*' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "anubis publishes no host port — it is internal-network-only" {
|
@test "anubis publishes no host port — it is internal-network-only" {
|
||||||
|
|||||||
Reference in new issue
Block a user