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
This commit is contained in:
2026-09-29 08:58:10 +00:00
parent b12e204d1f
commit dcb26a239f
2 changed files with 34 additions and 26 deletions
+29 -22
View File
@@ -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` | | `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` | | `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` | | `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) | | `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 #### 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 anything set. A set value replaces the default entirely. A set but
unparseable value aborts startup. 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 - Any such client, whether it connects directly or through the proxy,
`X-Forwarded-For` (see the requirements below). If your clients can choose its own rate-limit key by sending its own
connect to webhooker directly from private addresses, set `X-Forwarded-For`. A direct client's header is walked because the
`TRUSTED_PROXIES` to the proxy's address alone. client is itself trusted; behind the proxy, the client's own address
- Clients whose own addresses are private are skipped as trusted hops is skipped as a trusted hop when the chain is walked (below), so the
when the chain is walked (below), so one that sends no entry it wrote is taken as the client. If any of your clients have
`X-Forwarded-For` of its own shares the proxy's bucket. Setting the private addresses, you must set `TRUSTED_PROXIES` to the proxy's
list to the proxy's address alone gives each its own bucket. 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 A proxy the list does not cover, such as nginx on the same host
reaching webhooker over loopback, is not trusted: every request through 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:** - **Environment variables:**
- `WEBHOOKER_ENVIRONMENT=prod` - `WEBHOOKER_ENVIRONMENT=prod`
- `TRUSTED_PROXIES`: normally leave it unset. Docker networks use - `TRUSTED_PROXIES`: Docker networks use private addresses, so the
private addresses, so the default covers your reverse proxy on default covers your reverse proxy on that network. Under the
that network. If the network's addresses are outside the RFC 1918 default, any client with a private address, whether it connects
ranges, or clients reach the app from private addresses other directly or through the proxy, can choose its own rate-limit key
than through the proxy, set it to the proxy's address there. The by sending its own `X-Forwarded-For`. If any clients have private
`remoteIP` field of the `http request` log line for a request that addresses, or the network's addresses are outside the RFC 1918
came through the proxy shows it; the health check's own lines show ranges, set it to the proxy's address there. The `remoteIP` field
`::1`. See [Trusted proxies](#trusted-proxies). 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 - Leave `BIND_ADDRESS` and `DATA_DIR` unset: the image sets
`BIND_ADDRESS` to `0.0.0.0`, and `DATA_DIR` defaults to `BIND_ADDRESS` to `0.0.0.0`, and `DATA_DIR` defaults to
`/var/lib/webhooker`. `/var/lib/webhooker`.
@@ -843,8 +850,7 @@ reports.
peer, which is the proxy on every request: all clients collapse into 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 one global bucket per limit and the receiver's per-IP limits become
service-wide ceilings. See [Trusted proxies](#trusted-proxies). If service-wide ceilings. See [Trusted proxies](#trusted-proxies). If
clients also connect from private addresses, list the proxy and any clients have private addresses, list the proxy and nothing else.
nothing else.
4. **Send `Host` as `$http_host`, not `$host`.** `$host` strips the 4. **Send `Host` as `$http_host`, not `$host`.** `$host` strips the
port. webhooker's Origin/Referer check compares against the host it port. webhooker's Origin/Referer check compares against the host it
was given, so on any port other than 443 `$host` makes every form 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 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 rate-limit `POST /pages/login` there — the one place a limit can be
applied without reintroducing the lockout, because the proxy sees the applied without reintroducing the lockout, because the proxy sees the
real client address. Setting `TRUSTED_PROXIES` does not stop the real client address. `TRUSTED_PROXIES` does not stop the saturation,
saturation, but it makes the source visible in the failure logs. 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 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 enforced in the webhook handler) can layer on top of this env-level
+5 -4
View File
@@ -180,10 +180,11 @@ type Config struct {
// only forwarded header read. Unless TRUSTED_PROXIES is set it // only forwarded header read. Unless TRUSTED_PROXIES is set it
// is the RFC 1918 private ranges (defaultTrustedProxies). // is the RFC 1918 private ranges (defaultTrustedProxies).
// Other peers' forwarded headers are ignored and they are // Other peers' forwarded headers are ignored and they are
// identified by the connection's own address. Members can // identified by the connection's own address. Under the
// choose their own rate-limit key, so where clients also // default any client with a private address, directly or
// connect from private addresses this must be set to the // through a proxy, can choose its own rate-limit key, so
// proxy hosts alone. // where any clients have private addresses this must be set
// to the proxy hosts alone.
TrustedProxies []netip.Prefix TrustedProxies []netip.Prefix
// AllowedEgressCIDRs is the set of networks a delivery target // AllowedEgressCIDRs is the set of networks a delivery target