X-Forwarded-For and IP Allow-List

Filter like it's 1999

What is IP Allow-Listing

Typically when you want to access a remote resource (e.g. login to a server) you need to provide credentials. It might be a simple username/password, it could be via SSH keys, it could use Mutual TLS with client-side certificates… doesn’t really matter.

One concern is “what happens if the credential is stolen”. IP allow-listing is a way of restricting where you can use that credential from. “You must be coming from 66.228.55.57 in order to login as Stephen”.

This works quite well when restricting server-to-server communication, because servers tend to have fixed addresses (human-to-server wasn’t so useful because desktops or home ISP connection IP addresses may change frequently). Well… servers used to have static addresses, but in the modern cloudy world this isn’t always true any more. I jokingly call IP allow-listing a 20th Century solution because of this. However sometimes you still want to use it.

It’s simple, right?

In the early days, servers would communicate directly with each other, perhaps with a firewall in the middle

client server

In this model your server can see exactly the IP address of the client machine (“Oh, you are 66.228.55.57; OK, I’ll let you login as Stephen”).

Nope!

Unfortunately this type of design is not really secure or performant. You may have load balancers in the path; you may (should!) have WAFs in the path; there may be proxy servers (on both the client and server side!).

client waf lb server

Now your server only sees the IP address of the load balancer! So we need a solution for this.

X-Forwarded-For

This is a de-facto header for https communications that can be added to the request header. So in the above diagram the WAF would add a header

    X-Forwarded-For: 66.228.55.57

The load balancer would then add an extra entry for the WAFs IP address

    X-Forwarded-For: 66.228.55.57, 10.10.10.100

So now your server can look at this header and determine the original connection address.

So we can trust the left-most entry?

Unfortunately not. If the client had their own X-Forward-For header then this would appear first

    X-Forwarded-For: <client_values>, 66.228.55.57, 10.10.10.100

This might legitimately happen if the client was sitting behind a proxy server

client proxy waf lb server
    X-Forwarded-For: 192.168.100.42, 66.228.55.57, 10.10.10.100

Worse, an attacker could present their own values

    X-Forwarded-For: <attacker_provided>, 66.228.55.57, 10.10.10.100

And these fields may not be well-formatted resulting in a real mess.

    X-Forwarded-For: "ha ha I'm, attacking, you, 66.228.55.57, 10.10.10.100

So how do I use this header?

The first step is to not use it, unless you need it. If you’re in the first scenario (server to server, e.g. inside your company, or via trusted VPNs or fixed links to partner companies) where there is no WAF/LB in the path then you must not use the X-Forward-For header; it’s not trustworthy.

The next step is to start working from the right. So we start with knowing the load balancer is talking to us, and has presented the string

    X-Forwarded-For: 192.168.100.42, 66.228.55.57, 10.10.10.100

Hmm, we know “10.10.10.100” is the internal address of the WAF. So we ignore that. Hmm, “66.228.55.57”? Nope, that’s not one we know so we’ll use that. That’s the IP address we can use for allow-listing.

This process works for a longer chain of trusted services; you might have a kubernettes environment with Istio; the Istio gateway proxy may add its own X-Forward-For header.

    X-Forwarded-For: 192.168.100.42, 66.228.55.57, 10.10.10.100, 10.10.20.53

Now we see the Load Balancer address, but the same parsing process still works; start at the right and remove the addresses we know are ours.

Remember, this string may be badly formatted so ensure whatever you’re using to parse this line is robust!

Summary

IP Allow-listing may be considered a mitigating control for credential theft. You can’t always use it because of cloudy infrastructure and dynamic addresses. And intermediate infrastructure such as WAFs and load balancers make it harder to know the client IP address. The X-Forward-For header is a solution to pass this information on, but you have to be careful how you trust it!