Implement Anubis to give the bots a harder time

On the morning of 1 May 2025, I noticed the FreshPorts production website was under heavy load. For the most part, the database server was running at 100%. The usual load is 7%.

CPU usage for the past 7 days, all about 7% until today at 0300 when it hit 95-100%
CPU usage for the past 7 days, all about 7% until today at 0300 when it hit 95-100%

Even if you look back 4 weeks, this was highly unusual.

CPU usage for the past 4 weeks, all about 7% until today.  Sure, some spikes.
CPU usage for the past 4 weeks, all about 7% until today. Sure, some spikes.

I found it was one IP address. I blocked it. The load dropped.

It had been going for hours. Time to try Anubis.

In this post:

POW (Proof of Work)

Anubis is a proof-of-work middle-ware solution. It takes the incoming traffic, does some magic, then either passes the request along to your webserver or it doesn’t. More on this later. My goal with this post is to document what I’ve done (for myself) and to provide a simple explanation of how to implement it (for you).

The explanation

This is how I explain adding in Anubis to others. There is a more detailed example, with Nginx configuration below.

This explanation assumes you have a working website configuration, including TLS. Let’s refer to that as TheOriginal.

Copy and past that configuration into a new ‘server’, listening on a different port number. I have chosen 127.0.0.1:8080. Let’s refer to this as TheCopy.

In TheCopy, remove the TLS – you won’t need it. Or leave it in. My example assumes it is gone.

In TheOriginal, instead of serving up content, you direct incoming queries to Anubis.

You configure Anubis so it passes the queries along to TheCopy you made above.

Done.

More or less. The details are yet to come, but an example I’ll so you below will help.

Install

The install, as always, is simple:

[11:49 test-nginx01 dvl ~] % sudo pkg install go-anubis
Updating local repository catalogue...
local repository is up to date.
All repositories are up to date.
The following 1 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
	go-anubis: 1.15.1

Number of packages to be installed: 1

The process will require 10 MiB more space.
4 MiB to be downloaded.

Proceed with this action? [y/N]: y
[test-nginx01.int.unixathome.org] [1/1] Fetching go-anubis-1.15.1.pkg: 100%    4 MiB   3.9MB/s    00:01    
Checking integrity... done (0 conflicting)
[test-nginx01.int.unixathome.org] [1/1] Installing go-anubis-1.15.1...
[test-nginx01.int.unixathome.org] [1/1] Extracting go-anubis-1.15.1: 100%
=====
Message from go-anubis-1.15.1:

--
Anubis has been installed! Typically anubis is run behind a proxy, such as
nginx, caddy, haproxy, or similar, that passes the `x-real-ip` header to
anubis, which runs by default on port 8923. You will need to supply the
target url that you are protecting.

Amend rc.conf as required. For example:

anubis_enable=YES
anubis_args="-target http://localhost:4000/myapp -bind :8923"

For more information, see https://anubis.techaro.lol/

Configuration

My choices for configuration are:

[11:49 test-nginx01 dvl ~] % sudo sysrc anubis_enable=YES anubis_args="-target http://localhost:4000/myapp -bind :8923"
anubis_enable="YES"
anubis_args="-target http://127.0.0.1:8080 -bind :8923 -difficulty 2 -cookie-domain=freshports.org -policy-fname=/usr/local/etc/anubis-freshports.json"
[11:50 test-nginx01 dvl ~] % 

When I started Anubis with sudo service anubis start, I saw the following in /var/log/messages:

May  1 12:13:29 test-nginx01 anubis[62887]: Rule error IDs:
May  1 12:13:29 test-nginx01 anubis[62887]: 
May  1 12:13:29 test-nginx01 anubis[62887]: {"time":"2025-05-01T12:13:29.021133213Z","level":"INFO","source":{"function":"main.main","file":"github.com/TecharoHQ/anubis/cmd/anubis/main.go","line":222},"msg":"listening","url":"http://localhost:8923","difficulty":4,"serveRobotsTXT":false,"target":"http://127.0.0.1:8080","version":"v1.15.1","debug-x-real-ip-default":""}

There it is, waiting.

[12:13 test-nginx01 dvl /usr/local/etc/rc.d] % sockstat -4 -p 8923                           
USER     COMMAND    PID   FD  PROTO  LOCAL ADDRESS         FOREIGN ADDRESS      
www      anubis     62888 3   tcp4   10.55.0.42:8923       *:*

But wait, you told it localhost – this is a FreeBSD jail, plain, vanilla. That’s what you get. For production, I will move this to something like 127.0.0.24:8923 on lo1.

A more detailed example

This is the working Nginx configuration before adding Anubis. Much of the website details are missing to keep it simple.

[17:36 stage-nginx01 dvl /usr/local/etc/nginx/includes] % cat freshports.conf 
# redirect from port 80 to 443
server {
  listen 10.55.0.42:80;

  server_name test-nginx01.int.unixathome.org;

  return 301 https://$server_name$request_uri;
}

server {
  listen 10.55.0.42:443 ssl;
  http2 on;
  
  server_name test-nginx01.int.unixathome.org;

  include "/usr/local/etc/freshports/virtualhost-common.conf";      # this contains the documentroot etc
  include "/usr/local/etc/freshports/virtualhost-common-ssl.conf";

  ssl_certificate     /usr/local/etc/ssl/test-nginx01.int.unixathome.org.fullchain.cer;
  ssl_certificate_key /usr/local/etc/ssl/test-nginx01.int.unixathome.org.key;

}

This is after adding in Anubis:

# redirect from port 80 to 443
# no changes here
server {
  listen 10.55.0.42:80;

  server_name test-nginx01.int.unixathome.org;

  include "/usr/local/etc/freshports/virtualhost-common.conf";

  return 301 https://$server_name$request_uri;
}

# take your original ssl server, and comment out all the content, and add in the proxy stuff.
server {
  listen 10.55.0.42:443 ssl;
  http2 on;
  
  server_name test-nginx01.int.unixathome.org;

#  include "/usr/local/etc/freshports/virtualhost-common.conf";
#  include "/usr/local/etc/freshports/virtualhost-common-ssl.conf";

  # this gets added in
  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_pass http://anubis;
  }

  ssl_certificate     /usr/local/etc/ssl/test-nginx01.int.unixathome.org.fullchain.cer;
  ssl_certificate_key /usr/local/etc/ssl/test-nginx01.int.unixathome.org.key;
}

# this is what your TLS based config above will redirect to
# this matches settings in /etc/rc.conf - see next section
upstream anubis {
  server 127.0.0.1:8923 max_fails=99999 max_conns=1024 fail_timeout=1s;
  keepalive 256;
  keepalive_timeout 120s;
  keepalive_requests 256;
}


# this is a copy of the original content, minus ssl and listening on another port
server {
  listen 127.0.0.1:8080;
  
  server_name test-nginx01.int.unixathome.org;

  # this ensures the logs created are correct
  set_real_ip_from  10.55.0.42;
  real_ip_header    X-Forwarded-For;
  real_ip_recursive on;


  include "/usr/local/etc/freshports/virtualhost-common.conf"; # this what serves up your content.
#  include "/usr/local/etc/freshports/virtualhost-common-ssl.conf";

#  ssl_certificate     /usr/local/etc/ssl/test-nginx01.int.unixathome.org.fullchain.cer;
#  ssl_certificate_key /usr/local/etc/ssl/test-nginx01.int.unixathome.org.key;
}

The Anubis configuration:

[17:43 test-nginx01 dvl ~] % grep anubis /etc/rc.conf
anubis_enable="YES"
anubis_args="-target http://localhost:8080 -bind :8923 -difficulty 2 -cookie-domain=freshports.org -policy-fname=/usr/local/etc/anubis-freshports.json"

The policy file, taken from https://paulwilde.uk/ponderings/setting-up-anubis-on-freebsd/ with the backend section added by me.

[17:43 test-nginx01 dvl ~] % cat /usr/local/etc/anubis-freshports.json
{"bots":
        [
        {
                "name": "generic-bot-catchall",
                "user_agent_regex": "(?i:bot|crawler|gpt)",
                "action": "CHALLENGE",
                "challenge": {
                        "difficulty": 16,
                        "report_as": 4,
                        "algorithm": "slow"
                }
        },
        {
                "name": "well-known",
                "path_regex": "^/.well-known/.*$",
                "action": "ALLOW"
        },
        {
                "name": "backend",
                "path_regex": "^/backend/.*$",
                "action": "ALLOW"
        },
        {
                "name": "favicon",
                "path_regex": "^/favicon.ico$",
                "action": "ALLOW"
        },
        {
                "name": "robots-txt",
                "path_regex": "^/robots.txt$",
                "action": "ALLOW"
        },
        {
                "name": "generic-browser",
                "user_agent_regex": "Mozilla",
                "action": "CHALLENGE"
        }
        ]
}

That should get you going. Let me know if it does not and I will amend this post.

Log entries

Here is a log entry from my laptop accessing the website. It has been reformatted so you do not have to scroll horizontally:

May  3 20:20:28 test-nginx01 anubis[58856]: {"time":"2025-05-03T20:20:28.161082763Z","level":"INFO","source":
{"function":"github.com/TecharoHQ/anubis/lib.(*Server).PassChallenge",
"file":"github.com/TecharoHQ/anubis/lib/anubis.go","line":402},"msg":"challenge took",
"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15",
"accept_language":"en-US,en;q=0.9","priority":"u=0, i","x-forwarded-for":"10.1.98.18","x-real-ip":"10.1.98.18",
"check_result":{"name":"bot/generic-browser","rule":"CHALLENGE"},"elapsedTime":87}

That IP address (10.1.98.18 ) is the one used by my laptop.

Website Pin Facebook Twitter Myspace Friendfeed Technorati del.icio.us Digg Google StumbleUpon Premium Responsive

Leave a Comment

Scroll to Top