Gin IP retrieval security best practices
Intro
Services usually sit behind multiple layers of proxies that forward requests. But when we need to perform identity checks, audit logging, or anything related to the request IP, how do we obtain the correct information?
This is a summary of the best security practices I learned from the Go Gin Trusted Proxies documentation.
Request Headers Can Be Easily Spoofed
In Gin, usually use c.ClientIP() to get the client IP. Internally, it automatically parses headers such as X-Forwarded-For to determine the source IP.
In typical reverse proxy setups, the original client IP is forwarded via headers like X-Forwarded-For or X-Real-IP. For example, in Nginx:
server { listen 80; server_name your-domain.com; location / { # Forward request to Gin service proxy_pass http://127.0.0.1:8080; # Pass real client IP proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # real client IP # In multi-proxy setups, append all IPs (first is real client) proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}The X-Forwarded-For header typically looks like:
X-Forwarded-For: <client>, <proxy>, …, <proxyN>
What surprised me is that Gin **trusts all proxies by default**, which means using `ClientIP()` to get the IP is **not secure**, it can be easily spoofed.
```bashcurl -s -H "X-Forwarded-For: 203.0.113.196" http://localhost:8080/pingSolution 1: Trust No One, Use RemoteAddr
Gin’s documentation mentions that Engine.SetTrustedProxies(nil) means trusting no proxies, and it will fall back to Request.RemoteAddr (retrieved from the TCP connection).
This approach is acceptable when you don’t need proxy awareness. However, if you are behind a reverse proxy, then all client IPs will appear as the proxy server’s IP.
Solution 2: Properly Configure Trusted Proxies
When a proxy is present, the correct approach is to explicitly define which IPs or CIDR ranges are trusted proxies. Only requests coming from those sources will have their X-Forwarded-For headers trusted by ClientIP().
The term “trusted proxies” can be misleading. It may sound like security is based on trusting certain IP ranges in advance.
In reality, it behaves more like an “ignore list” mechanism: it skips known proxy hops. The actual security model is still based on not trusting header data blindly.
You can refer to the internal logic of Gin’s IP resolution:
Nginx configuration (passing real IP)
proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $remote_addr;Gin configuration
r.SetTrustedProxies([]string{"172.29.0.0/16"})r.GET("/ping", func(c *gin.Context) { clientIP := c.ClientIP()})With this setup, even if an attacker spoofs the X-Forwarded-For header, Gin will ignore it unless the request originates from a trusted proxy network. Otherwise, it falls back to RemoteAddr, making spoofing ineffective.
Summary
After discussions with colleagues, I re-examined how Gin’s ClientIP() actually works under the hood and what strategies it uses to make IP resolution secure and reliable. Some earlier assumptions turned out to be incorrect and were revised after deeper understanding.