Real Client IP with Cloudflare and NestJS

x32x01
  • by x32x01 ||
  • #1
If your NestJS application sits behind Cloudflare, Caddy, a load balancer, or another reverse proxy, req.ip may not be the actual user's IP.
The problem is usually not the GeoIP database or the IP lookup library. The real issue is that your application is looking at the wrong layer of the network.

For example, your production request path might look like this:
Browser → Cloudflare → Caddy → NestJS

The NestJS server is not directly connected to the browser. It is connected to Caddy. Caddy is connected to Cloudflare, and Cloudflare is the layer that directly receives the user's request.

That distinction matters for everything from GeoIP lookups to rate limiting and security logs.
The key rule is simple: never trust a forwarded IP just because it appears in a header. Trust it only when it comes from a proxy you actually trust.
Why req.ip Can Be Wrong Behind a Reverse Proxy

When a browser connects directly to your backend, the server can usually identify the remote peer from the network connection.

But with a reverse proxy, the connection looks different.

Suppose the real user has this IP: 197.40.x.x
The request goes through: Browser → Cloudflare → Caddy → NestJS
When NestJS receives the connection, the immediate network peer may be Caddy rather than the browser.
So if your application simply takes the socket's remote address and assumes it is the client IP, you may get the proxy's IP instead of the user's IP.

There is nothing technically broken here.
The TCP connection really was made by Caddy.
The problem is the application's assumption about what that IP represents.
This can produce confusing results when you use the IP for GeoIP lookups. A user in Egypt might suddenly appear to be in another country because you looked up the proxy's address instead of the original client address.
The code can be working perfectly while the input is wrong.
Why You Should Not Blindly Trust X-Forwarded-For


A common solution is to read: X-Forwarded-For
and take an IP from it.
That can work when the proxy chain is configured correctly, but blindly trusting this header creates a security problem.
HTTP headers are client-controlled unless a trusted component in your infrastructure is responsible for setting or sanitizing them.

For example, if your backend is directly reachable by an attacker, they could send:
HTTP:
X-Forwarded-For: 8.8.8.8
If your application blindly treats that value as the user's real IP, the attacker can influence the IP your application sees.

That can affect:
  • Rate limiting
  • Security logs
  • Login history
  • Fraud detection
  • Country restrictions
  • Suspicious activity detection
  • Audit logs
  • IP-based access controls
So the important question is not: "Which IP header should I use?"

The better question is:
"Which proxy is allowed to tell my application what the original client IP was?"
Cloudflare Does Not Automatically Make CF-Connecting-IP Trustworthy

Cloudflare provides the CF-Connecting-IP header to identify the original client IP when traffic passes through Cloudflare.
That makes it useful when Cloudflare is part of your trusted infrastructure.
But the header name itself does not make it trustworthy.

If your origin server can be reached directly, someone may be able to send a request directly to the origin with a forged header such as:
HTTP:
CF-Connecting-IP: 197.40.x.x
Your application cannot determine from the header name alone whether Cloudflare actually added it.
This is why protecting the origin is an important part of handling client IPs correctly.

If Cloudflare is supposed to be your trusted edge, your infrastructure should be configured so that normal public traffic reaches the origin through the intended path rather than allowing arbitrary clients to bypass that trust boundary.
The Real Problem Is the Trust Chain

Consider this request:
Browser

Cloudflare

Caddy

NestJS

Suppose the real client IP is: 197.40.x.x
Cloudflare receives the connection directly from the user and therefore knows the original client IP.
Cloudflare then connects to Caddy.
Caddy connects to NestJS.
By the time the request reaches NestJS, the application is several network layers away from the original client.

The application therefore needs a reliable way to reconstruct the original client IP from information supplied by trusted infrastructure.
This is why simply choosing the first or last IP in a forwarded header is not a universal solution.
IP Headers Represent a Proxy Chain
With multiple proxies, X-Forwarded-For can contain several addresses.

For example:
HTTP:
X-Forwarded-For: 197.40.x.x, 203.0.113.10, 198.51.100.20
Those addresses can represent different points in the request path depending on how each proxy is configured.

If your infrastructure looks like this:
Browser

Cloudflare

Load Balancer

Caddy

NestJS
then the application may receive information representing several proxy hops.

The important point is that you should not simply decide:
"The first IP is always the user."
or:
"The last IP is always the user."

The correct interpretation depends on which proxies are trusted and how the chain is configured.

Configure Trust at the Application Layer

If NestJS is using Express, the Express trust proxy configuration is particularly important.
It determines which proxies Express considers trusted when calculating values such as: req.ip
and:
req.ips

For example, blindly enabling proxy trust without understanding your network topology can allow clients to influence the address Express considers to be the original client.
The correct configuration depends on your actual deployment.

Your application should know something like:
Browser → Cloudflare → Caddy → NestJS

rather than assuming:
Browser → NestJS

If you add another load balancer later, the trust model may need to change as well.

The same principle applies when NestJS uses Fastify. Its proxy-related configuration should match the actual infrastructure rather than being enabled blindly.

Do Not Build the Logic Around a Fixed Header Priority

A tempting implementation looks like this conceptually:
CF-Connecting-IP → otherwise X-Forwarded-For → otherwise req.ip
The problem is that this turns IP detection into a guessing game.

It essentially says:
"If I find an IP-looking value in a header, I will probably trust it."
That is not a trust model.

A better architecture is:
  1. Identify every proxy in the request path.
  2. Decide which proxies are trusted.
  3. Make sure untrusted clients cannot bypass those proxies.
  4. Configure each proxy to pass the client IP correctly.
  5. Configure the application framework to trust only the expected proxies.
  6. Use the resulting client IP consistently throughout the application.
This keeps the responsibility for reconstructing the client IP aligned with the infrastructure.
Why This Matters for Rate Limiting
GeoIP is usually the first place where this problem becomes visible, but it is not the most serious consequence.
Imagine your application implements rate limiting per IP.
If NestJS sees the proxy's IP instead of the real client IP, thousands of users could appear to come from the same address.
One user could then consume a shared rate limit and unintentionally affect other users.
The opposite problem can also happen.
If your application blindly trusts a client-controlled forwarded header, an attacker could change the IP on every request.

For example:
HTTP:
X-Forwarded-For: 1.1.1.1
Then:
HTTP:
X-Forwarded-For: 2.2.2.2
If your rate limiter trusts those values without validating the proxy chain, the attacker may effectively bypass an IP-based limit.
So you can end up with rate-limiting code that looks correct but provides little real protection.
Your Production, Staging, and Local Environments May Be Different
Another easy mistake is assuming every environment has the same network path.

Production might use: Browser → Cloudflare → Caddy → NestJS
Staging might use: Browser → Caddy → NestJS
Local development might use: Browser → NestJS
If the application assumes Cloudflare is always present, the same IP-detection logic may behave differently across environments.
Your IP handling should therefore be based on the actual infrastructure for each environment.
This is especially important when testing security features locally. A configuration that makes sense behind several trusted proxies may be unnecessary or incorrect when the application is running directly.
A Practical Mental Model

Instead of thinking: "I need to find the real IP from a header."
think: "I need to identify the original client through a trusted chain of proxies."
That small change in thinking makes the architecture much easier to reason about.

For every request, ask:
  • Who connected directly to my application?
  • Which proxy connected to that server?
  • Which proxy received the request from the previous layer?
  • Which components are trusted?
  • Can a client bypass those components?
  • Which component is responsible for supplying the original client IP?
  • Does the framework know which proxies it should trust?
Once those questions have clear answers, GeoIP and IP-based security features become much easier to implement correctly.
Final Takeaway

Getting the user's IP behind Cloudflare, Caddy, and NestJS is not simply a matter of choosing between CF-Connecting-IP, X-Forwarded-For, and req.ip.
The real issue is trust.

Your backend needs to know which proxies are trusted, how the request travels through them, and whether clients can bypass that chain.
Once the infrastructure and proxy trust configuration are correct, the application can safely use the resulting client IP for GeoIP lookups, rate limiting, security logs, fraud detection, and other IP-based features.
The most dangerous approach is not having no IP detection at all.
It is having IP detection that looks correct, works without errors, and quietly trusts the wrong source.
Frequently Asked Questions
Why does NestJS show the proxy IP instead of the user's IP?

Because NestJS is receiving the network connection from the reverse proxy, not directly from the user's browser. The application needs a correctly configured trusted proxy chain to determine the original client IP.
Is X-Forwarded-For safe to trust?
Not by itself. It should only be trusted when the request has passed through proxies you trust and the origin cannot be bypassed in a way that allows attackers to inject or manipulate the forwarded IP information.
Can I always use CF-Connecting-IP with Cloudflare?
It is useful when Cloudflare is your trusted edge, but the header should not be treated as trustworthy merely because of its name. Your origin and proxy configuration must prevent untrusted clients from spoofing the same information.
Should I always take the first IP from X-Forwarded-For?
No. The correct IP depends on your proxy chain and which proxies are trusted. With multiple proxies, you need to understand how each layer adds and forwards client IP information.
Why is an incorrect client IP a security problem?
Because IP addresses are often used for rate limiting, audit logs, fraud detection, access restrictions, suspicious activity detection, and other security decisions. Using the wrong or attacker-controlled IP can make these controls unreliable.
 
Similar threads
x32x01
Replies
0
Views
5
x32x01
x32x01
x32x01
Replies
0
Views
55
x32x01
x32x01
x32x01
Replies
0
Views
62
x32x01
x32x01
x32x01
Replies
0
Views
65
x32x01
x32x01
x32x01
Replies
0
Views
78
x32x01
x32x01
Forum Statistics
Threads
1,012
Messages
1,017
Members
15
Latest Member
Mohamed
Back
Top