diff --git a/README.md b/README.md index 6a2c902..b65b5d2 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ TTY detection, and security headers are always applied. | `RETENTION_SWEEP_INTERVAL` | How often the retention reaper and archive sweeper run (Go duration, must be positive) | `1h` | | `SESSION_IDLE_TIMEOUT` | Idle session timeout (Go duration) | `24h` | | `RECEIVER_RATE_LIMIT` | Receiver requests/minute per IP per entrypoint (10x that per IP across the route) | `120` | -| `TRUSTED_PROXIES` | CIDRs whose forwarded headers are trusted. A set value replaces the default. If clients connect directly from private addresses, set it to the proxy's address alone. See [Trusted proxies](#trusted-proxies) | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` (RFC 1918) | +| `TRUSTED_PROXIES` | CIDRs whose forwarded headers are trusted. A set value replaces the default. Under the default, any client with a private address, whether it connects directly or through the proxy, can choose its own rate-limit key by sending its own `X-Forwarded-For`; if any clients have private addresses, set it to the proxy's address alone. See [Trusted proxies](#trusted-proxies) | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` (RFC 1918) | | `ALLOWED_EGRESS_CIDRS` | CIDRs that delivery targets may reach despite the SSRF blocklist. Read [Allowing egress to your own network](#allowing-egress-to-your-own-network) before setting it | `""` (none) | #### Allowing egress to your own network @@ -391,16 +391,21 @@ reaching webhooker over a Docker network or a private LAN without anything set. A set value replaces the default entirely. A set but unparseable value aborts startup. -Trusting those ranges has two consequences: +Trusting those ranges has two consequences for clients with private +addresses: -- Any peer in them chooses its own rate-limit key through - `X-Forwarded-For` (see the requirements below). If your clients - connect to webhooker directly from private addresses, set - `TRUSTED_PROXIES` to the proxy's address alone. -- Clients whose own addresses are private are skipped as trusted hops - when the chain is walked (below), so one that sends no - `X-Forwarded-For` of its own shares the proxy's bucket. Setting the - list to the proxy's address alone gives each its own bucket. +- Any such client, whether it connects directly or through the proxy, + can choose its own rate-limit key by sending its own + `X-Forwarded-For`. A direct client's header is walked because the + client is itself trusted; behind the proxy, the client's own address + is skipped as a trusted hop when the chain is walked (below), so the + entry it wrote is taken as the client. If any of your clients have + private addresses, you must set `TRUSTED_PROXIES` to the proxy's + address alone. +- A client behind the proxy that sends no `X-Forwarded-For` of its own + shares the proxy's bucket, because its own address is skipped too. + Setting the list to the proxy's address alone gives each its own + bucket. A proxy the list does not cover, such as nginx on the same host reaching webhooker over loopback, is not trusted: every request through @@ -766,14 +771,16 @@ repository's `Dockerfile` and runs it. The app needs: - **Environment variables:** - `WEBHOOKER_ENVIRONMENT=prod` - - `TRUSTED_PROXIES`: normally leave it unset. Docker networks use - private addresses, so the default covers your reverse proxy on - that network. If the network's addresses are outside the RFC 1918 - ranges, or clients reach the app from private addresses other - than through the proxy, set it to the proxy's address there. The - `remoteIP` field of the `http request` log line for a request that - came through the proxy shows it; the health check's own lines show - `::1`. See [Trusted proxies](#trusted-proxies). + - `TRUSTED_PROXIES`: Docker networks use private addresses, so the + default covers your reverse proxy on that network. Under the + default, any client with a private address, whether it connects + directly or through the proxy, can choose its own rate-limit key + by sending its own `X-Forwarded-For`. If any clients have private + addresses, or the network's addresses are outside the RFC 1918 + ranges, set it to the proxy's address there. The `remoteIP` field + of the `http request` log line for a request that came through + the proxy shows it; the health check's own lines show `::1`. See + [Trusted proxies](#trusted-proxies). - Leave `BIND_ADDRESS` and `DATA_DIR` unset: the image sets `BIND_ADDRESS` to `0.0.0.0`, and `DATA_DIR` defaults to `/var/lib/webhooker`. @@ -843,8 +850,7 @@ reports. peer, which is the proxy on every request: all clients collapse into one global bucket per limit and the receiver's per-IP limits become service-wide ceilings. See [Trusted proxies](#trusted-proxies). If - clients also connect from private addresses, list the proxy and - nothing else. + any clients have private addresses, list the proxy and nothing else. 4. **Send `Host` as `$http_host`, not `$host`.** `$host` strips the port. webhooker's Origin/Referer check compares against the host it was given, so on any port other than 443 `$host` makes every form @@ -2716,8 +2722,9 @@ re-fills both verification slots on its first two requests. The remedies are to block the source at the reverse proxy, or to rate-limit `POST /pages/login` there — the one place a limit can be applied without reintroducing the lockout, because the proxy sees the -real client address. Setting `TRUSTED_PROXIES` does not stop the -saturation, but it makes the source visible in the failure logs. +real client address. `TRUSTED_PROXIES` does not stop the saturation, +but when it covers the proxy the source is visible in the failure logs, +and the default covers a proxy on a private network. Finer-grained per-webhook rate limits (configured in the web UI and enforced in the webhook handler) can layer on top of this env-level diff --git a/internal/config/config.go b/internal/config/config.go index 86d9c28..00bf0a0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -180,10 +180,11 @@ type Config struct { // only forwarded header read. Unless TRUSTED_PROXIES is set it // is the RFC 1918 private ranges (defaultTrustedProxies). // Other peers' forwarded headers are ignored and they are - // identified by the connection's own address. Members can - // choose their own rate-limit key, so where clients also - // connect from private addresses this must be set to the - // proxy hosts alone. + // identified by the connection's own address. Under the + // default any client with a private address, directly or + // through a proxy, can choose its own rate-limit key, so + // where any clients have private addresses this must be set + // to the proxy hosts alone. TrustedProxies []netip.Prefix // AllowedEgressCIDRs is the set of networks a delivery target