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` |
| `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
+5 -4
View File
@@ -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