Infiniroot Blog: We sometimes write, too.

Of course we cannot always share details about our work with customers, but nevertheless it is nice to show our technical achievements and share some of our implemented solutions.

Different proxy_pass upstream depending on client ip address in Nginx

Published on October 26th 2018


Sometimes there is a special situation when you need to show a different website to certain website users. Whether the situation is for clients coming from internal IP's, from specific countries (using GeoIP lookups) or bot user agents, ..., there are many use cases for such a need.This article will show the configuration in a Nginx web server, used here as a reverse proxy.

In this scenario certain client IP's needed to be (reverse) proxied to a different backend server (upstream) than the default server on which the "normal" web application is running.

The first idea how to implement the solution is (most likely) an if condition like this:

  location / {

    include /etc/nginx/proxy.conf;
    proxy_set_header X-Forwarded-Proto https;

    if ($remote_addr ~ "(172.30.123.50)|(172.30.123.55)") {
      proxy_pass https://different-upstream.example.com;
    }

    proxy_pass http://default-upstream.example.com:8080;

  }

The above config checks for the internal client IP addresses (saved in the global variable $remote_addr) 172.30.123.50 and 172.30.123.55. For these clients requests, the reverse proxy upstream is set to https://different-upstream.example.com. For all other clients, the default upstream (http://default-upstream.example.com:8080) is used.

Although this solution works, it is technically not advised for several reasons:

So if you care about a well working Nginx and a better solution, take a look at the http_geo_module and the http_map_module. This module's purpose is to create a map based on a condition with a defined target. Sounds complicated but it actually isn't. The following example will show you that.

First, above your server { } configuration, define the upstreams:

# Upstream definitions
upstream default {
  server default-upstream.example.com:8080;
}

upstream different {
  server different-upstream.example.com:443;
}

Note: The "different" upstream uses https. It is mandatory to define the port 443 in this case, otherwise default port 80 will be taken if no port is set.

Now comes the magic: The geo-map itself:

geo $remote_addr $backend {
  default http://default;
  172.30.123.50 https://different;
  172.30.123.55 https://different;
}

The map config explained:

As you can see inside the map itself, default points to value "http://default;" which refers to the upstream called "default".
On the other hand, the entries with the two internal ip addresses point to value "https://different;", which, of course, is the upstream called "different".

Within the server { } configuration the map can be called, for example inside the location /:

server {
[...]

  location / {

    include /etc/nginx/proxy.conf;
    proxy_set_header X-Forwarded-Proto https;

    proxy_pass $backend;

  }

[...]
}

Nginx now needs to access the $backend variable to determine the proxy_pass upstream. This calls the "map" entry from before which defines the wanted upstream server.

Note: It might also work to just define the full upstream URL inside the map, without having to define the upstreams first. But I didn't try that.

TL;DR: There's always a way around if, usually using a map. It's not that difficult to use, is easier to maintain than to add ip addresses to the if condition and most importantly ensures a well working Nginx config!

Update October 30th 2018: While fixed IP addresses worked with the "map" module, IP ranges (e.g. 10.10.0.0/16) did not work. For this reason I switched to the "geo" map, which was made for working with IP addresses and ranges.