Reverse Proxy and Remote Addr

April 04, 2018 3 minutes

Context

Days ago I hosted https://filenigma.com as an experiment on deploying WSGI applications manually. One of the features of the program is that it deletes files based on who downloads them.

We used to get the uploader or downloader IP as follows:

client_ip = request.environ['REMOTE_ADDR']

Which, worked perfectly on the development environment, as we just used the development HTTP server.

Problem

When we published the application in production, I did it using Gunicorn as the WSGI server, running behind Nginx (which serves as reverse proxy and static file server).

The files weren’t being deleted after download anymore.

Solution

In the Nginx configuration for the site, set the X-Real-IP header to $remote_addr1. Example:

server {
    ...    
    location / {
        ...
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        ...
    }
    ...
}

Then, in the program, the client IP can be easily retrieved by doing:

# Get proxied IP if it applies, otherwise get it "normally".
client_ip = request.environ.get('HTTP_X_REAL_IP', 
                                request.environ['REMOTE_ADDR'])

IT’S IMPORTANT TO NOTE, THAT IN A NON PROXIED SCENARIO THE HTTP_X_REAL_IP HEADER COULD BE FORGED, so if getting the correct IP is critical, then the method to use should be set explicitly. Behind Nginx, with the provided config it doesn’t matter, as whatever the client sent will be overwritten.

How it works?

Behind a reverse proxy, what a server sees as the “requesting client” is actually the reverse proxy itself. An application server can be behind several proxies for whatever reason. In our case, as we know that Nginx handles client requests first, we decided to set an HTTP header in the request with the client’s IP as value. This solved our problem.

Now, the reason why we left the X-Forwarded-For header too, is that as we said, the application server may be behind several proxies.

                 |
     INTERNET    |                            INTRANET
                 |
    +--------+   |    +-----------+        +-----------+      +------------+
    | Client | -----> | 1st proxy |  --->  | 2nd proxy | ---> | App server | 
    +--------+   |    +-----------+        +-----------+      +------------+
    158.x.x.95   |     195.201.x.x             -----              ------
                 |     192.168.0.10         192.168.0.20       192.168.0.42
                 |

Properly configured proxies will each add their own X-Forwarded-For with information about who’s making the request. The App server would then receive the following list of values for that header:

  1. 158.x.x.95
  2. 192.168.0.10
  3. 192.168.0.20

In cases where’s not possible to set a header like X-Real-IP in the 1st proxy and just call it a day, it’s common to get the first value from this list of values. But it comes with its own security issues, as the client may forge its own set of X-Forwarded-For headers in the original request.

Flask provides a way1 to deal with this via ProxyFix (either default or custom), but if possible, it’s better to just do what the solution in this note said.