self-hosted · rust · apache-2.0

Catch bots, not people.

GroundShade is a small Rust proxy you install behind your TLS terminator (Caddy, nginx, HAProxy). It watches your origin's health and how each client looks on the wire. When things are calm, traffic passes straight through. When your backend starts hurting, only the suspicious clients hit a wall.

A drop-in alternative to Cloudflare and Anubis. Single binary, no telemetry, no phone-home. Running in production on kycnot.me: 99.98% of bots never even try to solve the wall.

codeberg.org/groundshade/groundshade /docs

apache-2.0 · ~140 mb image · in-memory state · no telemetry

vs cloudflare & anubis
GroundShade compared against Cloudflare and Anubis
Cloudflare Anubis GroundShade
self-hosted
no data sent to third parties
invisible to real users partial
search engines pass through partial
api clients can solve it
no-js challenge
open-source license MIT Apache-2.0
what you get
  • traffic spikes don't fire it

    Spikes aren't suspicious. Symptoms are. The defense ladder lifts on p95 latency and 5xx rate. Individual clients get walled on per-/24 rate, JA4 rate, and unsolved-wall history. An HN front-page hug on a healthy origin? Nothing fires.

  • search engines walk right through

    Real Googlebot, Bingbot, DuckDuckBot, AppleBot. Each verified by rDNS plus forward-DNS, then waved through. A test suite enforces the invariant. Your SEO is never the collateral damage of your defense.

  • one wall, browsers and bots alike

    Browsers solve SHA-256 PoW in pure JS with crypto.subtle.digest. No WASM, no extra bundle. CI jobs and cron scripts solve the same challenge through a JSON envelope. Python and shell reference solvers ship in the repo.

  • real users outlive your redeploys

    Push a new version; sessions stay alive. The HMAC signing key persists to disk, so trust cookies bound to (route, IP /24, JA4) survive the restart. Rotate the key (or hit shields-up) to drop every token at once.

  • survives its own attack

    Bounded everywhere. Count-min sketches for rate, fixed-size LRUs for keys, a ring buffer for the detector. Under memory pressure, the accept layer refuses new connections. CI gate: 100k req/s from 10k synthetic attackers on 2 cores. Regressions don't merge.

  • every decision is on screen

    Loopback dashboard with per-route sparklines and a per-request inspector that shows which gates fired, and why. /metrics is a real Prometheus endpoint. Webhooks fire on every defense transition. Hot reload on SIGHUP.

tested in production

GroundShade runs in front of kycnot.me, a small site that attracts the kind of attention small sites shouldn't get: content scrapers, a long tail of bots looking for free data, and constant DDoS attacks.

The site used to sit behind Cloudflare. Default settings let most scrapers through, and the origin kept hurting. Stricter settings (Under Attack mode, managed challenges) stopped the scrapers but also caught real visitors and broke a few search-engine crawlers. There was no sweet spot.

GroundShade hits a different one. It doesn't react to how loud the traffic is. It reacts to whether your backend is hurting, plus three patterns it watches on every client.

Production counters from kycnot.me, anchored to a 15-hour window
1,144,179 walls served extrapolated · ~21 / sec
268 fetched the proof-of-work
161 submitted a valid solve
99.98% stopped at the wall

Real counter snapshot from a 15-hour window on kycnot.me. The top number ticks at the observed rate (estimate).

GroundShade admin dashboard: one route in normal state, 1.27M requests handled since start, traffic split (14% forwarded, 86% challenged), browser PoW funnel showing 1,081,763 served then 226 fetched then 142 solved with 99.98% drop, client family breakdown by browser / script / forged / bot / unknown, behavioural signal counters, and connection caps.
The same numbers, read live from the admin dashboard. Loopback by default; bearer-token auth when published. Reads /metrics directly, no extra polling endpoint.
how it works

Three behavioural signals run on every request after the fast lane. A real visitor matches none of them. A scraper farm matches all three.

  • 01

    rate per /24 of IP address

    Cheap VPS scrapers live inside the same /24. One client looks fine; the prefix together looks loud.

  • 02

    rate per TLS fingerprint (JA4)

    Bots rotate IPs but reuse the same TLS library. JA4 catches the shared fingerprint across thousands of addresses.

  • 03

    per-prefix memory of unsolved challenges

    A /24 that gets walled 20 times without solving once flips to "challenge on sight". A single solve clears it permanently.

The wall itself is a SHA-256 challenge: cheap to verify on your server, expensive to solve at scale. Solving one page costs the scraper about a second of CPU. Their economics break before yours do.

where it sits
client → tls terminator (caddy / nginx / haproxy) → groundshade → your origin

GroundShade does not terminate TLS. That stays in your existing proxy, which also forwards the JA4 fingerprint as X-JA4. Single Rust binary or a ~140 MB Docker image. State stays in memory by default; Redis is optional when you run more than one replica.

quick start
services:
  groundshade:
    image: codeberg.org/groundshade/groundshade:latest
    restart: unless-stopped
    environment:
      GROUNDSHADE_UPSTREAM_URL: http://your-app:3000
      GROUNDSHADE_ADMIN_TOKEN: ${GROUNDSHADE_ADMIN_TOKEN:?}
      GROUNDSHADE_TRUSTED_PROXIES: 172.18.0.2/32
    ports:
      - "127.0.0.1:9090:9090"
    volumes:
      - groundshade-state:/var/lib/groundshade

Make an admin token with openssl rand -hex 32. Point your TLS proxy at groundshade:8080 instead of your app. Full walk-through: /docs/getting-started.

is this for you

Good fit

  • You host your own stuff on your own boxes.
  • You already run Caddy, nginx, or HAProxy in front.
  • Scrapers and traffic bursts are your main pain.
  • You want logs you own and code you can read.

Pick something else if

  • You want a hosted service. GroundShade is self-hosted only.
  • You need a full WAF or geo-blocking product.
  • You need HTTP/3 today. (It's coming, not yet.)
  • You need global anycast. GroundShade runs per node.
roadmap

Things on the way, in rough order. Tracked on Codeberg issues.

  • CrowdSec bouncer

    Consult the local LAPI per request, sub-millisecond. Two-way: emit signals back when the challenge layer detects a clear bot failure, so CrowdSec learns and can contribute to the Community Blocklist (opt-in, anonymised).

  • HTTP/3 (QUIC)

    Today: HTTP/1.1, HTTP/2, WebSockets, SSE. HTTP/3 is deferred but planned, behind the same transport split.

  • native plugins

    Drop-in shims for Caddy, nginx, HAProxy, Envoy, and Traefik via the proxy's clean core/transport split. C-ABI core means thin ports, not rewrites.

  • learned per-route baselines

    Auto-tune the latency and 5xx thresholds from each route's own history. Today the thresholds are static defaults you can override.

  • optional hosted control plane

    Multi-site dashboard, alerting, signed-config GitOps, cross-fleet pattern sharing. For ops shops running many instances. The Apache-2.0 core stays self-hostable forever.

docs