Site icon GetPageSpeed

NGINX Allow Deny: Complete IP Whitelist & Blacklist Guide

NGINX Allow Deny: Complete IP Whitelist & Blacklist Guide

The NGINX allow deny directives are essential tools for controlling access to your web server by IP address. Whether you need to create an IP whitelist for admin areas or blacklist malicious actors, understanding how NGINX allow deny rules work is critical for server security.

This comprehensive guide covers everything you need to know about NGINX allow deny configuration, including CIDR notation for network ranges, the powerful geo module for large IP lists, and real-world examples with tested configurations verified using Gixy, the NGINX configuration security analyzer.

Understanding NGINX Allow and Deny Directives

The allow and deny directives are part of NGINX’s ngx_http_access_module, which is compiled into NGINX by default. These NGINX allow deny directives control whether a client IP address can access a particular location.

Basic Syntax

allow address | CIDR | unix: | all;
deny address | CIDR | unix: | all;

The directives accept:

Context

These directives can be used in:

How NGINX Evaluates Allow Deny Rules

Understanding rule evaluation order is essential for correct NGINX allow deny configuration. NGINX processes allow and deny rules in the order they appear, and the first matching rule wins.

location /admin {
    allow 192.168.1.0/24;    # Rule 1: Allow office network
    allow 10.8.0.0/24;       # Rule 2: Allow VPN range
    deny all;                # Rule 3: Deny everyone else
}

For a request from 192.168.1.50:

  1. Check Rule 1: Does 192.168.1.50 match 192.168.1.0/24? Yes β†’ Allow access
  2. Rules 2 and 3 are never evaluated

For a request from 203.0.113.10:

  1. Check Rule 1: Does 203.0.113.10 match 192.168.1.0/24? No
  2. Check Rule 2: Does 203.0.113.10 match 10.8.0.0/24? No
  3. Check Rule 3: Does 203.0.113.10 match all? Yes β†’ Deny access (HTTP 403)

Default Behavior Without deny all

A common mistake when configuring NGINX allow deny rules is forgetting the deny all directive at the end of a whitelist:

# WRONG - Missing deny all!
location /admin {
    allow 192.168.1.0/24;
    allow 10.8.0.0/24;
    # No deny all - non-matching IPs are ALLOWED!
}

When no rule matches, NGINX returns NGX_DECLINED internally, which means the request proceeds normally. Always add deny all; at the end of a whitelist configuration.

The Gixy security analyzer flags this as a HIGH severity issue:

Problem: [allow_without_deny] Found allow directive(s) without deny in the same context.
Severity: HIGH
Reason: You probably want "deny all;" after all the "allow" directives

You can verify your configurations using Gixy (available from the GetPageSpeed repository):

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install gixy
gixy /etc/nginx/nginx.conf

CIDR Notation Explained

CIDR (Classless Inter-Domain Routing) notation specifies IP ranges efficiently. The format is IP/prefix_length, where the prefix length indicates how many bits define the network portion.

Common CIDR Prefix Lengths

CIDR Subnet Mask IP Count Example Range
/32 255.255.255.255 1 Single IP only
/24 255.255.255.0 256 192.168.1.0 – 192.168.1.255
/16 255.255.0.0 65,536 192.168.0.0 – 192.168.255.255
/8 255.0.0.0 16,777,216 10.0.0.0 – 10.255.255.255

Private Network Ranges

These CIDR blocks are commonly used in internal networks:

# RFC 1918 private address ranges
allow 10.0.0.0/8;        # 10.0.0.0 - 10.255.255.255
allow 172.16.0.0/12;     # 172.16.0.0 - 172.31.255.255
allow 192.168.0.0/16;    # 192.168.0.0 - 192.168.255.255

# Localhost
allow 127.0.0.0/8;       # 127.0.0.0 - 127.255.255.255

CIDR Validation Warning

NGINX validates that your CIDR notation makes sense. If you specify host bits that would be masked out, NGINX issues a warning:

nginx: [warn] low address bits of 192.168.1.50/24 are meaningless

This warning means you wrote 192.168.1.50/24 when you likely meant 192.168.1.0/24. The /24 mask zeros out the last octet, so the .50 is ignored. While NGINX accepts the configuration, the warning helps catch mistakes.

Creating an NGINX IP Whitelist

An NGINX IP whitelist allows only specified addresses and blocks everyone else. This is the most secure approach for protecting admin interfaces.

Basic Whitelist for Admin Areas

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    server_tokens off;  # Hide NGINX version

    # Protect admin area - whitelist only
    location /admin {
        # Office network
        allow 192.168.1.0/24;

        # VPN range
        allow 10.8.0.0/24;

        # Specific remote worker IP
        allow 203.0.113.50;

        # Block everyone else
        deny all;

        try_files $uri $uri/ /admin/index.php?$args;
    }

    location / {
        try_files $uri $uri/ /index.php?$args;
    }
}

NGINX Whitelist IP Using External File

For easier maintenance, store your allowed IPs in a separate file:

# /etc/nginx/conf.d/site.conf
location /admin {
    include /etc/nginx/allowlist.conf;
    deny all;

    try_files $uri $uri/ /admin/index.php?$args;
}
# /etc/nginx/allowlist.conf
# Office network
allow 192.168.1.0/24;

# VPN range
allow 10.8.0.0/24;

# Remote workers
allow 203.0.113.50;   # Alice
allow 198.51.100.75;  # Bob

# Localhost for CLI access
allow 127.0.0.1;
allow ::1;

After modifying the allowlist file, reload NGINX:

nginx -t && systemctl reload nginx

Creating an IP Blacklist

An NGINX blacklist blocks specific addresses while allowing everyone else. This is useful for blocking known bad actors.

Basic Blacklist

location / {
    # Block known bad IPs
    deny 192.168.100.1;
    deny 192.168.100.2;

    # Block entire problematic subnet
    deny 10.99.0.0/24;

    # Allow everyone else
    allow all;

    try_files $uri $uri/ /index.php?$args;
}

Order Matters for Exceptions

You can create exceptions within a blacklist by placing specific allows before broader denies:

location / {
    # Allow this specific IP even though its subnet is blocked
    allow 192.168.100.10;

    # Block the rest of the subnet
    deny 192.168.100.0/24;

    # Allow everyone else
    allow all;
}

IPv6 Support

NGINX fully supports IPv6 addresses in allow and deny directives:

location /admin {
    # IPv4
    allow 192.168.1.0/24;

    # IPv6 localhost
    allow ::1;

    # IPv6 private range (Unique Local Address)
    allow fd00::/8;

    # IPv6 link-local
    allow fe80::/10;

    # Specific IPv6 address
    allow 2001:db8::1;

    # IPv6 subnet
    allow 2001:db8::/32;

    deny all;
}

When clients connect via IPv6 but your server also has an IPv4 address, ensure you include both IPv4 and IPv6 versions of allowed addresses.

The Geo Module for Large IP Lists

For managing thousands of IP addresses, the NGINX allow deny approach becomes inefficient. NGINX evaluates rules sequentially, so a list of 10,000 IPs means up to 10,000 comparisons per request.

The geo module solves this with optimized data structures. It maps IP addresses to variables using either a radix tree (for CIDR notation) or a two-level lookup table (for IP ranges), both providing near-constant-time lookups.

Basic Geo Block

geo $is_blocked {
    default 0;            # Not blocked by default
    192.168.100.1 1;      # Block this IP
    192.168.100.2 1;      # Block this IP
    10.99.0.0/24 1;       # Block entire subnet
}

server {
    listen 80;
    server_tokens off;

    location / {
        if ($is_blocked) {
            return 403;
        }

        # Normal request handling
        try_files $uri $uri/ /index.php?$args;
    }
}

Geo Block with Access Levels

geo $access_level {
    default       external;
    127.0.0.1     localhost;
    ::1           localhost;
    10.0.0.0/8    internal;
    172.16.0.0/12 internal;
    192.168.0.0/16 internal;
}

server {
    listen 80;
    server_tokens off;

    # Only allow internal users to access admin
    location /admin {
        if ($access_level = external) {
            return 403;
        }
        try_files $uri $uri/ /admin/index.php?$args;
    }
}

Loading Geo Data from External File

geo $blocked_ip {
    default 0;
    include /etc/nginx/geo-blocklist.conf;
}
# /etc/nginx/geo-blocklist.conf
# Format: IP/CIDR value;
192.168.100.0/24 1;
10.99.0.0/16 1;
203.0.113.0/24 1;

Geo Ranges for Large Datasets

For very large IP lists (like GeoIP data), use the ranges directive for more efficient storage:

geo $country {
    ranges;
    default ZZ;
    1.0.0.0-1.0.0.255 AU;
    1.0.1.0-1.0.3.255 CN;
    8.8.8.0-8.8.8.255 US;
}

Range mode uses a two-level lookup table with 65,536 buckets indexed by the high 16 bits of the IPv4 address, then binary search within each bucket. This provides fast lookups even with millions of entries.

Protecting WordPress Admin Areas

Here is a complete example for protecting WordPress administration using NGINX allow deny rules:

# Define allowed admin IPs
geo $allow_wp_admin {
    default 0;

    # Office network
    192.168.1.0/24 1;

    # VPN range
    10.8.0.0/24 1;

    # Localhost for WP-CLI
    127.0.0.1 1;
    ::1 1;
}

server {
    listen 80;
    server_name example.com;
    root /var/www/wordpress;
    index index.php;
    server_tokens off;

    # Protect wp-admin directory
    location /wp-admin {
        if ($allow_wp_admin = 0) {
            return 403;
        }

        location ~ [.]php$ {
            include fastcgi_params;
            fastcgi_pass unix:/run/php-fpm/www.sock;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    }

    # Protect wp-login.php
    location = /wp-login.php {
        allow 192.168.1.0/24;
        allow 10.8.0.0/24;
        allow 127.0.0.1;
        allow ::1;
        deny all;

        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Block xmlrpc.php entirely
    location = /xmlrpc.php {
        deny all;
    }

    # Standard WordPress handling
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ [.]php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

Critical Caveat: The return Directive Bypasses Access Control

The return directive executes in the rewrite phase, which happens before the access phase where NGINX allow deny rules are evaluated. This means:

location /admin {
    allow 10.0.0.1;
    deny all;
    return 200 "Admin panel";  # Bypasses access control!
}

This configuration does not protect /admin because return executes before the access check.

Gixy detects this vulnerability automatically:

Problem: [return_bypasses_allow_deny] Return directive bypasses allow/deny restrictions
Severity: MEDIUM
Reason: allow/deny do not restrict access to responses produced by return in the same scope

Use try_files or content handlers (like fastcgi_pass, proxy_pass) instead:

location /admin {
    allow 10.0.0.1;
    deny all;
    try_files $uri $uri/ /admin/index.php?$args;  # Access check runs first
}

Security Best Practices

Based on analysis with Gixy, follow these security best practices:

Hide NGINX Version

Always disable version disclosure to prevent attackers from targeting known vulnerabilities:

http {
    server_tokens off;

    server {
        # ...
    }
}

Gixy flags missing server_tokens off as HIGH severity:

Problem: [version_disclosure] Do not enable server_tokens on
Severity: HIGH
Reason: Missing server_tokens directive - defaults to 'on' which promotes information disclosure

Validate Configurations with Gixy

Before deploying, run Gixy to catch security misconfigurations. Gixy is available from the GetPageSpeed repository:

# Install the GetPageSpeed repository
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm

# Install Gixy
sudo dnf install gixy

# Analyze your configuration
gixy /etc/nginx/nginx.conf

# Check specific file
gixy /etc/nginx/conf.d/site.conf

Testing Your NGINX Allow Deny Configuration

Always test your configuration before applying it to production:

# Check syntax
nginx -t

# Reload configuration
systemctl reload nginx

# Test from different IPs using curl
curl -I http://example.com/admin

Simulating Requests from Different IPs

For testing, you can use the ngx_http_realip_module with a custom header:

# Testing configuration only - remove in production!
set_real_ip_from 127.0.0.1;
real_ip_header X-Test-IP;

Then test with:

curl -H "X-Test-IP: 192.168.1.50" http://localhost/admin
curl -H "X-Test-IP: 203.0.113.10" http://localhost/admin

Configuration Inheritance

NGINX allow deny rules follow the standard inheritance model:

server {
    allow 10.0.0.0/8;
    deny all;

    location /public {
        # No access rules - inherits server block rules
    }

    location /api {
        allow all;  # Has own rules - does NOT inherit
    }
}

Performance Considerations

Allow/Deny Performance

The access module evaluates rules sequentially with O(n) complexity. For small lists (under 100 entries), this is negligible. For larger lists, use the geo module.

Geo Module Performance

The geo module uses optimized data structures:

For lists with thousands of IPs, the geo module is significantly faster.

Common NGINX Allow Deny Mistakes to Avoid

  1. Forgetting deny all at the end of a whitelist (Gixy: HIGH severity)
  2. Using return with access rules – the return bypasses access checks (Gixy: MEDIUM severity)
  3. Leaving server_tokens on – exposes NGINX version (Gixy: HIGH severity)
  4. Wrong rule order – remember first match wins
  5. Meaningless CIDR bits – 192.168.1.100/24 should be 192.168.1.0/24
  6. Missing IPv6 – if your server supports IPv6, include IPv6 rules
  7. Testing only from localhost – always test from external IPs before deploying

Summary

The NGINX allow deny directives provide straightforward IP-based access control. For simple configurations with a handful of IPs, they work well. For larger IP lists or more complex logic, the geo module offers better performance and flexibility.

Key points to remember:

With proper IP whitelisting using NGINX allow deny rules and validation using tools like Gixy, you can significantly reduce your attack surface by ensuring only trusted networks can access sensitive areas of your application.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience β€’ Maintainer of GetPageSpeed RPM repository β€’ Contributor to open-source NGINX modules

Exit mobile version