Files
RenewCMS/features/trusted_proxy_client_ip_for_rate_limiting.qa.md
T

4.8 KiB

QA procedure: trusted_proxy_client_ip_for_rate_limiting

Package: test (black-box), extending test/login_and_password_reset_rate_limiting_http_test.go's setup (mockUserRepo, stubHasher, mockMailRepo, the full useCases set, auth.NewHandler(...)).

Test infrastructure additions needed first

  1. Implement api/middleware.TrustedProxyRealIP(trustedProxies []string) func(http.Handler) http.Handler.
  2. In tests, build a chi.NewRouter() mirroring NewAuthRouter's /login and /password-reset groups: middleware.TrustedProxyRealIP(trustedProxies) then middleware.NewRateLimiter(maxAttempts, window) then the handler. chimiddleware.RealIP is not part of this router.
  3. Simulate a request's connection address by setting req.RemoteAddr = "<ip>:<port>" (host:port, matching real request shape) before calling the router. Simulate a proxy-forwarded header with req.Header.Set("X-Real-IP", "<ip>") or req.Header.Set("X-Forwarded-For", "<ip>") as needed.

Setup shared by every case

  1. Call validator.InitValidator() once.
  2. Build mockUserRepo, mockMailRepo, stubHasher, and the full useCases set, then auth.NewHandler(...).
  3. Use AUTH_RATE_LIMIT_MAX_ATTEMPTS=1 and a window covering the whole test (window=time.Hour) throughout, so a second request from the same effective key is always the one that trips 429 and no scenario needs to wait out a window.
  4. Seed mockUserRepo with user "alice", a correct password hash, so POST /v1/auth/login can return 200 where a scenario needs a non-429, non-403 baseline; a wrong-password body works equally well to prove "not rejected by rate limiting" since only the status code being not 429 matters for these cases (assert != 429 in addition to whatever status login returns for the given credentials).

HTTP-level checks

  1. trustedProxies = nil (empty). Send POST /v1/auth/login with RemoteAddr = "203.0.113.5:1" and header X-Real-IP: 198.51.100.1. Assert the response is not 429. Send a second, identical request (same RemoteAddr, same header). Assert it is 429 — proves the header had no effect; both requests hit the connection-address counter.
  2. trustedProxies = ["10.0.0.1"]. Send POST /v1/auth/login with RemoteAddr = "203.0.113.5:1" (not in the trusted list) and header X-Real-IP: 1.1.1.1. Assert not 429. Send a second request from the same RemoteAddr with header X-Real-IP: 2.2.2.2 (a different value). Assert it is 429 — proves an untrusted source can't split its own attempts across counters by varying the header.
  3. trustedProxies = ["10.0.0.1"]. Send POST /v1/auth/login with RemoteAddr = "10.0.0.1:1" and header X-Real-IP: 198.51.100.9. Assert not 429. Send a second request with the same RemoteAddr and the same header. Assert 429 — proves the trusted proxy's forwarded client is honored as the counter key.
  4. trustedProxies = ["10.0.0.1"]. Send POST /v1/auth/login with RemoteAddr = "10.0.0.1:1" and header X-Real-IP: 198.51.100.9, consuming that forwarded client's limit (assert not 429). Then send a second request with the same RemoteAddr but header X-Real-IP: 198.51.100.10. Assert not 429 — a different forwarded client behind the same trusted proxy has its own counter.
  5. trustedProxies = ["10.0.0.1"]. Send POST /v1/auth/login with RemoteAddr = "10.0.0.1:1" and header X-Forwarded-For: 198.51.100.9 (no X-Real-IP). Assert not 429. Send a second, identical request. Assert 429 — proves X-Forwarded-For is never read, even from a trusted proxy; both requests are keyed on the proxy's own address 10.0.0.1.
  6. trustedProxies = ["10.0.0.1"]. Send POST /v1/auth/login with RemoteAddr = "10.0.0.1:1" and no forwarding header at all. Assert not 429. Send a second, identical request. Assert 429 — a trusted proxy with nothing to forward falls back to its own address as the key.

Manual verification (not exercised by the Go test)

  • Real behaviour behind the documented nginx and Caddy reverse proxies (docs/deployment.md) — that each sets X-Real-IP from the actual peer address rather than a client-controlled value, and that TRUSTED_PROXIES on the RenewCMS host is set to the proxy's real address (e.g. 127.0.0.1 when co-located) — is not exercised by an in-process httptest router and should be checked by hand against a real reverse-proxy deployment.
  • config.NewSettingsFromEnv parsing TRUSTED_PROXIES from the environment (semicolon-separated, matching CORS_ALLOWED_ORIGINS) is a use-case-free config concern; if a pure Go test for internal/infrastructure/config already exists for CORS_ALLOWED_ORIGINS (test/runtime_settings_from_env_test.go), extend it with a parallel case rather than adding it here — flagged for the coder/qa stage to place correctly.