gladios

Stop bots and HTTP floods before they reach your app

gladios adds a “Checking your browser” page to the nginx you already run. People pass in about a second, once a week. Bots and flood tools that don’t run JavaScript never get past it, and turning them away costs nginx no more than serving a file.

  • No service or proxy to run
  • No third party sees your traffic
  • Free and open source
curl -fsSL https://gladios.dev/install.sh | sudo sh

Then one line per server: include snippets/gladios.conf;

gladios’s mascot, a gladiator reading a book

Checking your browser

A quick check keeps bots away. It runs about once a week on each device.

Ready when you are.

The real solver, slowed down so you can watch; the numbers are real. See the whole page →

Floods don’t run JavaScript. Browsers do.

A browser spends ~65,000 hashes on a puzzle; nginx checks the answer with one. It all runs in nginx and its JavaScript module (njs), so there’s no proxy hop and no process to keep alive. Same idea as Anubis, minus the service.

No pass yet
  1. Challenge pagestatic file, 200
  2. Browser solves the puzzleSHA-256 on Web Workers
  3. Signed pass cookie
  4. Reloads the same URL
Has a pass
  1. njs checks itone HMAC
  2. Your app

Forged, expired or from another network: the challenge page again, and a strike for fail2ban.

Bypassed
  1. API clients, webhooks, monitors…your rules
  2. Your app

Stateless

Puzzles and passes are HMACs. Nothing is stored, so every worker and host sharing the secret agrees.

Bound to the network

A pass only works from the address (or IPv6 /64) that solved it, so it can’t be shared across a botnet.

Cheap where it counts

No cookie costs a static file. A pass-shaped cookie costs one HMAC. Nothing else reaches njs.

Forgers get banned

Forged passes leave strikes for fail2ban, with ban digests to Slack, Discord or a webhook.

Installed in a minute

Debian, Ubuntu, Alpine or nginx.org’s packages. The installer takes care of njs.

  1. Run the installer

    curl -fsSL https://gladios.dev/install.sh | sudo sh

    Installs njs if it’s missing, copies gladios into place and writes a secret and /etc/gladios/settings.conf. It never reloads nginx, and backs out if nginx -t fails. Read it first; run it again to upgrade.

  2. Guard a server

    server {
        include snippets/gladios.conf;
        location /healthz { auth_request off; }  # left out
    }
  3. Reload nginx

    sudo nginx -t && sudo systemctl reload nginx

Who gets checked

Anything that can’t run a browser needs a bypass, in /etc/gladios/settings.conf:

map $request_uri $gladios_bypass {
    default          0;
    ~^/\.well-known/ 1;   # ACME and friends
    ~^/api/          1;
}

Requirements

  • nginx with njs 0.7.9+. On Ubuntu 22.04, use nginx.org’s packages.
  • HTTPS: the pass cookie is Secure.
  • Behind a proxy, realip restoring the client’s address.
  • Optional: fail2ban, and Python 3.8+ for ban notifications.

Tens of thousands of requests a second, on two cores

The check runs inside the nginx worker that already has the request.

RequestMost servedp50CPU at 1,000 req/s
With a valid passchecked, then the app’s page
35–36k/s
1.7 ms 0.14 cores
With a forged passchecked, challenge page, strike
24–26k/s
2.4 ms 0.19 cores
Without a cookiechallenge page, nothing to check
35–36k/s
1.7 ms 0.15–0.18 cores
GET /.gladios/api/challengeunthrottled
59–61k/s
1.0 ms 0.11–0.17 cores
POST /.gladios/api/verifyunthrottled
47–50k/s
1.2 ms 0.12–0.13 cores
nginx pinned to 2 cores, oha on others, TLS keep-alive, 64 connections; Debian 12’s nginx 1.22 and njs 0.7.9 in an Apple M1 Pro’s Linux VM. nginx’s three processes took 11–12 MB. test/bench/run.sh runs it on yours.

Make it yours

Drop a mascot.webp (or png, svg, jpg, gif) in /etc/gladios/site/ to replace the gladiator on the default page, or put a whole index.html of your own there. Upgrades never touch it.

What it won’t do

  • Stop a real headless browser. It pays the work, as with any check like this; per-network passes and rate limits make that costly at scale.
  • Let visitors without JavaScript in.
  • Show monitors your app is down: the page answers 200, so bypass them.