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
| 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 |
-
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-
/24rate, 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.
/metricsis a real Prometheus endpoint. Webhooks fire on every defense transition. Hot reload onSIGHUP.
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.
| 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 |
/metrics directly, no extra polling
endpoint.
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.
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.
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.
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.
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.
- getting started The 5-minute drop-in path.
- operating Daily workflows, metrics, tuning table.
- deployment hardening Pre-deploy security checklist.
- behind cloudflare Orange-cloud setup and what gets silenced.
- architecture Request lifecycle, signals, state surfaces.
- building from source Docker images, Cargo profiles, multi-arch.
- faq JA4 plugin notes, healthcheck cascades, common mistakes.