Different proxy_pass upstream depending on client ip address in Nginx

Written by - 1 comments

Published on - Listed in Nginx Internet Linux


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:

  • There is no else condition which clearly identifies an alternative action. Just leaving the second proxy_pass after the if condition becomes the "default".
  • If is evil. If you're an experienced Nginx administrator, you know about that already!

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:

  • geo: Tells Nginx to create a new "geo-map"...
  • $remote_addr: ... based on the client ip address ($remote_addr)...
  • $backend: ... and save the following value into the new variable $backend
  • default: This is a reserved keyword and specifies the default entry if nothing of the map is matched (in this scenario this means all client ip addresses which are not listed)

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.


Add a comment

Show form to leave a comment

Comments (newest first)

Mohammed Essehemy from wrote on Jul 5th, 2020:

Thanks for your helpful article


RSS feed

Blog Tags:

  AWS   Android   Ansible   Apache   Apple   Atlassian   BSD   Backup   Bash   Bluecoat   CMS   Chef   Cloud   Coding   Consul   Containers   CouchDB   DB   DNS   Database   Databases   Docker   ELK   Elasticsearch   Filebeat   FreeBSD   Galera   Git   GlusterFS   Grafana   Graphics   HAProxy   HTML   Hacks   Hardware   Icinga   Influx   Internet   Java   KVM   Kibana   Kodi   Kubernetes   LVM   LXC   Linux   Logstash   Mac   Macintosh   Mail   MariaDB   Minio   MongoDB   Monitoring   Multimedia   MySQL   NFS   Nagios   Network   Nginx   OSSEC   OTRS   Office   OpenSearch   PGSQL   PHP   Perl   Personal   PostgreSQL   Postgres   PowerDNS   Proxmox   Proxy   Python   Rancher   Rant   Redis   Roundcube   SSL   Samba   Seafile   Security   Shell   SmartOS   Solaris   Surveillance   Systemd   TLS   Tomcat   Ubuntu   Unix   VMWare   VMware   Varnish   Virtualization   Windows   Wireless   Wordpress   Wyse   ZFS   Zoneminder