documentation

deployment hardening

Pre-deploy security checklist. Admin port, trusted proxies, body limits, and reload safety. Read this once before publishing.

updated

GroundShade’s security guarantees depend on correct network boundaries. Confirm these settings before publishing.

Admin port

  • Bind admin to loopback on bare metal: listen.admin: "127.0.0.1:9090".
  • If admin binds any non-loopback address, set listen.admin_token or GROUNDSHADE_ADMIN_TOKEN. Startup refuses unauthenticated non-loopback admin listeners.
  • In Docker, publish the host port to loopback only: "127.0.0.1:9090:9090". The container itself can listen on 0.0.0.0:9090 (the published image does this by default).
  • Prometheus must send Authorization: Bearer <admin-token> when a token is set.

Trusted proxies

GroundShade ignores X-Forwarded-For and X-Real-IP unless the immediate TCP peer is in listen.trusted_proxies.

Defaults trust only loopback:

listen:
  trusted_proxies:
    - "127.0.0.1/32"
    - "::1/128"

For Docker, add your fronting proxy’s pinned container IP:

listen:
  trusted_proxies:
    - "172.18.0.10/32"

or via env:

GROUNDSHADE_TRUSTED_PROXIES=172.18.0.10/32

Do not set 0.0.0.0/0 or a broad public range. Any trusted peer can supply the client IP used for fast-lane IP allowlists, crawler verification, behavioural signal buckets, logs, and trust-token binding.

Connection-cap bypass

Peers in listen.trusted_proxies also bypass the per-IP connection cap (selfdef.max_connections_per_ip). This is intentional: a fronting Caddy or nginx container aggregates many real clients into one TCP peer and would otherwise saturate the cap.

The global cap (max_connections_total) and backpressure still apply.

If groundshade_connections_rejected_total{reason="per_ip"} is climbing behind a fronting proxy, the proxy’s IP is not in listen.trusted_proxies.

Pin tightly. Use a /32 for a single container IP. A /16 for the entire docker bridge grants cap bypass to every container on it, so a compromised sibling container could exhaust the global cap without per-IP throttling.

Behind Cloudflare orange-cloud. The XFF chain has an extra hop (CF edge → your fronting proxy). Both must appear in trusted_proxies. See behind cloudflare for the full recipe and the architectural limits you accept.

Body limits

selfdef.max_body_bytes (default 10 MiB) applies to forwarded request bodies and to GroundShade’s own POST endpoints:

  • /.well-known/groundshade/solve
  • /admin/login
  • /admin/shields

Oversized internal-endpoint requests return 413 Payload Too Large.

selfdef.max_header_bytes (default 32 KiB) applies to all inbound headers.

Reloads

SIGHUP re-reads the config file and reapplies environment overrides. Docker and systemd-style deployments can keep values like GROUNDSHADE_ADMIN_TOKEN, GROUNDSHADE_UPSTREAM_URL, and GROUNDSHADE_TRUSTED_PROXIES outside YAML and they survive each reload.

A validation failure logs a warning and keeps the old config running. SIGHUP cannot take the proxy down through bad config.

Pre-deploy checklist

  • listen.trusted_proxies lists every fronting peer with a /32 (or its v6 equivalent).
  • listen.admin is 127.0.0.1:9090, or an admin token is set.
  • GROUNDSHADE_ADMIN_TOKEN is from openssl rand -hex 32, not a sentence.
  • State directory is mounted as a volume so trust.key survives restarts.
  • Host port 9090 is published to host loopback only.
  • Webhook URLs (if any) point at internal endpoints, not third parties.
  • observe.log_ip_hash: true unless you have a written reason to log raw IPs.