Anubis 1.28.2 on AWS User Guide
Overview
This guide covers launching and verifying Anubis 1.28.2 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other VPN, proxy and remote access servers in the listing are described on the listing's guide hub.
Anubis is a web AI firewall: a small reverse proxy that sits in front of a website and "weighs the soul" of every request before passing it on. Known AI scrapers and abusive crawlers are denied outright, ordinary browsers solve a short proof-of-work challenge once (a few hundred milliseconds of JavaScript, then a signed cookie), and well-behaved clients and search engines go straight through. It protects sites such as code forges, wikis and documentation from being hammered by scrapers.
On this image, nginx terminates HTTPS on port 443 and hands every request to Anubis on 127.0.0.1:8923; Anubis forwards the traffic it lets through to the site it protects (its TARGET). Out of the box that target is a small demo page on 127.0.0.1:8000, so you can see every part working before you point it at your own application.
cloudimg compiles Anubis 1.28.2 from the upstream release source with Go 1.27.3 and a patched HTTP/2 library (golang.org/x/net 0.60.0): the upstream 1.28.2 binary carries five Go HTTP/2 advisories that this build does not. The build record, with the source and toolchain checksums, is in /usr/share/doc/anubis/BUILD-PROVENANCE.txt.
What is on the image:
- Anubis 1.28.2 (
anubis@default.service) running as the unprivilegedanubisuser, with upstream's default bot policy and an on-disk (bbolt) store - nginx (Ubuntu 24.04 package) serving HTTPS on 443 with a self-signed certificate generated on your instance's first boot; port 80 only redirects to HTTPS
- A per-instance ed25519 signing key for Anubis's challenge cookies, and a per-instance password for the metrics endpoint, both generated on first boot
- A demo upstream page on
127.0.0.1:8000, the defaultTARGETuntil you point Anubis at your own site - A self-test,
anubis-selftest, and a certificate helper,anubis-tls-cert - No metadata access: neither Anubis nor nginx can reach the instance metadata service, and Anubis refuses to be pointed at it
Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3
Security posture
- No default credential and no shared secret. The image ships with no signing key, no metrics password, no TLS key and no rendered policy. First boot generates all of them for this instance only. The signing key lives in
/etc/anubis/keys/default.ed25519(mode 0600, owneranubis) and Anubis reads it from that file, so it never appears on a command line or in the process environment. - Only your names are served. nginx answers only the names first boot read from the instance metadata (public IPv4 address, public DNS name, private IPv4 address) and
localhost. A request for any other host name gets no answer on port 80 or 443. After a solved challenge, Anubis redirects only to those same names (REDIRECT_DOMAINS), so it cannot be used as an open redirect. - Client addresses come from the connection, never from headers. nginx overwrites
X-Real-IPandX-Forwarded-Forwith the real peer address and removes client-suppliedX-Forwarded-HostandX-Original-URIheaders before Anubis sees the request. - Internal services stay local. Anubis (
8923), its metrics and health server (9090) and the demo upstream (8000) listen on127.0.0.1only. The metrics server needs this instance's password; its profiling (pprof) routes are off. - TLS 1.2 and 1.3 only on port 443.
- No SSRF to instance credentials.
anubis@default.serviceandnginx.servicecarry anIPAddressDenyrule for the metadata addresses (169.254.169.254, fd00:ec2::254, 168.63.129.16), so the proxy can never fetch instance credentials, whatever itsTARGET. Anubis's start-up check also refuses aTARGETthat names the metadata service (169.254.169.254,instance-data,fd00:ec2::254,168.63.129.16). The AMI is registered IMDSv2 only. - Least privilege. Anubis runs as the
anubissystem user with no capabilities, a read-only system and a private/tmp. - Fails closed. Anubis and nginx start only after first boot has written this instance's secrets and a ready marker, and every Anubis start runs a check that refuses to start it without a valid signing key and rendered policy. If first boot cannot finish, both stay stopped and first boot tries again on the next boot.
- Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- The site you want to protect, reachable from the instance (in the same VPC, or over the internet)
Recommended instance type: m5.large. Anubis itself is light; size the instance for your traffic.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| Anubis 1.28.2 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Anubis 1.28.2 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group allows SSH (22), HTTP (80, redirect only) and HTTPS (443). Restrict SSH to your own IP address.
- Choose your VPC and a public subnet (or attach an Elastic IP later), leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. It creates a security group that allows SSH from your management address and the web ports from everyone, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR range with your own:
SG=$(aws ec2 create-security-group --group-name anubis \
--description "Anubis web AI firewall" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--associate-public-ip-address \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=anubis}]'
You do not need --metadata-options: the AMI already makes IMDSv2 required.
Step 2: Connect and read the connection details
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot finishes within a minute of launch. It issues the TLS certificate for the instance's addresses, generates the signing key and the metrics password, and starts Anubis and nginx. The details are in /root/anubis-credentials.txt, readable only by root. This command prints everything except the metrics password:
sudo grep -v '^METRICS_PASSWORD=' /root/anubis-credentials.txt
# Anubis 1.28.2 - generated on first boot by anubis-firstboot.service
# These values are unique to this instance. Nothing here was baked into the image.
ANUBIS_HOST=3.83.251.202
ANUBIS_URL=https://3.83.251.202/
TLS_CERT_SHA256=D8:FA:54:42:85:1C:28:38:46:E5:26:48:27:E7:CB:7A:04:32:E4:E7:36:64:AF:06:FE:7D:09:66:20:61:23:56
ANUBIS_TARGET=http://127.0.0.1:8000
SIGNING_KEY_FILE=/etc/anubis/keys/default.ed25519
METRICS_URL=http://127.0.0.1:9090/metrics
METRICS_USERNAME=metrics
# Open TCP 443 (and 80, which only redirects) in the security group to the clients you want to reach the site.
# Protect your own site: set TARGET in /etc/anubis/default.env, then sudo systemctl restart anubis@default
# New name or Elastic IP: sudo anubis-tls-cert <name-or-ip> ...
# Self-test: sudo anubis-selftest
# Guide: https://www.cloudimg.co.uk/guides/anubis-1-28-2-on-ubuntu-24-04-aws/
Step 3: Confirm Anubis and nginx are running
Both services are active and first boot has completed. nginx listens on ports 80 and 443 on every interface; Anubis (8923), its metrics server (9090) and the demo upstream (8000) listen on 127.0.0.1 only:
systemctl is-active anubis@default.service nginx.service anubis-firstboot.service
anubis --version
sudo ss -Htlnp '( sport = :80 or sport = :443 or sport = :8923 or sport = :9090 or sport = :8000 )' | awk '{print $4}' | sort -u
active
active
active
Anubis 1.28.2
0.0.0.0:443
0.0.0.0:80
127.0.0.1:8000
127.0.0.1:8923
127.0.0.1:9090
[::]:443
[::]:80
Step 4: Run the self-test
The shipped self-test checks the whole path from the instance itself: an ordinary client reaches the protected site through nginx and Anubis, a known AI crawler is denied, a browser is challenged first, the metrics server refuses a request without the password and shows Anubis's counters with it, a host name the instance does not serve gets no answer, port 80 redirects to HTTPS, and the instance metadata service is unreachable from inside both services. No secret is printed:
sudo anubis-selftest
ok: anubis@default and nginx are active
ok: an ordinary client reaches the protected site through nginx -> Anubis
ok: a known AI crawler (Amazonbot) is denied by Anubis
ok: a browser gets the Anubis proof-of-work challenge first
ok: metrics: 401 without the password, Anubis's counters with it (loopback only)
ok: unknown Host: no answer on 80 or 443; port 80 redirects to https://3.83.251.202/
ok: metadata: 169.254.169.254 is unreachable from inside anubis@default and nginx
SELFTEST_OK
Step 5: See how Anubis treats different visitors
The same page requested with three different user agents: a command-line client goes straight through to the site, an AI crawler gets the deny page ("Oh noes!"), and a browser gets the proof-of-work challenge ("Making sure you're not a bot!"). Anubis answers both with HTTP 200 on purpose, so scrapers learn nothing from the status code. (--compressed makes curl ask for gzip like a real browser; Anubis treats a "browser" that cannot take gzip as a likely bot.)
for ua in "curl/8.5.0" \
"Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)" \
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0"; do
printf '%-14s ' "${ua%%/*}"
curl -sk --compressed -H "Host: <ANUBIS_HOST>" -A "$ua" -o /tmp/anubis-page.html -w '%{http_code} ' https://127.0.0.1/
grep -o '<title>[^<]*</title>' /tmp/anubis-page.html
done
rm -f /tmp/anubis-page.html
curl 200 <title>cloudimg Anubis demo upstream</title>
Mozilla 200 <title>Oh noes!</title>
Mozilla 200 <title>Making sure you're not a bot!</title>
Open https://<public-ip>/ in your own browser: after accepting the self-signed certificate you see the challenge page for a moment, then the demo page (cloudimg checked this with Chrome against a fresh instance: challenge solved, redirected back to the instance's own address, demo page served). The cookie Anubis sets is signed with this instance's key and lasts 7 days by default. Headless browsers that announce themselves (HeadlessChrome) are denied by the default policy.
Step 6: Point Anubis at your own site
Anubis forwards the traffic it lets through to TARGET in /etc/anubis/default.env. The shipped settings:
grep -E '^(BIND|TARGET|POLICY_FNAME|ED25519_PRIVATE_KEY_HEX_FILE|REDIRECT_DOMAINS)=' /etc/anubis/default.env
BIND=127.0.0.1:8923
TARGET=http://127.0.0.1:8000
POLICY_FNAME=/etc/anubis/default.botPolicies.yaml
ED25519_PRIVATE_KEY_HEX_FILE=/etc/anubis/keys/default.ed25519
REDIRECT_DOMAINS=3.83.251.202,ec2-3-83-251-202.compute-1.amazonaws.com,172.31.84.64,localhost,127.0.0.1
Set TARGET to your application's address, for example a web server in the same VPC, and restart Anubis. The start-up check validates the configuration first; Anubis keeps the original Host header, so name-based virtual hosts on your backend keep working:
sudo sed -i 's|^TARGET=.*|TARGET=http://<your-backend>:8080|' /etc/anubis/default.env
sudo systemctl restart anubis@default
sudo anubis-selftest
The self-test's "ordinary client" check looks for the demo page, so once you have changed TARGET it reports that line as failed; everything else still applies. Allow traffic from this instance's security group to your backend, and stop exposing the backend directly so scrapers cannot go around Anubis.
WebSockets. nginx does not forward WebSocket upgrades by default. If your site needs them, add proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade"; to the location / block of /etc/nginx/sites-available/cloudimg-anubis and make sure your application checks the Origin of WebSocket requests, then sudo nginx -t && sudo systemctl reload nginx.
Behind a load balancer. If you put an Application Load Balancer in front of this instance, nginx sees the load balancer's address as the client. Tell nginx to trust the load balancer's X-Forwarded-For only from your VPC range with set_real_ip_from <vpc-cidr>; and real_ip_header X-Forwarded-For; in the server block, so Anubis gets the visitor's real address.
Step 7: Use your own domain name
anubis-tls-cert re-issues the self-signed certificate, updates the names nginx answers to and Anubis may redirect to, and restarts Anubis (visitors solve one new challenge). With no argument it reads the instance's current addresses from the instance metadata (useful after attaching an Elastic IP); with arguments it uses the names you give, for example your own DNS name:
sudo anubis-tls-cert | tail -2
sudo anubis-selftest | tail -1
nginx now answers to: 3.83.251.202 ec2-3-83-251-202.compute-1.amazonaws.com 172.31.84.64 localhost 127.0.0.1 (Anubis REDIRECT_DOMAINS=3.83.251.202,ec2-3-83-251-202.compute-1.amazonaws.com,172.31.84.64,localhost,127.0.0.1)
SHA-256 fingerprint: 31:74:C4:EB:AF:0A:18:16:6B:11:5E:C4:4E:58:57:AE:E3:FF:3F:74:3B:4F:4D:32:4F:BC:2C:9E:BA:2B:A2:AC
SELFTEST_OK
For your own domain run sudo anubis-tls-cert <www.example.com> with the name your DNS record points at this instance. To use a certificate from a public CA instead, copy its full chain to /etc/nginx/cloudimg-tls/anubis.crt and its key to /etc/nginx/cloudimg-tls/anubis.key (mode 0600, owner root), then sudo systemctl reload nginx.
Step 8: Read Anubis's metrics
The Prometheus metrics server listens on 127.0.0.1:9090 and needs the user metrics and this instance's password. The anubis_* counters appear once Anubis has evaluated a request after its last restart. This command reads the password inside a root shell and hands it to curl on standard input, so it never appears on a command line:
curl -s -o /dev/null -w 'without the password: %{http_code}\n' http://127.0.0.1:9090/metrics
sudo sh -c 'printf "user = \"metrics:%s\"\n" "$(sed -n "s/^METRICS_PASSWORD=//p" /root/anubis-credentials.txt)" | curl -s -K - http://127.0.0.1:9090/metrics | grep -m 5 -E "^anubis_(policy_results|proxied_requests_total)"'
without the password: 401
anubis_policy_results{action="ALLOW",asn="",asn_description="",rule="threshold/minimal-suspicion"} 3
anubis_policy_results{action="CHALLENGE",asn="",asn_description="",rule="threshold/moderate-suspicion"} 2
anubis_policy_results{action="DENY",asn="",asn_description="",rule="bot/ai-catchall"} 1
anubis_policy_results{action="DENY",asn="",asn_description="",rule="bot/ai-crawlers-training"} 1
anubis_policy_results{action="WEIGH",asn="",asn_description="",rule="bot/generic-browser"} 2
To scrape it from a Prometheus server elsewhere, use an SSH tunnel or a local agent rather than opening port 9090.
Step 9: Metadata access is blocked
Both services carry the metadata deny rule, and the self-test proved in Step 4 that the metadata service does not answer from inside either of them:
systemctl show -p IPAddressDeny anubis@default.service nginx.service | grep .
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Even if TARGET were changed to http://169.254.169.254/ (or instance-data.ec2.internal), the start-up check would refuse to start Anubis, and the deny rule would block the connection anyway. Anubis's optional S3 storage backend therefore needs static access keys in the policy file; the instance role is deliberately out of reach.
Step 10: Tune the bot policy
The policy is upstream's default (/usr/share/doc/anubis/botPolicies.upstream.yaml) with two cloudimg changes: the store is bbolt under /var/lib/anubis/default (upstream recommends it over the in-memory store for a single server), and the metrics block binds 127.0.0.1:9090 with this instance's password. The rendered file is /etc/anubis/default.botPolicies.yaml (mode 0640, it holds the metrics password); edit it and restart Anubis:
sudo nano /etc/anubis/default.botPolicies.yaml
sudo systemctl restart anubis@default && sudo anubis-selftest | tail -1
The default policy's country and ASN rules only take effect when you add a MaxMind GeoIP database in a geoip block (Anubis logs a warning at start-up until then). See the Anubis policy documentation for allow lists, thresholds and challenge types.
Step 11: How the fail-closed start works
Anubis and nginx start only once first boot has written /var/lib/cloudimg/anubis-ready, and every Anubis start runs a check (as root, before dropping to the anubis user) that refuses to start it without a valid signing key and rendered policy, or with a TARGET that names the metadata service:
systemctl cat anubis@default.service | grep -E '^(ConditionPathExists|ExecStartPre|User)='
systemctl cat nginx.service | grep -E '^ConditionPathExists='
sudo stat -c '%a %U:%G %n' /etc/anubis/keys/default.ed25519 /etc/anubis/default.botPolicies.yaml /etc/nginx/cloudimg-tls/anubis.key /root/anubis-credentials.txt
ConditionPathExists=/var/lib/cloudimg/anubis-ready
User=anubis
ExecStartPre=+/usr/local/sbin/anubis-preflight %i
ConditionPathExists=/var/lib/cloudimg/anubis-ready
600 anubis:anubis /etc/anubis/keys/default.ed25519
640 root:anubis /etc/anubis/default.botPolicies.yaml
600 root:root /etc/nginx/cloudimg-tls/anubis.key
600 root:root /root/anubis-credentials.txt
Step 12: Security recommendations
- Keep SSH open only to your management addresses. Open 80 and 443 to the visitors your site serves.
- Put the backend you protect where only this instance can reach it, so scrapers cannot bypass Anubis.
- Use your own DNS name and a CA certificate (Step 7) for a public site.
- Keep the signing key private: anyone with it can mint valid Anubis cookies. To rotate it, generate a new one (
sudo sh -c 'umask 077; openssl rand -hex 32 | tr -d "\n" > /etc/anubis/keys/default.ed25519; chown anubis:anubis /etc/anubis/keys/default.ed25519') and restart Anubis; visitors simply solve one new challenge. - Leave unattended security upgrades on. Anubis is a single binary in
/usr/local/bin/anubis; cloudimg ships updated versions as new versions of this listing.
Support and licensing
Anubis is open-source software by Xe Iaso and Techaro, distributed under the MIT License; the licence and cloudimg's build record (source, toolchain, the one dependency change and their checksums, plus a source offer) are in /usr/share/doc/anubis/. cloudimg provides the packaging, the patched build, the secure defaults, the per-instance key, password and certificate, the metadata block, the fail-closed start, the self-test and 24/7 support.
cloudimg is not affiliated with or endorsed by Techaro or the Anubis project. Anubis is used here only to identify the software. nginx is a trademark of F5, Inc.
For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.