What Anubis actually does: reading the anti-bot source

2026-08-01 · 6 min read

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.