Feature: The rate limiter keys on the real connection address, trusting a forwarded client IP only from a configured proxy As the maintainer of RenewCMS I want the login/password-reset rate limiter to key on the raw TCP peer address by default, and to honor a proxy-supplied client IP only when that peer address is a configured trusted proxy So that an untrusted client can never choose which rate-limit counter it hits by sending its own X-Real-IP or X-Forwarded-For header — which is possible today, because chimiddleware.RealIP rewrites r.RemoteAddr from either header unconditionally, for every request Background: Given POST /v1/auth/login and POST /v1/auth/password-reset are each wrapped, inside NewAuthRouter, by a middleware that runs before RateLimiter (api/middleware/rateLimit.go), which keys its counters on r.RemoteAddr And this slice replaces chimiddleware.RealIP on those two route groups with TrustedProxyRealIP(trustedProxies []string) func(http.Handler) http.Handler (api/middleware/trustedProxyRealIP.go) And TrustedProxyRealIP reads the host portion of the request's current r.RemoteAddr — the raw TCP connection's peer address, as set by net/http before any middleware runs — and compares it against the configured trustedProxies list And when that connection address is in trustedProxies and the request carries an X-Real-IP header, TrustedProxyRealIP rewrites r.RemoteAddr's host to the header's value before calling the next handler, so RateLimiter downstream keys its counter on the forwarded address instead And when that connection address is not in trustedProxies, r.RemoteAddr is left untouched and any X-Real-IP or X-Forwarded-For header present on the request is ignored entirely — it has no effect on which counter the request is recorded against And TrustedProxyRealIP never reads X-Forwarded-For, from a trusted proxy or otherwise; only X-Real-IP is consulted And when the connection address is a trusted proxy but the request has no X-Real-IP header, r.RemoteAddr is left untouched, so the request is keyed on the proxy's own address And a new environment variable, TRUSTED_PROXIES, is a semicolon-separated list of IP addresses (matching the CORS_ALLOWED_ORIGINS parsing convention), given a default of "" in cmd/server/main.go's envDefaults (not requiredEnvVars, so existing deployments keep working unchanged, always keying on the raw connection address), read by config.NewSettingsFromEnv into a new config.Settings.TrustedProxies ([]string) field, documented in .env.example and docs/configuration.md And docs/deployment.md's nginx example sets `proxy_set_header X-Real-IP $remote_addr;` (the real peer, not $proxy_add_x_forwarded_for) and its Caddy example sets the equivalent header from the real peer, and both examples note that TRUSTED_PROXIES must list the proxy's own address (typically 127.0.0.1 when the proxy and RenewCMS share a host) for the forwarded header to be honored And this is a transport-level concern with no domain or use-case involvement: neither TrustedProxyRealIP nor RateLimiter imports internal/domain, internal/infrastructure/useCases, persistence or mailer Scenario: With no trusted proxies configured, a forwarded header changes nothing Given TRUSTED_PROXIES is empty and AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 When a client connecting from "203.0.113.5" sends POST /v1/auth/login with header X-Real-IP: "198.51.100.1" And the same client immediately sends a second POST /v1/auth/login, still with X-Real-IP: "198.51.100.1" Then the first response is not rejected by rate limiting and the second is 429 — the counter is keyed on the connection address "203.0.113.5", not the header value Scenario: An untrusted connection address cannot use the header to dodge its own limit Given TRUSTED_PROXIES is "10.0.0.1" and AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 When a client connecting from "203.0.113.5" (not in TRUSTED_PROXIES) sends POST /v1/auth/login with header X-Real-IP: "1.1.1.1" And the same client sends a second POST /v1/auth/login with a different header, X-Real-IP: "2.2.2.2" Then the first response is not rejected by rate limiting and the second is 429 — both requests are counted against the connection address "203.0.113.5", regardless of the differing header values, because "203.0.113.5" is not a trusted proxy Scenario: A configured trusted proxy's forwarded header is honored Given TRUSTED_PROXIES is "10.0.0.1" and AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 When a request arrives with connection address "10.0.0.1" and header X-Real-IP: "198.51.100.9" And a second request arrives with connection address "10.0.0.1" and the same header, X-Real-IP: "198.51.100.9" Then the first response is not rejected by rate limiting and the second is 429 — both are counted against the forwarded client "198.51.100.9", not against the proxy's own address Scenario: Two real clients behind the same trusted proxy get independent counters Given TRUSTED_PROXIES is "10.0.0.1" and AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 When a request arrives with connection address "10.0.0.1" and header X-Real-IP: "198.51.100.9", consuming that forwarded client's limit And a second request arrives with connection address "10.0.0.1" and header X-Real-IP: "198.51.100.10" Then the second request is not rejected by rate limiting — it is tracked separately from "198.51.100.9" Scenario: A trusted proxy's own X-Forwarded-For header is never consulted Given TRUSTED_PROXIES is "10.0.0.1" and AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 When a request arrives with connection address "10.0.0.1" and header X-Forwarded-For: "198.51.100.9" (no X-Real-IP header) And a second such request arrives with the same connection address and the same X-Forwarded-For header Then the first response is not rejected by rate limiting and the second is 429 — both are counted against the proxy's own connection address "10.0.0.1", because X-Forwarded-For is never read Scenario: A trusted proxy with no forwarded header falls back to its own address Given TRUSTED_PROXIES is "10.0.0.1" and AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 When a request arrives with connection address "10.0.0.1" and no X-Real-IP header And a second such request arrives, also from connection address "10.0.0.1" with no X-Real-IP header Then the first response is not rejected by rate limiting and the second is 429 — both are counted against "10.0.0.1" itself