Reverse Proxy and Remote Addr
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_addr
1. 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:
- 158.x.x.95
- 192.168.0.10
- 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.