NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Go modules · #127 by repository stars
Last release 2 days ago
30 Sep 2026
Ships on a steady schedule
a new release about every 9 days
Nearly every release is documented
notes for 27 of 28 stable releases
Nothing withdrawn
no release was ever pulled
2 years old
294 releases · first in 2025
One column per month.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
> Someone has to make an effort at reconciliation if these conflicts are ever going to end.
Someone has to make an effort at reconciliation if these conflicts are ever going to end.
In this release, we finally fix the odd number of CPU cores bug, pave the way for lighter weight challenges, make Anubis more adaptable, and more.
A new "proof of React" has been added. It runs a simple app in React that has several chained hooks. It is much more lightweight than the proof of work check.
segments function was added for splitting a path into its slash-separated segments.static/js/workers/*.mjs.REDIRECT_DOMAINS. You can pass *.bugs.techaro.lol to allow redirecting to anything ending with .bugs.techaro.lol. There is a limit of 4 wildcards.Some phones have an odd number of CPU cores. This caused interesting issues. This was fixed by using Math.trunc to convert the number of CPU cores back into an integer.
opts.Logger to your slog instance of choice (#864).favicon pattern in data/common/keep-internet-working.yaml has been updated to permit requests for png/gif/jpg/svg files as well as ico.--cookie-prefix flag has been fixed so that it is fully respected.data/common/keep-internet-working.yaml have been updated to appropriately escape the '.' character in the regular expression patterns.Anubis operates by issuing a challenge and having the client present a solution for that challenge. Challenges are identified by a unique UUID, which is stored in the database.
The problem is that a challenge could potentially be used twice by a dedicated attacker making a targeted attack against Anubis. Challenge records did not have a "spent" or "used" field. In total, a dedicated attacker could solve a challenge once and reuse that solution across multiple sessions in order to mint additional tokens.
This was fixed by adding a "spent" field to challenges in the data store. When a challenge is solved, that "spent" field gets set to true. If a future attempt to solve this challenge is observed, it gets rejected.
With the advent of store based challenge issuance in #749, this means that these challenge IDs are only good for 30 minutes. Websites using the most recent version of Anubis have limited exposure to this problem.
Websites using older versions of Anubis have a much more increased exposure to this problem and are encouraged to keep this software updated as often and as frequently as possible.
Thanks to @taviso for reporting this issue.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Anubis now supports these new languages:
Anubis now supports these new languages:
Fixes GHSA-jhjj-2g64-px7c.
This could allow an attacker to craft an Anubis pass-challenge URL that forces a redirect to nonstandard URLs, such as the javascript: scheme which executes arbitrary JavaScript code in a browser context when the user clicks the "Try again" button.
This has been fixed by disallowing any URLs without the scheme http or https.
Additionally, the "Try again" button has been fixed to completely ignore the user-supplied redirect location. It now redirects to the home page (/).
Nothing published for this version
Nothing published for this version
This contained an incomplete fix for GHSA-jhjj-2g64-px7c. Do not use this version.
This contained an incomplete fix for GHSA-jhjj-2g64-px7c. Do not use this version.
Nothing published for this version
Nothing published for this version
Expired records are now properly removed from bbolt databases (#848).
Anubis now supports the missingHeader to assert the absence of headers in requests.
Anubis now supports these new languages:
v1.21.0 changed the core challenge flow to maintain information about challenges on the server side instead of only doing them via stateless idempotent generation functions and relying on details to not change. There was a subtle bug introduced in this change: if a client has an unknown challenge ID set in its test cookie, Anubis will clear that cookie and then throw an HTTP 500 error.
This has been fixed by making Anubis throw a new challenge page instead.
Previously the "fast" proof of work solver had a fragment of JavaScript that attempted to only post an update about proof of work progress to the main browser window every 1024 iterations. This fragment of JavaScript was subtly incorrect in a way that passed review but actually made the workers send an update back to the main thread every iteration. This caused a pileup of unhandled async calls (similar to a socket accept() backlog pileup in Unix) that caused stack space exhaustion.
This has been fixed in the following ways:
0 instead of all workers.Hopefully this should limit the event loop thrashing and let ia32 browsers (as well as any environment with a smaller stack size than amd64 and aarch64 seem to have) function normally when processing Anubis proof of work challenges.
In some cases, the parallel solution finder in Anubis could cause all of the worker promises to leak due to the fact the promises were being improperly terminated. This was fixed by having Anubis debounce worker termination instead of allowing it to potentially recurse infinitely.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
We try to introduce breaking changes as much as possible, but these are the changes that may be relevant for you as an administrator:
Please, be at ease. You are among friends here.
In this release, Anubis becomes internationalized, gains the ability to use system load as input to issuing challenges, finally fixes the "invalid response" after "success" bug, and more! Please read these notes before upgrading as the changes are big enough that administrators should take action to ensure that the upgrade goes smoothly.
The biggest change is that the "invalid response" after "success" bug is now finally fixed for good by totally rewriting how Anubis' challenge issuance flow works. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active storage backend for up to 30 minutes. This also fixes #746 and other similar instances of this issue.
In order to reduce confusion, the "Success" interstitial that shows up when you pass a proof of work challenge has been removed.
Anubis now is able to store things persistently in memory, on the disk, or in Valkey (this includes other compatible software). By default Anubis uses the in-memory backend. If you have an environment with mutable storage (even if it is temporary), be sure to configure the bbolt storage backend.
Anubis now supports localized responses. Locales can be added in lib/localization/locales/. This release includes support for the following languages:
If facts or local regulations demand, you can set Anubis default language with the FORCED_LANGUAGE environment variable or the --forced-language command line argument:
FORCED_LANGUAGE=de
Anubis can dynamically take action based on the system load average, allowing you to write rules like this:
## System load based checks.
# If the system is under high load for the last minute, add weight.
- name: high-load-average
action: WEIGH
expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
weight:
adjust: 20
# If it is not for the last 15 minutes, remove weight.
- name: low-load-average
action: WEIGH
expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
weight:
adjust: -10
Something to keep in mind about system load average is that it is not aware of the number of cores the system has. If you have a 16 core system that has 16 processes running but none of them is hogging the CPU, then you will get a load average below 16. If you are in doubt, make your "high load" metric at least two times the number of CPU cores and your "low load" metric at least half of the number of CPU cores. For example:
| Kind | Core count | Load threshold |
|---|---|---|
| high load | 4 | 8.0 |
| low load | 4 | 2.0 |
| high load | 16 | 32.0 |
| low load | 16 | 8 |
Also keep in mind that this does not account for other kinds of latency like I/O latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero.
There are a bunch of other assorted features and fixes too:
COOKIE_SECURE option to set the cookie Secure flagBIND_NETWORK/--bind-network value from the bind address (#677)./healthz route for use in platform-based health checks.We try to introduce breaking changes as much as possible, but these are the changes that may be relevant for you as an administrator:
Previously Anubis did no accounting for challenges that it issued. This means that if Anubis restarted during a client, the client would be able to proceed once Anubis came back online.
During the upgrade to v1.21.0 and when v1.21.0 (or later) restarts with the in-memory storage backend, you may see a higher rate of failed challenges than normal. If this persists beyond a few minutes, open an issue.
If you are using the in-memory storage backend, please consider using a different storage backend.
The following potentially breaking change applies to native installs with systemd only:
Each instance of systemd service template now has a unique RuntimeDirectory, as opposed to each instance of the service sharing a RuntimeDirectory. This change was made to avoid the RuntimeDirectory getting nuked any time one of the Anubis instances restarts.
If you configured Anubis' unix sockets to listen on /run/anubis/foo.sock for instance anubis@foo, you will need to configure Anubis to listen on /run/anubis/foo/foo.sock and additionally configure your HTTP load balancer as appropriate.
If you need the legacy behaviour, install this systemd unit dropin:
# /etc/systemd/system/anubis@.service.d/50-runtimedir.conf
[Service]
RuntimeDirectory=anubis
Just keep in mind that this will cause problems when Anubis restarts.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
The big ticket items are as follows:
The big ticket items are as follows:
metarefresh (#95)robots2policy CLI utility to convert robots.txt files to Anubis challenge policies using CEL expressions (#409)A lot of performance improvements have been made:
And some cleanups/refactors were added:
/test-error endpoint and update the testing endpoint /make-challenge to only be enabled in
development--xff-strip-private flag/envvar to toggle skipping X-Forwarded-For private addresses or not--strip-base-prefix flag/envvar to strip the base prefix from request paths when forwarding to target servers--cookie-dynamic-domain/COOKIE_DYNAMIC_DOMAIN flag/envvarRequest weight is one of the biggest ticket features in Anubis. This enables Anubis to be much closer to a Web Application Firewall and when combined with custom thresholds allows administrators to have Anubis take advanced reactions. For more information about request weight, see the request weight section of the policy file documentation.
TL;DR when you have one or more WEIGHT rules like this:
bots:
- name: gitea-session-token
action: WEIGH
expression:
all:
- '"Cookie" in headers'
- headers["Cookie"].contains("i_love_gitea=")
# Remove 5 weight points
weight:
adjust: -5
You can configure custom thresholds like this:
thresholds:
- name: minimal-suspicion # This client is likely fine, its soul is lighter than a feather
expression: weight < 0 # a feather weighs zero units
action: ALLOW # Allow the traffic through
# For clients that had some weight reduced through custom rules, give them a
# lightweight challenge.
- name: mild-suspicion
expression:
all:
- weight >= 0
- weight < 10
action: CHALLENGE
challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
algorithm: metarefresh
difficulty: 1
report_as: 1
# For clients that are browser-like but have either gained points from custom
# rules or report as a standard browser.
- name: moderate-suspicion
expression:
all:
- weight >= 10
- weight < 20
action: CHALLENGE
challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 2 # two leading zeros, very fast for most clients
report_as: 2
# For clients that are browser like and have gained many points from custom
# rules
- name: extreme-suspicion
expression: weight >= 20
action: CHALLENGE
challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 4
report_as: 4
These thresholds apply when no other ALLOW, DENY, or CHALLENGE rule matches the request. WEIGHT rules add and remove request weight as needed:
bots:
- name: gitea-session-token
action: WEIGH
expression:
all:
- '"Cookie" in headers'
- headers["Cookie"].contains("i_love_gitea=")
# Remove 5 weight points
weight:
adjust: -5
- name: bot-like-user-agent
action: WEIGH
expression: '"Bot" in userAgent'
# Add 5 weight points
weight:
adjust: 5
Of note: the default "generic browser" rule assigns 10 weight points:
# Generic catchall rule
- name: generic-browser
user_agent_regex: >-
Mozilla|Opera
action: WEIGH
weight:
adjust: 10
Adjust this as you see fit.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Return data/bots/ai-robots-txt.yaml to avoid breaking configs #599
data/bots/ai-robots-txt.yaml to avoid breaking configs #599Mostly a bunch of small features, no big ticket things this time.
Mostly a bunch of small features, no big ticket things this time.
GOARCH=ppc64le)check-spelling for spell checking--target-insecure-skip-verify flag/envvar to allow Anubis to hit a self-signed HTTPS backendX-Firefox-Ai header is set--target-host flag/envvar to allow changing the value of the Host header in requests forwarded to the target serviceRuntimeDirectory to systemd unit settings so native packages can listen over unix sockets--target-sni flag/envvar to allow changing the value of the TLS handshake hostname in requests forwarded to the target service--version flaganubis_proxied_requests_total metric to count proxied requestsApplebot as "good" web crawlerNothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
The big ticket feature in this release is CEL expression matching support. This allows you to tailor your approach for the individual services you are
The big ticket feature in this release is CEL expression matching support. This allows you to tailor your approach for the individual services you are protecting.
These can be as simple as:
- name: allow-api-requests
action: ALLOW
expression:
all:
- '"Accept" in headers'
- 'headers["Accept"] == "application/json"'
- 'path.startsWith("/api/")'
Or as complicated as:
- name: allow-git-clients
action: ALLOW
expression:
all:
- >-
(
userAgent.startsWith("git/") ||
userAgent.contains("libgit") ||
userAgent.startsWith("go-git") ||
userAgent.startsWith("JGit/") ||
userAgent.startsWith("JGit-")
)
- '"Git-Protocol" in headers'
- headers["Git-Protocol"] == "version=2"
The docs have more information, but here's a tl;dr of the variables you have access to in expressions:
| Name | Type | Explanation | Example |
|---|---|---|---|
headers |
map[string, string] |
The headers of the request being processed. | {"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"} |
host |
string |
The HTTP hostname the request is targeted to. | anubis.techaro.lol |
method |
string |
The HTTP method in the request being processed. | GET, POST, DELETE, etc. |
path |
string |
The path of the request being processed. | /, /api/memes/create |
query |
map[string, string] |
The query parameters of the request being processed. | ?foo=bar -> {"foo": "bar"} |
remoteAddress |
string |
The IP address of the client. | 1.1.1.1 |
userAgent |
string |
The User-Agent string in the request being processed. |
Mozilla/5.0 Gecko/20100101 Firefox/137.0 |
This will be made more elaborate in the future. Give me time. This is a simple, lovable, and complete implementation of this feature so that administrators can get hacking ASAP.
Other changes:
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →