documentation

getting started

Drop GroundShade in behind Caddy, nginx, or HAProxy. Five minutes to a working request path.

updated

GroundShade sits between your TLS terminator and your app. It does not terminate TLS itself; your fronting proxy keeps doing that and forwards the JA4 fingerprint as X-JA4.

client → TLS terminator (Caddy / nginx / HAProxy) → groundshade → your origin

Run with Docker Compose

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:?set an admin token}
      GROUNDSHADE_TRUSTED_PROXIES: 172.18.0.2/32
    ports:
      - "127.0.0.1:9090:9090"
    volumes:
      - groundshade-state:/var/lib/groundshade

volumes:
  groundshade-state:

Generate the admin token once:

openssl rand -hex 32

Put it in your .env. The token gates /admin/* and /metrics. Startup refuses to bind admin on a non-loopback address without a token.

Then point your fronting proxy at groundshade:8080 instead of your app. Note that, if you are using docker, groundshade and your app need to share the same docker network.

Check the dashboard

The container’s admin port is published to host loopback on 127.0.0.1:9090. Open it in a browser:

http://127.0.0.1:9090/

The login page accepts your admin token and sets a cookie. After login you land on the dashboard.

If you’re not on the host, tunnel over SSH:

ssh -L 9090:127.0.0.1:9090 your-host

The JA4 arm of the rate signal needs your fronting proxy to forward the TLS fingerprint as X-JA4. Without it you keep the per-IP/24 arm and trustless persistence; you lose detection of botnets that share a TLS library across many IPs.

Stock Caddy has no JA4 placeholder. The build recipe lives at examples/deploy/caddy-ja4.Dockerfile and the matching Caddyfile at examples/deploy/caddy-ja4.Caddyfile. For nginx and HAProxy see nginx-ja4.conf and haproxy-ja4.cfg.

GroundShade auto-detects JA4. After 100 requests (or 60 seconds), if no JA4 has arrived, it logs a single WARN and the dashboard’s signals row reads JA4: not detected. Everything else keeps working.

Trusted proxies

GROUNDSHADE_TRUSTED_PROXIES should list only your fronting proxy’s pinned IP. Trusted peers get X-Forwarded-For and X-Real-IP honored and bypass the per-IP connection cap. Pin tight: a /16 would grant cap bypass to every container on the bridge.

Defaults trust only loopback (127.0.0.1/32, ::1/128).

Where to go next