fix(security)!: pin trusted proxies to stop client IP spoofing#231
Open
appleboy wants to merge 6 commits into
Open
fix(security)!: pin trusted proxies to stop client IP spoofing#231appleboy wants to merge 6 commits into
appleboy wants to merge 6 commits into
Conversation
- 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>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Contributor
There was a problem hiding this comment.
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
TrustedProxiesconfig 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.
- 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>
- 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>
- 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>
- 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>
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #206
AuthGate created its Gin engine with
gin.New()and never calledSetTrustedProxies, so Gin trusted all proxies —c.ClientIP()honoured client-suppliedX-Forwarded-For/X-Real-IPeven on a directly reachable server. Because the per-IP rate limiter, audit-log source IPs, and session fingerprints all derive fromClientIP(), an attacker could evade per-IP rate limits and forge audit/session IPs by rotating crafted headers.This pins a new
TRUSTED_PROXIESallowlist into the engine viaSetTrustedProxies, so by defaultClientIP()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: newTrustedProxies []stringfield, parsed fromTRUSTED_PROXIESvia the existinggetEnvSlicehelper (comma-split + trim). AvalidateTrustedProxies()step inConfig.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 aftergin.New(), plus a pureresolveTrustedProxieshelper implementing the mapping: unset →nil,*→["0.0.0.0/0","::/0"], else the list. Logs the effective mode at startup;log.Fatalfon aSetTrustedProxieserror (consistent withserveStaticFiles).docs/CONFIGURATION.md,docs/DEPLOYMENT.md,docs/RATE_LIMITING.md,.env.example,CLAUDE.md): documentTRUSTED_PROXIESand correct the nginx examples to overwriteX-Forwarded-Forwith$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 readc.ClientIP()and inherit the fix at the engine level.Resolution mapping (the contract)
TRUSTED_PROXIESSetTrustedProxies(nil)→ClientIP()==RemoteAddr(spoofed headers ignored)*SetTrustedProxies(["0.0.0.0/0","::/0"])→ trust all (legacy)10.0.0.0/8,192.168.1.5foo)Config.Validate()errors; server refuses to startAI Authorship
plan.md.Change classification
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 viaSetTrustedProxies,*wildcard escape hatch, corrected nginx docs, breaking-change note. The plan'smust not modifylist (ratelimit.go,context.go,auth.go, token/JWT/session-crypto) was respected — no changes outside the announced scope.Verification
TestConfig_Validate_TrustedProxies(CIDR/IP/wildcard accepted; invalid +*-mixed rejected),TestLoad_TrustedProxies(comma trim, wildcard parse).TestResolveTrustedProxies(nil / wildcard-expansion / list mapping).RemoteAddr=203.0.113.9,X-Forwarded-For: 1.2.3.4→ClientIP()resolves to203.0.113.9.RemoteAddr, differentX-Forwarded-For→ same bucket; second request returns429.TRUSTED_PROXIESfails fast —Config.Validate()returns an error mentioningTRUSTED_PROXIES; server does not start.Trusted proxies: none (using RemoteAddr)/wildcard (trust all)/[10.0.0.0/8 192.168.1.5], andTRUSTED_PROXIES=not-an-ipexits 1 withInvalid configuration: invalid TRUSTED_PROXIES entry.make generate && make fmt && make lint && make buildall clean (lint 0 issues); fullgo test ./...passes.Verifiability check
Security check
TRUSTED_PROXIESentries validated at startup; malformed config refuses to start.Risk & rollback
TRUSTED_PROXIESis set. Mitigation: prominent breaking-change docs, the*wildcard for an immediate (insecure) restore, and corrected nginx examples.Reviewer guide
internal/config/config.go(validateTrustedProxies),internal/bootstrap/router.go(setupTrustedProxies/resolveTrustedProxies— the wildcard expansion andnilmapping are the security-critical lines).*_test.gofiles and the docs/.env.exampleedits — test names + the resolution table convey intent.