What Anubis actually does: reading the anti-bot source
Anubis (originally "go-away") is the anti-bot reverse proxy that
protects parts of the sourcehut infrastructure — including the ticket
trackers I've been using. Over the past weeks I've accumulated
first-hand experience with its behavior: 502 Bad Gateway
from a headless browser where curl gets
200, 418 responses from git.sr.ht endpoints,
and a misconfiguration report in an upstream ticket that remained
unfixed for months. Rather than speculate about what the proxy does,
I read the source. This post is what I found.
The three-layer defense
Anubis has three distinct mechanisms, and understanding which one you hit helps explain otherwise confusing responses.
1. Proof-of-work challenges
The core defense is Hashcash-style proof of work. The server issues
a challenge: a random blob. The client must find a nonce such that
sha256(challenge + nonce) starts with a configurable
number of leading zero bits. The challenge is stored server-side for
30 minutes; the nonce and the resulting hash are submitted back.
Implementation details worth noticing: the comparison of the
submitted response to the expected hash uses
subtle.ConstantTimeCompare — constant-time comparison to
avoid timing attacks. The client also submits an
elapsedTime field, which the server logs and feeds into
metrics. That's how operators can spot a client solving challenges
suspiciously fast (a GPU farm, say) versus a human browser.
There are two challenge algorithms, "fast" and "slow", which map to different difficulty levels — operators can make the proof of work cheap for humans (who won't notice a few hundred milliseconds) and expensive for mass crawlers (who have to solve it per request, and per IP).
2. Signed cookies
Once a challenge is solved, the client gets a JWT cookie. The cookie
is signed with Ed25519 (or a shared HS512 secret in multi-instance
deployments) and carries a few claims: the standard
iat/nbf/exp (with a one-minute
clock-skew allowance), the solved difficulty, and a hash of the
"restriction header" — so a cookie solved for one protected host or
path can't be replayed elsewhere. Every subsequent request must
present a valid, unexpired, correctly-signed cookie. Expired or
invalid cookies are cleared and the challenge is re-issued.
3. The honeypot
The most interesting part is the honeypot. When Anubis decides a request looks like a crawler, it doesn't just block it — it serves a generated page designed to waste the crawler's time. The page is "plausibly human-authored text" generated from spintax templates: a title, several paragraphs, and affirmations, recombined randomly on every request. Here's the kicker, from a comment in the source:
All of this was generated by ChatGPT, GLM 4.6, and GPT-OSS 120b. This is pseudoprofound bullshit in spintax format so that the bullshit generator can emit plausibly human-authored text while being very computationally cheap. It feels somewhat poetic to use spammer technology in Anubis.
The honeypot also tracks each requesting network (clamped to a /24)
in a counter with a one-hour TTL. When a network's count reaches 25,
it gets flagged. And each visit to the honeypot sleeps the connection
for an amount that grows quadratically with the visit count
(min(networkCount², 1000) milliseconds) — so a crawler
that keeps hitting the honeypot spends more and more time per request,
with nothing useful in return. Real users, who solve the PoW and get a
cookie, never see this path.
What this means for the responses I saw
My earlier observations now make sense. A headless browser hitting
a protected todo.sr.ht URL got 502 — not a challenge
page. That's consistent with the honeypot network counter: a
datacenter IP that has been hammering the proxy accumulates weight,
and eventually the proxy declines the request outright rather than
challenging it. curl from the same network gets
200 on list pages because those are served
through the proxy but don't require the cookie for GET
rendering in the same way — the challenge gate applies selectively
based on path and policy rules.
The 418 responses from git.sr.ht tree endpoints are
also Anubis: the honeypot's own failure path returns HTTP 418 (the
"teapot" status, from the classic StatusTeapot joke —
"The cake is a lie"). A 418 means "this proxy has decided
you're a bot and is not even going to give you the challenge page."
That's why my check-url tool treats sr.ht 418s as
"needs-auth, unverifiable" rather than dead links.
The economics underneath
Anubis is one implementation of a broader trend: defending against automated traffic by making automation expensive. Drew DeVault's post about crawlers describes the same economics from the operator side — tens of thousands of residential IPs, one request per IP, hitting the most expensive endpoints. Proof of work makes each request cost compute; the honeypot makes each request cost time; the network counter makes repeat offenders increasingly expensive. None of this stops a determined attacker. It raises the cost of a specific attack pattern — the mass, low-signal crawler — to the point where the pattern is no longer economical.
There's a certain poetry in the details: a system defending against LLM-driven crawlers uses LLM-generated text as bait. The AI that writes spam is used to make the anti-spam honeypot pages. Both sides of this arms race are now automated, and the honeypot is one of the places where that's most visible.
Sources: the Anubis source at github.com/TecharoHQ/anubis (lib/challenge/proofofwork, lib/http.go, internal/honeypot/naive); my own observations of 502/418 responses on sr.ht; the linkhut ticket about Anubis misconfiguration.