What to do when nginx swallows the client’s IP address?

Joey Twiddle
2 min readMar 1, 2019

We have an API server running in Node.js, and we want to know the IP address and location of each user when they authenticate.

That’s pretty easy to do in express or connect:

const geoip = require('geoip-lite');  // ...  const clientIpAddress = req.connection.remoteAddress;  const geoLocation = clientIpAddress
&& geoip.lookup(clientIpAddress)
|| {};

This works fine in development, but when it comes to production, there is a problem.

The problem arises because our Node.js server is sitting behind an nginx reverse proxy which provides https, with some help from LetsEncrypt.

Our Node.js server exposes an http service on a private local port, whilst nginx exposes a public https port, and passes all traffic to the private port.

The nginx configuration for that look like this:

server {
server_name api.example.com;
listen 443 ssl http2;
listen [::]:443 ssl http2 ipv6only=on;
# ...ssl certificates here... # Pass all requests to our local Node.js server
location / {
proxy_pass http://localhost:8200;
}
}

The problem is that when we do this, the express server sees the connection coming from nginx, so it thinks the user’s IP address is 127.0.0.1 every time! nginx has swallowed the client’s IP address, so now our API server cannot see it.

The solution

The solution is to ask nginx to pass the IP address along to our Node.js server, via a custom http header.

We just need to add a singleproxy_set_header line to our nginx configuration:

    # Pass all requests to our local Node.js server
location / {
proxy_pass http://localhost:8200;
# Pass the original IP address in an http header
proxy_set_header X-Forwarded-For $remote_addr;
}

Now it’s trivial for our express app to extract that header:

  const clientIpAddress = req.headers['x-forwarded-for']
|| req.connection.remoteAddress;

Now in the production environment, the API server will use the header that nginx passed it.

And in development environments, when that header is missing, it will use the direct IP address of the client (remoteAddress).

Caveat

There is one small concern with our implementation.

If we ever move our application somewhere where it is no longer wrapped by that nginx configuration, then the header won’t be set by nginx.

And if nginx isn’t setting the header, then it is possible for the client to set the header to whatever they want!

A malicious user could provide us with a fake IP address, and our Node.js code would believe it.

For that reason, in our organisation we have an extra configuration which we toggle on only when we are behind a suitably configured nginx.

  const clientIpAddress = config.trustXForwardedFor
? req.headers['x-forwarded-for'])
: req.connection.remoteAddress;

We default that config variable to false, and add the instruction to set it true to our deployment documentation, next to the nginx config.

Some further implementation could add warnings to the server log, if we notice we are receiving empty geoips (which happens when we don’t get the real IP address).

I hope this will help you keep hold of those IP addresses.

--

--