How to Implement IP-based Access Control in Nginx

How to Implement IP-based Access Control in Nginx

At some point, almost every server administrator needs to restrict access to something — an admin panel, a staging environment, an internal API, or a monitoring dashboard — to a specific set of IP addresses. Nginx makes this surprisingly straightforward with its built-in access module, and in this guide I’m going to walk through everything from the basics of allow/deny to more advanced patterns like combining IP rules with geo-based blocking and rate limiting.

I’ll be honest: IP-based access control is not a silver bullet for security. IP addresses can be spoofed at the network layer in certain attack scenarios, and legitimate users behind shared NAT or VPNs can get inconvenienced. But it remains one of the most effective first lines of defense for internal tooling, and it pairs well with other layers like authentication and TLS client certificates.

What IP-based Access Control Actually Does

At its core, Nginx evaluates a list of allow and deny rules in the order they appear in your configuration, for every incoming connection. The first matching rule wins. If no rule matches, the default behavior is to deny access (unless you’ve explicitly allowed all with allow all; somewhere in the chain).

This module is called ngx_http_access_module and it’s compiled into Nginx by default in virtually every distribution’s package, so you generally don’t need to install anything extra.

Requirements

  • Nginx installed and running (check with nginx -v).
  • Sudo/root access to edit config files under /etc/nginx/.
  • Knowledge of the IP addresses or CIDR ranges you want to allow or block. If you’re restricting access to your own office or home, run curl ifconfig.me from that location to find the public IP you’ll be connecting from.
  • A basic server block already configured for the site you want to protect.

Basic Syntax: allow and deny

The two directives you’ll use constantly:

allow 203.0.113.10;
deny all;

This allows exactly one IP address and denies everyone else. You can specify individual IPs, CIDR ranges, or the keyword all.

location /admin {
    allow 203.0.113.0/24;   # allow an entire subnet
    allow 198.51.100.5;     # allow a specific additional IP
    deny all;               # deny everyone else
}

Order matters. Nginx evaluates rules top to bottom and stops at the first match, so putting deny all; before your allow lines would block everyone, including the IPs you meant to allow.

Applying Rules at Different Levels

You can place allow/deny directives inside http, server, or location blocks, depending on how broad you want the restriction to be.

Restricting an entire site:

server {
    listen 80;
    server_name internal.example.com;

    allow 10.0.0.0/8;
    deny all;

    location / {
        root /var/www/internal;
    }
}

Restricting just one path, like an admin panel or status page:

server {
    listen 80;
    server_name example.com;

    location / {
        root /var/www/example.com;
    }

    location /admin {
        allow 203.0.113.10;
        deny all;

        proxy_pass http://127.0.0.1:8080;
    }

    location = /nginx_status {
        stub_status;
        allow 127.0.0.1;
        deny all;
    }
}

That last block is a common real-world pattern — exposing Nginx’s stub_status module only to localhost, so external monitoring tools have to go through an authenticated proxy or SSH tunnel to reach it.

Combining IP Rules with Basic Authentication

For anything genuinely sensitive, I like to layer IP restriction with HTTP basic auth. Even if someone spoofs or shares an allowed IP, they’d still need valid credentials.

sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd admin
location /admin {
    allow 203.0.113.0/24;
    deny all;

    auth_basic "Restricted Area";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://127.0.0.1:8080;
}

With satisfy all; (the default when both allow/deny and auth_basic are present), a client must pass both checks. If you wanted “either IP allowed OR valid credentials,” you’d explicitly set satisfy any; instead.

location /admin {
    satisfy any;

    allow 203.0.113.0/24;
    deny all;

    auth_basic "Restricted Area";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

Using the geo Module for Larger Rule Sets

When you have dozens or hundreds of IPs to manage, hardcoding them all into allow lines gets messy fast. The geo module lets you build a lookup map, which is cleaner and often faster to evaluate.

http {
    geo $allowed_ip {
        default         0;
        10.0.0.0/8      1;
        203.0.113.0/24  1;
        198.51.100.42   1;
    }

    server {
        listen 80;
        server_name internal.example.com;

        location / {
            if ($allowed_ip = 0) {
                return 403;
            }
            root /var/www/internal;
        }
    }
}

This approach scales better and keeps your server blocks tidy, especially if you maintain the IP list in a separate included file:

geo $allowed_ip {
    default 0;
    include /etc/nginx/allowed_ips.conf;
}
# /etc/nginx/allowed_ips.conf
10.0.0.0/8 1;
203.0.113.0/24 1;
198.51.100.42 1;

This way, updating the allowlist is a matter of editing one small file and reloading Nginx, without touching your main server configuration.

Handling Real Client IPs Behind a Load Balancer or CDN

If Nginx sits behind a load balancer, reverse proxy, or CDN like Cloudflare, the IP Nginx sees by default is the balancer’s IP, not the real client’s. You need the ngx_http_realip_module to correctly identify clients for allow/deny to work as intended.

http {
    set_real_ip_from 10.0.0.0/8;         # trusted proxy range
    real_ip_header    X-Forwarded-For;
    real_ip_recursive on;

    server {
        listen 80;
        location /admin {
            allow 203.0.113.10;
            deny all;
        }
    }
}

set_real_ip_from should be set to the IP ranges of your trusted proxies (your load balancer, or Cloudflare’s published IP ranges if that’s your setup). Never trust X-Forwarded-For blindly from the public internet, since it’s a client-controllable header — only trust it when it comes from a proxy you actually control.

Complete Example Configuration

Here’s a realistic setup combining several of these techniques — a public site with a protected admin area, a monitoring endpoint locked to localhost, and IP restriction backed by a maintained include file:

http {
    set_real_ip_from 10.0.0.0/8;
    real_ip_header    X-Forwarded-For;
    real_ip_recursive on;

    geo $office_ip {
        default 0;
        include /etc/nginx/office_ips.conf;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            root /var/www/example.com;
            index index.html;
        }

        location /admin {
            satisfy all;

            if ($office_ip = 0) {
                return 403;
            }

            auth_basic "Restricted Area";
            auth_basic_user_file /etc/nginx/.htpasswd;

            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        location = /nginx_status {
            stub_status;
            allow 127.0.0.1;
            deny all;
        }
    }
}

Testing Your Configuration

Always validate before reloading:

sudo nginx -t
sudo systemctl reload nginx

To verify from an allowed machine:

curl -I http://example.com/admin

You should get a 200 or a basic auth prompt (401), not a 403.

From a disallowed machine (or by spoofing headers if you’re testing locally with a proxy tool), you should get a 403 Forbidden.

To simulate a specific source IP without owning that IP, the most reliable method is testing from an actual machine in that network — VPN into the office network, or SSH into a jump box within the allowed range, then curl from there.

If you’re behind a CDN or load balancer and testing real-IP handling, check what Nginx actually sees:

location /whoami {
    default_type text/plain;
    return 200 "remote_addr=$remote_addr\n";
}

Compare that output against your actual public IP to confirm real_ip_header is working correctly.

Troubleshooting Common Issues

Everyone gets 403’d, including allowed IPs. Check rule order — a deny all; placed before your allow lines will short-circuit everything. Also double check you’re not sitting behind a proxy that’s masking the real client IP; without realip configured, $remote_addr will be the proxy’s IP, not the client’s.

IP restriction doesn’t seem to apply at all. Make sure the allow/deny block is actually inside the location (or server) you think it is — a common mistake is placing rules in a sibling location that never matches the requested path.

Legitimate users behind NAT get blocked. Large organizations, mobile carriers, and some ISPs share a small number of public IPs across many users via NAT. If you’re restricting to an office IP and remote employees on shared connections get blocked, consider a VPN-based approach instead so everyone shares one predictable, controlled exit IP.

Rules work over HTTP but not HTTPS, or vice versa. Make sure you’ve replicated the allow/deny block in both the :80 and :443 server blocks if you’re running separate blocks for HTTP and HTTPS — a common oversight when a redirect-only HTTP block exists alongside a fuller HTTPS block.

Security Considerations

IP-based restriction should be treated as one layer, not your entire security model. Keep these points in mind:

  • IP addresses can change (dynamic residential IPs) or be spoofed in some network configurations, particularly if you’re not careful with UDP-based protocols or misconfigured proxies.
  • Always combine IP restriction with authentication for anything truly sensitive — admin panels, database management tools, CI/CD dashboards.
  • Regularly audit your allowlists. Stale entries for former employees, decommissioned servers, or old office locations are a common source of unnecessary exposure.
  • If you’re allowlisting cloud provider IP ranges (for internal service-to-service traffic, for instance), remember those ranges can be reassigned to other customers over time — pin down security groups or VPC-level controls as an additional layer rather than relying purely on Nginx-level IP rules.
  • Use set_real_ip_from carefully. If you trust X-Forwarded-For from an untrusted source, an attacker can simply set that header themselves and bypass your IP restrictions entirely.

Performance Tips

  • For large allowlists (hundreds or thousands of entries), geo maps are more efficient than long chains of allow directives, since Nginx builds an internal radix tree for IP lookups rather than evaluating rules sequentially.
  • Keep location-level restrictions scoped as narrowly as possible — don’t apply expensive geo lookups globally if only one path needs them.
  • If you’re doing geo-based blocking by country (not just specific IPs), consider the ngx_http_geoip2_module with a MaxMind database rather than maintaining massive manual IP range lists — it’s far more maintainable and performant at scale.

Real-World Use Cases

  • Internal admin panels and dashboards: restrict to office IPs or VPN exit IPs.
  • Staging and pre-production environments: keep search engines and the public out entirely while still allowing your team and CI systems to reach it.
  • Monitoring and metrics endpoints: lock down /nginx_status, Prometheus exporters, or health-check endpoints to internal networks only.
  • Webhook receivers: many third-party services (payment processors, GitHub, Slack) publish fixed IP ranges for their outgoing webhooks — restricting your webhook endpoint to those ranges reduces spoofed or abusive traffic.
  • Database or cache management UIs (like phpMyAdmin, Redis Commander) that should never be exposed to the open internet.

Best Practices

  • Default to deny, explicitly allow what’s needed — never build a config that defaults to allow-all with a few exceptions carved out.
  • Maintain allowlists in separate, well-documented include files rather than scattering IPs across multiple server blocks.
  • Layer authentication on top of IP restriction for anything sensitive.
  • Set up realip correctly any time you’re behind a proxy, load balancer, or CDN — otherwise your rules are silently checking the wrong IP.
  • Periodically review and prune your allowlists as part of routine maintenance.
  • Log denied requests somewhere you can review, so you can catch both attempted intrusions and legitimate users who got blocked unexpectedly.

IP-based access control is one of those features that feels almost too simple when you first set it up, but it’s genuinely effective when done carefully. Combined with TLS, authentication, and good logging, it forms a solid foundation for protecting the parts of your infrastructure that were never meant to be public.

Total
1
Shares

Leave a Reply

Previous Post
How to Enable Browser Cache in Nginx

How to Enable Browser Cache in Nginx

Next Post
How to Use Variables in Nginx Configuration

How to Use Variables in Nginx Configuration

Related Posts