Skip to content

fix(security)!: pin trusted proxies to stop client IP spoofing#231

Open
appleboy wants to merge 6 commits into
mainfrom
worktree-206
Open

fix(security)!: pin trusted proxies to stop client IP spoofing#231
appleboy wants to merge 6 commits into
mainfrom
worktree-206

Conversation

@appleboy
Copy link
Copy Markdown
Member

@appleboy appleboy commented May 31, 2026

Summary

Closes #206

AuthGate created its Gin engine with gin.New() and never called SetTrustedProxies, so Gin trusted all proxies — c.ClientIP() honoured client-supplied X-Forwarded-For / X-Real-IP even on a directly reachable server. Because the per-IP rate limiter, audit-log source IPs, and session fingerprints all derive from ClientIP(), an attacker could evade per-IP rate limits and forge audit/session IPs by rotating crafted headers.

This pins a new TRUSTED_PROXIES allowlist into the engine via SetTrustedProxies, so by default ClientIP() returns the real TCP peer (RemoteAddr) and forwarding headers are ignored. Operators behind a real proxy opt in by listing proxy CIDRs/IPs; TRUSTED_PROXIES=* restores the legacy trust-all behaviour for migration.

What changed (production)

  • internal/config/config.go: new TrustedProxies []string field, parsed from TRUSTED_PROXIES via the existing getEnvSlice helper (comma-split + trim). A validateTrustedProxies() step in Config.Validate() accepts empty (trust none), a single * (trust all), or a list of CIDRs/bare IPs; malformed entries — or * mixed with other entries — fail fast at startup.
  • internal/bootstrap/router.go: setupTrustedProxies(r, cfg) called right after gin.New(), plus a pure resolveTrustedProxies helper implementing the mapping: unset → nil, *["0.0.0.0/0","::/0"], else the list. Logs the effective mode at startup; log.Fatalf on a SetTrustedProxies error (consistent with serveStaticFiles).
  • Docs (docs/CONFIGURATION.md, docs/DEPLOYMENT.md, docs/RATE_LIMITING.md, .env.example, CLAUDE.md): document TRUSTED_PROXIES and correct the nginx examples to overwrite X-Forwarded-For with $remote_addr (was $proxy_add_x_forwarded_for, which appends and leaves spoofed values intact).

The rate-limiter key-getter, RequestContextMiddleware, and the session-fingerprint middleware are intentionally untouched — they read c.ClientIP() and inherit the fix at the engine level.

Resolution mapping (the contract)

TRUSTED_PROXIES Behaviour
unset / empty (default) SetTrustedProxies(nil)ClientIP() == RemoteAddr (spoofed headers ignored)
* SetTrustedProxies(["0.0.0.0/0","::/0"]) → trust all (legacy)
10.0.0.0/8,192.168.1.5 trust only the listed proxies
invalid entry (e.g. foo) Config.Validate() errors; server refuses to start

AI Authorship

  • AI was used.
    • Tool / model: Claude Code (Opus 4.8), executing a pre-written plan.md.
    • AI-authored files: all files in this PR.
    • Human line-by-line reviewed: author to confirm before merge.

Change classification

  • Core code — engine-level IP resolution feeding rate limiting, audit logging, and session fingerprints. Failure is system-wide (security control). Needs line-by-line review.

Plan reference

Plan goal: pin trusted proxies so forwarding headers can't spoof the client IP. Done = TRUSTED_PROXIES (CIDR/IP list, default trust-none) wired via SetTrustedProxies, * wildcard escape hatch, corrected nginx docs, breaking-change note. The plan's must not modify list (ratelimit.go, context.go, auth.go, token/JWT/session-crypto) was respected — no changes outside the announced scope.

Verification

  • Unit tests — config: TestConfig_Validate_TrustedProxies (CIDR/IP/wildcard accepted; invalid + *-mixed rejected), TestLoad_TrustedProxies (comma trim, wildcard parse).
  • Router tests — TestResolveTrustedProxies (nil / wildcard-expansion / list mapping).
  • 3 e2e tests (1 happy path + 2 error/abuse):
    1. Default ignores spoofed headerRemoteAddr=203.0.113.9, X-Forwarded-For: 1.2.3.4ClientIP() resolves to 203.0.113.9.
    2. Spoofing can't multiply rate-limit quota — two requests, same RemoteAddr, different X-Forwarded-For → same bucket; second request returns 429.
    3. Invalid TRUSTED_PROXIES fails fastConfig.Validate() returns an error mentioning TRUSTED_PROXIES; server does not start.
    • Plus: trusted-proxy honours the header, and wildcard restores legacy behaviour.
  • Stress / soak test: N/A (no long-running/async code).
  • Manual verification: ran the built binary across all four modes — startup logs Trusted proxies: none (using RemoteAddr) / wildcard (trust all) / [10.0.0.0/8 192.168.1.5], and TRUSTED_PROXIES=not-an-ip exits 1 with Invalid configuration: invalid TRUSTED_PROXIES entry. make generate && make fmt && make lint && make build all clean (lint 0 issues); full go test ./... passes.

Verifiability check

  • Inputs/outputs documented in code comments and the resolution table above.
  • Reviewer can judge correctness from the test names + the small production diff.
  • Failures surface via the startup log line stating the effective trusted-proxy mode.

Security check

  • No secrets in the diff (test-only fixed IPs).
  • External input (forwarding headers) validated — only honoured from allowlisted proxies; default trusts none.
  • TRUSTED_PROXIES entries validated at startup; malformed config refuses to start.
  • Errors don't leak internals (config-validation messages name only the offending value).

Risk & rollback

  • Risk: Breaking change to default IP resolution. Deployments behind a proxy that previously "worked" (spoofable) will see client IPs collapse to the proxy's address after upgrade until TRUSTED_PROXIES is set. Mitigation: prominent breaking-change docs, the * wildcard for an immediate (insecure) restore, and corrected nginx examples.
  • Rollback: revert the commit — behaviour returns to trust-all. No schema/data migration involved.

Reviewer guide

  • Read carefully: internal/config/config.go (validateTrustedProxies), internal/bootstrap/router.go (setupTrustedProxies / resolveTrustedProxies — the wildcard expansion and nil mapping are the security-critical lines).
  • Spot-check OK: the *_test.go files and the docs/.env.example edits — test names + the resolution table convey intent.

- Add TRUSTED_PROXIES config parsed from a comma-separated CIDR/IP list and validated at startup
- Wire SetTrustedProxies into the router so client-supplied forwarding headers are ignored by default
- Resolve unset to trust-none, wildcard to trust-all, and a list to trust-only-listed proxies
- Log the effective trusted-proxy mode on startup
- Document overwriting X-Forwarded-For with the direct peer in the nginx examples
- Add config validation and router ClientIP behaviour tests

BREAKING CHANGE: Default client IP resolution now trusts no proxies, so
c.ClientIP() returns the direct TCP peer instead of honouring X-Forwarded-For /
X-Real-IP. Deployments behind a reverse proxy must set TRUSTED_PROXIES to the
proxy address(es) to recover the real client IP for rate limiting, audit logs,
and session fingerprints. Set TRUSTED_PROXIES=* to restore the legacy
trust-all behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 31, 2026 15:07
@codecov
Copy link
Copy Markdown

codecov Bot commented May 31, 2026

Codecov Report

❌ Patch coverage is 92.85714% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/bootstrap/router.go 75.00% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR secures client IP resolution by configuring Gin trusted proxies from a new TRUSTED_PROXIES setting, preventing spoofed forwarding headers from affecting rate limits, audit IPs, and session fingerprints by default.

Changes:

  • Adds TrustedProxies config loading and validation for empty/default, wildcard, CIDR, and bare IP values.
  • Applies trusted proxy resolution during router setup via SetTrustedProxies.
  • Adds tests and updates operator documentation for secure reverse-proxy configuration.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/config/config.go Adds config field, env loading, and validation for trusted proxy entries.
internal/config/config_test.go Covers validation and env parsing for TRUSTED_PROXIES.
internal/bootstrap/router.go Wires trusted proxy setup into Gin initialization.
internal/bootstrap/trustedproxies_test.go Adds resolver and end-to-end client IP/rate-limit behavior tests.
docs/RATE_LIMITING.md Documents secure proxy header handling for rate limiting.
docs/DEPLOYMENT.md Updates nginx guidance and trusted proxy setup instructions.
docs/CONFIGURATION.md Documents the new TRUSTED_PROXIES option and behavior.
.env.example Adds example trusted proxy configuration guidance.
CLAUDE.md Notes the new security-sensitive configuration option.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/CONFIGURATION.md Outdated
Comment thread internal/config/config_test.go
- Scope TRUSTED_PROXIES in the default-unset test so an ambient env var or local .env cannot make it flaky
- Overwrite X-Forwarded-For with the direct peer in the troubleshooting nginx snippet to match the new guidance

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

- Export IsWildcardProxies so router proxy resolution reuses the single wildcard definition instead of duplicating the len==1 check

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Comment thread .env.example Outdated
Comment thread docs/CONFIGURATION.md Outdated
Comment thread docs/CONFIGURATION.md Outdated
Comment thread docs/RATE_LIMITING.md Outdated
Comment thread docs/DEPLOYMENT.md Outdated
Comment thread docs/TROUBLESHOOTING.md Outdated
- Replace broad RFC1918 ranges in TRUSTED_PROXIES examples with exact proxy/LB IPs
- Warn that trusting a wide range lets any host in it spoof the client IP via forwarding headers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread docs/RATE_LIMITING.md Outdated
- Replace the broad 10.0.0.0/8 troubleshooting example with exact proxy IPs to match the surrounding guidance

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

- Clarify that AuthGate honours both X-Forwarded-For and X-Real-IP from
  a trusted proxy, so the reverse-proxy guidance must overwrite both
- Add the proxy_set_header X-Real-IP example alongside X-Forwarded-For
  so overwriting only one header cannot leave the other client-spoofable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: pin trusted proxies so forwarding headers can't spoof client IP

2 participants