feat(membership): trust reverse-proxy forwarded client IP for rate limit
The per-IP rate limiter keys on the transport RemoteAddr. Behind the same-origin Caddy proxy that fronts the control plane, every request arrives with the proxy's single IP, which collapses the limiter into one bucket shared by the whole world — a flood from one client throttles all of them. Add an opt-in `--trusted-proxies` flag (comma-separated IPs/CIDRs). When the immediate peer is one of the named proxies, clientIP now believes its X-Forwarded-For (read right-to-left, skipping trusted hops) or X-Real-IP and keys on the real client. A direct, non-trusted peer's forwarding headers are ignored entirely, so this opens no quota-fanning hole: an attacker connecting straight to the public :8470 cannot spoof a key. The zero value (no flag) preserves the prior RemoteAddr-only behavior exactly. Covered by ratelimit_proxy_test.go: trusted vs untrusted peers, XFF right-to-left precedence, client-prepended forgery, X-Real-IP fallback, and rejection of malformed proxy entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,17 @@ type Server struct {
|
||||
// before — so this is opt-in per deployment. Entries are matched exactly (scheme
|
||||
// + host + port); never use "*" with credentials. Set by the command from a flag.
|
||||
AllowedOrigins []string
|
||||
|
||||
// trustedProxies names the reverse proxies whose forwarding headers
|
||||
// (X-Forwarded-For / X-Real-IP) the rate limiter is allowed to believe. It
|
||||
// exists for the same-origin deployment where a single proxy (Caddy) fronts
|
||||
// the control plane: without it every proxied request would share the proxy's
|
||||
// one IP and collapse the per-IP rate limit into a single bucket for the whole
|
||||
// world. Only when the immediate peer is one of these addresses is the
|
||||
// forwarded client IP trusted; the zero value (nil) trusts nobody, preserving
|
||||
// the RemoteAddr-only behavior that predates the flag. Set by the command via
|
||||
// SetTrustedProxies. See clientIP.
|
||||
trustedProxies trustedProxyMatcher
|
||||
}
|
||||
|
||||
// Posture describes the security posture a membershipd node runs with. It is
|
||||
@@ -165,7 +176,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Per-IP rate limit runs first, ahead of auth and body reads, so a flood is
|
||||
// shed at the cheapest possible point. The health probe is exempt so liveness
|
||||
// checks are never throttled.
|
||||
if !isAuthExempt(r) && !s.limiter.allow(clientIP(r), now) {
|
||||
if !isAuthExempt(r) && !s.limiter.allow(s.clientIP(r), now) {
|
||||
writeErr(w, http.StatusTooManyRequests, "rate limit exceeded")
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user