NGINX

How to use multiple real IP headers with NGINX

by , , revisited on


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

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. But the real_ip_header directive does not support variables.

In the following example solution, some users access the site through Cloudflare and sometimes through DDos Guard service.

We have to account for the fact that the realip module does not support variables in real_ip_header. The solution involves:

  • Creating geo maps designating values used in deciding which HTTP headers to use for real IP value
  • Setting up a single synthetic input HTTP header (not actually coming from CDN) with the value of the real IP, for real_ip_header directive.

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:

real_ip_recursive on;
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;

How it works

For each request:

  • geo $realip_remote_addr marks whether the TCP peer belongs to DDoS-Guard or Cloudflare
  • map uses those flags to choose the right header: X-Real-IP, CF-Connecting-IP, or the default X-Forwarded-For – single variable $real_ip is set up to tell NGINX which HTTP headers to use for real IP
  • in the REWRITE phase, more_set_input_headers "X-IP: $real_ip"; turns that variable into a synthetic header X-IP

The more_set_input_headers runs at the end of the REWRITE phase, while real_ip_header is enforced by the realip module in the later PREACCESS phase. This is why setting a synthetic input header in rewrite still affects remote_addr correctly.

Caveats

X-IP should be an arbitrary synthetic header name to make it work; do not use any of the actual header names your CDNs use for real IP designation. Otherwise, NGINX will finalize real IP detection at POST_READ phase, and the synthetic header won’t be used.

In other words, it is essential to use different, arbitrary name for our rewritten real client header, e.g. real_ip_header X-IP;.

  1. Andrew

    This won’t work since more_set_input_headers happens in the rewrite phase, but the realip module is evaluated in the post read phase.

    Reply
    • Satheesh

      Yep, tested and ended up facing the issue where, headers created are not read by the setting “real_ip_header”.

      Reply
      • Danila Vershinin

        The realip module may run at two distinct phases: POST_READ and PREACCESS. Why it may not work for you, is that your specific configuration causes the evaluation at the POST_READ to change IP address, then the evaluation at PREACCESS phase will not happen.

        Make sure to use a truly synthetic header, more details in Caveats section.

        Reply
  2. hell

    nice for ip spoof (X-Forwarded-For)

    Reply
    • Danila Vershinin

      The IP spoof is not possible. For realip to apply the provided IP, the connecting IP has to match IP ranges that you trust via set_real_ip_from directives, even for the default X-Forwarded-For header.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

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