Restricting IP Addresses with nginx reverse proxy

My ERPNext is running behind an nginx reverse proxy and it is working just fine. However, I would like to restrict the IP addresses that can access the system. Yet when I look at any of the security settings of the users, the last IP address is always that of the reverse proxy.

The nginx config is forwarding the IP address, yet ERPNext does not seem to trust the X-Forwarded-For or X-Real-IP headers being forwarded. Is there a way to change that?

location / {
    proxy_pass http://192.168.7.28:80;
    proxy_redirect off;
    
    # Improved IP forwarding headers
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Port $server_port;
    
    proxy_set_header Host $host;
    proxy_set_header Range $http_range;
    proxy_set_header If-Range $http_if_range;
    
    add_header X-Frame-Options SAMEORIGIN;
    
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    client_max_body_size 512M;
}

And is there a way to set ERPNext to only trust the X-Forwarded-For header when the request comes from the trusted nginx proxy?

Why not use firewall rules to limit access?

Well, I’m not saying there are other viable solutions to a/this problem. But ERPNext does provide that functionality, so I was wondering if and how I can configure it.

From ChatGPT: -

What you’re running into is the classic “reverse proxy hides client IP” issue. ERPNext (really Frappe, the framework under it) reads the request IP from the WSGI server (Gunicorn/uwsgi) which, by default, only sees the proxy’s IP.

You’re correct that even though nginx is sending X-Forwarded-For / X-Real-IP, Frappe won’t automatically trust them. You have to explicitly configure it.


1. Make Gunicorn / Frappe trust the proxy headers

Frappe sits on top of Werkzeug, which supports ProxyFix middleware. This tells the app: “when requests come from certain proxies, treat their headers as authoritative for the real client IP/host”.

You can enable it via common_site_config.json (in your sites/ directory):

{
  "proxy_fix": true,
  "proxy_fix_x_for": 1,
  "proxy_fix_x_proto": 1,
  "proxy_fix_x_host": 1,
  "proxy_fix_x_port": 1,
  "proxy_fix_x_prefix": 1
}
  • proxy_fix: true → turns it on.
  • proxy_fix_x_for: 1 → trust the first IP in X-Forwarded-For.
  • You can adjust the numbers if your chain has multiple proxies.

After editing, restart the bench (bench restart).


2. Only trust from your nginx proxy

If you want to avoid spoofing (a malicious client sending fake X-Forwarded-For directly), you need to combine ProxyFix with trusted proxy filtering at nginx:

In nginx:

Make sure only nginx sets these headers and strips any from the client:

location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;

    # Drop headers coming from the client
    proxy_set_header X-Forwarded-Proto $scheme;
}

This way, ERPNext will only see headers injected by your reverse proxy.


3. Restricting access by IP

If your goal is to allow only specific IPs to reach ERPNext, the best layer for that is nginx itself, not ERPNext:

location / {
    allow 192.168.1.0/24;   # your office LAN
    allow 203.0.113.42;     # specific public IP
    deny all;

    proxy_pass http://frappe;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
}

That way, traffic never even reaches ERPNext if the IP isn’t allowed.


:white_check_mark: Summary

  • Enable proxy_fix in ERPNext’s common_site_config.json so it respects X-Forwarded-For.
  • Strip/overwrite those headers in nginx so only trusted values reach ERPNext.
  • Do IP whitelisting at nginx if you want to restrict access completely.
1 Like

Thank you. While I got the initial hint also by AI, your reply let me follow up on this further with the help of AI.

Point 1 did unfortunately not solve the issue. However, since ERPNext also uses nginx for the webserver, I checked further with AI and got a few more hints.

  1. In the nginx config of the site, it suggested to replace a specific line about the proxy_set_header.
location @webserver {
    proxy_http_version 1.1;
    # Remove or change this line: proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Use this instead
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Frappe-Site-Name erp.xgt.com.hk;
    proxy_set_header Host $host;
    proxy_set_header X-Use-X-Accel-Redirect True;
    proxy_read_timeout 120;
    proxy_redirect off;
    proxy_pass  http://frappe-bench-frappe;
}

That got me a small step further. So now I can see the last IP used as the one being from the local router in the subnet. I have to check if the router is doing some kind of masking of the IP address.

While that is not the reverse proxy anymore, it is still not the IP address from the actual client. DeepSeek then further suggested changes in the /etc/nginx/nginx.conf however these also didn’t improve the required changes.

I might experiment a bit further with your suggested changes to the reverse proxy and restrict access there. However, I don’t want to restrict access to the complete server, but only the app part, as ERPNext is also configured to show the website publicly.

Frappe production server typically has Nginx running. If you have an additional Nginx proxy server in front, let’s identify them as frappe-nginx and proxy-nginx, respectively.

If your proxy-nginx IP address is 192.168.1.5, add the following to frappe-bench/config/nginx.conf

#adding info to pass client real IP not IP from proxy
set_real_ip_from 192.168.1.5;
real_ip_header X-Forwarded-For;

Additionally, on the frappe server I always add the proxy-nginx IP address in the /etc/hosts file with all the domains served by frappe.

**frappe@f15dev**:**~**$ cat /etc/hosts

127.0.0.1 localhost

127.0.1.1 f15dev

192.168.1.5 wikidev.site.com erpdev.mydomain.com

I need the above config or the pdf print button fails with com error.

I have a single Nginx proxy server that directs traffic to multiple Frappe and other web servers. Nginx rocks, as does Frappe Framework!

Here’s an example of my host config file on the nginx-proxy server:

server {
server_name erp.domain.com;

root /var/www/html;
index index.html index.htm index.nginx-debian.html;


location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_pass http://10.110.20.81:80;
proxy_read_timeout 90;

}

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/erp.domain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/erp.domain.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}
server {
    if ($host = erp.domain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


listen 80;
server_name erp.domain.com;
    return 404; # managed by Certbot


}

As you can see I perform SSL termination on the proxy-nginx server, not frappe-nginx.

When terminating SSL via proxy-nginx, I add the following line to all site_config.json files.

 "hostname": "https://wikidev.site.com",

This helps write the correct url for things like webhooks and more.

1 Like