Nginx

How to use multiple real IP headers with nginx

by , , revisited on


In some cases, you would have traffic flowing to your NGINX instance from two different cloud services / load balancers.
Some users will visit your site via service A, and other users coming from service B.

Then there is need to configure NGINX properly in order to log the correct visitor IP. When using load balancers, the connecting machine’s IP is not the end visitor’s IP address.

The standard means for communicating end visitor’s IP address is by supplying it in an HTTP header, commonly X-Forwarded-For. However, you may stumble on a case when the two different services will use different headers, e.g. X-Real-IP and CF-Connecting-IP.

NGINX is very flexible with its map and geo directives. And the real_ip_header directive can be set to a variable.

Let’s put those great features together and not without some duplication, achieve completion for this tricky task.
In the following example solution, some users access the site through Cloudflare and sometimes through DDos Guard service.

Non-working solution

I keep this obvious solution for educational purpose. It doesn’t work! See reasons why below.

The following goes into http {} section of your nginx.conf:

set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
# .. put all Cloudflare ranges like above

# ddos-guard.net range:
set_real_ip_from 186.2.160.0/24;

# DDoS Guard
geo $use_x_real_ip {
  default 0;
  186.2.160.0/24 1;
}

# Cloudflare
geo $use_x_cf_connecting_ip {
  default 0;
  103.21.244.0/22 1;
  103.22.200.0/22 1;
  103.31.4.0/22 1;
  # all other Cloudflare's ranges ...
}

map "$use_x_real_ip:$use_x_cf_connecting_ip" $real_ip_header {
  default 'X-Forwarded-For';
  "1:0" 'X-Real-Ip';
  "0:1" 'CF-Connecting-IP';
}

real_ip_header $real_ip_header;

How this is flawed:

  • The realip module does not really support variables for real_ip_header header. When we pass $real_ip_header, then that’s what it actually receives – the raw string “$real_ip_header”
  • The geo module works with $remote_addr by default. And this variable gets rewritten by realip module! So our geo maps had to use original connecting (load balancer’s) IP address, which is available in $realip_remote_addr variable

Working solution

The working solution accounts for the fact that the realip module does not support variables in real_ip_header. Thus, we have to rewrite X-Forwarded-For with the value of the relevant headers. We also fix our geo maps to look into the correct value for connecting IP.

First, make sure you have installed Headers More module. If you have installed NGINX from our repository, this can be done via:

yum install nginx-module-headers-more

Then load it at the top of your nginx.conf:

load_module modules/ngx_http_headers_more_filter_module.so;

The following goes into http {} section of your nginx.conf:

set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
# .. put all Cloudflare ranges like above

# ddos-guard.net range:
set_real_ip_from 186.2.160.0/24;

# DDoS Guard
geo $realip_remote_addr $use_x_real_ip {
  default 0;
  186.2.160.0/24 1;
}

# Cloudflare
geo $realip_remote_addr $use_x_cf_connecting_ip {
  default 0;
  103.21.244.0/22 1;
  103.22.200.0/22 1;
  103.31.4.0/22 1;
  # all other Cloudflare's ranges ...
}

map "$use_x_real_ip:$use_x_cf_connecting_ip" $real_ip {
  default $http_x_forwarded_for;
  "1:0" $http_x_real_ip;
  "0:1" $http_cf_connecting_ip;
}

more_set_input_headers "X-IP: $real_ip";
real_ip_header X-IP;

Note that in this solution it is essential to use different, arbitrary name for our rewritten real client header, e.g. real_ip_header X-IP;. Otherwise NGINX would not be able to lookup maps properly, and the result would be preference toward the default (X-Forwarded-For) even from networks where we care about other headers.


Also published on Medium.

  1. Dmitry

    Have you actually tried it yourself? Variables in real_ip_header do not work.

    Reply
  2. Peter

    Confirming what Dmitry said, this doesn’t work.

    Reply
    • Danila Vershinin

      Hi Peter & Dmitry,

      Indeed 🙂 I have played around with it and came up with something that actually works in my testing. Have a look at the working solution posted above!

      Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.