# Deployment hardening

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

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:

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

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

```yaml
listen:
  trusted_proxies:
    - "172.18.0.10/32"
```

or via env:

```sh
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](/docs/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.
