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.
```bash
curl -s -H "X-Forwarded-For: 203.0.113.196" http://localhost:8080/ping

Solution 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.

Gin Server(SetTrustedProxies: nil)Reverse Proxy (IP: 10.0.0.1)Gin Server(SetTrustedProxies: nil)Reverse Proxy (IP: 10.0.0.1)Ignore all headersuse RemoteAddr onlyIgnore all headersuse RemoteAddr onlyVisitor A (IP: 1.1.1.1)Visitor B (IP: 2.2.2.2)Send request (IP: 1.1.1.1)1Forward request (TCP source: 10.0.0.1)2Response (seen as 10.0.0.1)3Send request (IP: 2.2.2.2)4Forward request (TCP source: 10.0.0.1)5Response (still seen as 10.0.0.1)6Visitor A (IP: 1.1.1.1)Visitor B (IP: 2.2.2.2)

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:

Yes

No

No

Yes

Start

TrustedPlatform configured?

Read header directly

Return result

RemoteAddr in trusted CIDRs?

Return RemoteAddr IP

Parse RemoteIPHeaders
e.g. X-Forwarded-For

From right to left
find first non-trusted IP

Return resolved IP

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.

Further Reading