Skip to main content
Where do your visitors come from? Your access logs have an answer on every line: an IP address. Whether that address is your visitor’s, or a machine somewhere in the middle, depends on your setup. On Upsun, that address comes from the X-Client-IP request header. Most of the time it holds the real client IP without you configuring anything. This post is about the “most of the time” part.

Why the remote IP isn’t the client

The obvious candidate for the client IP is the remote address of the TCP connection. That stopped being reliable once everything sat behind load balancers and proxies. Between your visitor and your application sits a chain: edge routers, load balancers, and often a CDN. Each hop terminates the connection and opens a new one, and each new connection carries the IP of the hop, not the visitor. By the time a request reaches your container, the remote address is the last machine that touched it. That’s why proxies pass the original address along in a header instead. On Upsun, that header is X-Client-IP, set by our router. Its value is what your application sees as the remote address, and what ends up in /var/log/access.log. What goes into it depends on where the request comes from.

A chain of trust

When a request reaches an Upsun region directly from the client, there’s nothing to figure out. The remote address is the client, and that’s what goes into X-Client-IP. It gets interesting behind a CDN. We support 3 of them: Fastly, Cloudflare, and CloudFront. What these 3 have in common is an API that publishes the IP ranges of their points of presence. When a connection reaches our region, we check the remote address against those ranges. A match means the connection really comes from that CDN. And once we know that, we can trust what the CDN tells us. Each of them forwards the real client IP in its own request header, such as Fastly-Client-IP or CF-Connecting-IP. We take that value and put it in X-Client-IP. Your application sees the visitor’s real address, and the access log records it, even though the TCP connection comes from a CDN datacenter. The verification step is what carries the trust: anyone can send a request with a Fastly-Client-IP header, but only Fastly can send one from a Fastly IP address.

When your CDN isn’t on the list

Akamai doesn’t publish its IP ranges through an API. Without the ranges, there’s no way to verify that a connection comes from Akamai, and without that verification, its True-Client-IP header is a claim anyone can make. The chain of trust can’t be built, and X-Client-IP falls back to the remote address: an Akamai edge server. Your access log fills up with Akamai datacenter IPs. The same goes for any CDN that doesn’t expose its ranges. We can’t fix the platform access log in that situation. But you can still get the real client IP into your own logs, with 2 rules to follow.

Rule 1: set the header unconditionally

At the CDN level, configure a header that carries the client IP. It can be the CDN’s own header or a custom one. The important part: the CDN must set it on every request, overwriting anything already there. Never “set if missing”. The reason is the same trust argument as before. If the header is only set sometimes, then sometimes its value comes from the client, and a client can put anything in there. Overwriting on every request removes that possibility.

Rule 2: check who’s talking to you

In your application, before reading the header, check that the remote address belongs to your CDN. Most CDNs give their customers the list of ranges, even when there’s no public API for it:
  from ipaddress import ip_address, ip_network

  CDN_RANGES = [ip_network(r) for r in RANGES_FROM_YOUR_CDN]

  def client_ip(request):
      remote = ip_address(request.remote_addr)
      if any(remote in network for network in CDN_RANGES):
          return request.headers.get("True-Client-IP", str(remote))
      return str(remote)
Skip this check, and anyone who finds your origin’s address can send requests directly, header included, and your logs record whatever IP they chose. And origin addresses aren’t secret: they leak through DNS history, certificate transparency logs, and plain guessing. There’s one setup where the check is redundant: origin restriction. If nothing but your CDN can reach your project, because you set up HTTP access control with your CDN’s ranges, or mTLS with an Origin CA certificate from your CDN, then every request that reaches your app already comes from the CDN. In that case, read the header directly. Either way, the last step is yours: log the resolved client IP from your application, in your own access log. That gives you real visitor addresses even behind a CDN we can’t verify.

The takeaway

On Upsun, X-Client-IP holds the real client IP when requests arrive directly, or through Fastly, Cloudflare, or CloudFront. Behind anything else, it holds the last hop, and getting the real address into your logs is on you: one header set unconditionally at the CDN, one check on the remote address in your app, or origin restriction instead of the check. None of it is hard. But get one step wrong and nothing breaks: the logs keep filling up, with the wrong IPs in them.
Last modified on July 14, 2026