Gate forwarded-header trust behind trusted-proxy config (closes #88)
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
Every rate limiter keyed on httprate.KeyByRealIP, which believes True-Client-IP, X-Real-IP and the first X-Forwarded-For entry from any peer. A client could therefore mint a fresh bucket per request by rotating a spoofed header, or drain another client's bucket by claiming its address, which left the receiver, login and password change limits with no value against a deliberate attacker. The receiver, login and password change limiters now share one key function: the connection's own address, unless the direct peer is inside a network listed in the new TRUSTED_PROXIES CIDR list, in which case the forwarded client address is used. The list is empty by default, so nothing is trusted until an operator names their proxy; a set-but-unparseable value aborts startup, matching the handling of the other parsed variables. Within a trusted request X-Forwarded-For is walked right to left and the first hop that is not itself a trusted proxy wins, so client-prepended entries cannot be selected. Also folds in two cleanups from the same review: the 429 responder shared by all three limiters is extracted, and the RECEIVER_RATE_LIMIT error-path tests now assert that the failure names the variable and wraps ErrNonPositiveValue rather than only that some error occurred.
This commit is contained in:
37
README.md
37
README.md
@@ -95,6 +95,28 @@ TTY detection, and security headers are always applied.
|
||||
| `SENTRY_DSN` | Sentry error reporting DSN | `""` |
|
||||
| `SESSION_IDLE_TIMEOUT` | Idle session timeout (Go duration) | `24h` |
|
||||
| `RECEIVER_RATE_LIMIT` | Receiver requests/minute per IP per entrypoint | `120` |
|
||||
| `TRUSTED_PROXIES` | CIDRs whose forwarded headers are trusted | `""` (none) |
|
||||
|
||||
#### Trusted proxies
|
||||
|
||||
`TRUSTED_PROXIES` is a comma-separated list of CIDR blocks (a bare
|
||||
address such as `10.0.0.1` is accepted and treated as a single host),
|
||||
for example `10.0.0.0/8, 192.168.1.7, 2001:db8::/32`. It decides whose
|
||||
`X-Forwarded-For`, `X-Real-IP`, and `True-Client-IP` headers the rate
|
||||
limiters believe.
|
||||
|
||||
Forwarded headers are honoured **only** when the connecting peer is
|
||||
inside one of these blocks; for every other peer the client identity is
|
||||
the connection's own address and the headers are ignored. The default
|
||||
is the empty list, which trusts nobody — anything else would let any
|
||||
client pick its own rate limit bucket, minting a fresh one per request
|
||||
or draining someone else's. Set it to the address of your reverse
|
||||
proxy, and to nothing wider.
|
||||
|
||||
Within a trusted request, `X-Forwarded-For` is read right to left and
|
||||
the first hop that is not itself a trusted proxy wins, so entries a
|
||||
client prepended before reaching the proxy cannot be selected. A set
|
||||
but unparseable value aborts startup.
|
||||
|
||||
Sessions are bounded by two independent clocks, and end at whichever
|
||||
one runs out first:
|
||||
@@ -124,8 +146,9 @@ fatal configuration error: webhooker logs the offending variable and
|
||||
its value and refuses to start, rather than silently running with a
|
||||
substituted default. `PORT=eighty`, `DEBUG=ture`, and
|
||||
`RETENTION_SWEEP_INTERVAL=1 hour` all abort startup. `PORT` must
|
||||
additionally be a number in the range 1–65535, and
|
||||
`RECEIVER_RATE_LIMIT` must be at least 1.
|
||||
additionally be a number in the range 1–65535,
|
||||
`RECEIVER_RATE_LIMIT` must be at least 1, and every entry in
|
||||
`TRUSTED_PROXIES` must be a CIDR block or a bare IP address.
|
||||
|
||||
Boolean variables (`DEBUG`, `MAINTENANCE_MODE`) accept exactly the
|
||||
spellings Go's `strconv.ParseBool` accepts — `1`, `t`, `T`, `TRUE`,
|
||||
@@ -802,6 +825,16 @@ legitimate webhook senders). Requests over the limit receive HTTP 429
|
||||
with a `Retry-After` header. A set-but-invalid `RECEIVER_RATE_LIMIT`
|
||||
value aborts startup rather than silently falling back to the default.
|
||||
|
||||
Every limiter here — receiver, login, and password change — identifies
|
||||
the client the same way, through one shared key function: the
|
||||
connection's own address, unless the peer is listed in
|
||||
`TRUSTED_PROXIES`, in which case the forwarded client address is used
|
||||
instead. See [Trusted proxies](#trusted-proxies). Deployed without that
|
||||
variable set, a client behind a reverse proxy shares one bucket with
|
||||
every other client behind the same proxy, which is the safe direction
|
||||
to be wrong in: set `TRUSTED_PROXIES` to the proxy's address to get
|
||||
per-client limits back.
|
||||
|
||||
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
|
||||
abuse limit later; they are tracked as future work.
|
||||
|
||||
Reference in New Issue
Block a user