From dcb26a239fe3615bcf522fc6abcc71675a1e1528 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 29 Sep 2026 08:21:05 +0000 Subject: [PATCH] Say that any private-addressed client can choose its rate-limit key Under the default, a client with a private address picks its own rate-limit key through X-Forwarded-For whether it connects directly or through the proxy, so the README and the TrustedProxies comment now tell an operator with any such clients to set the list to the proxy alone. The login endpoint section no longer assumes the proxy is uncovered by default. Model: opus-5-5 --- README.md | 51 ++++++++++++++++++++++----------------- internal/config/config.go | 9 ++++--- 2 files changed, 34 insertions(+), 26 deletions(-) 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