How to Block IPs Based on URI Patterns Within a Single Nginx Location Block


4 views

Many developers encounter this frustrating limitation when trying to implement URI-based IP restrictions in Nginx. The standard approach would be:

location / {
    if ($uri ~ '^/(admin|api|private)') {
        allow 192.168.1.0/24;
        deny all;  # This won't work!
    }
    # Other directives...
}

This fails because Nginx specifically prohibits access control directives (allow/deny) inside if blocks. The error message you'll see is:

\"allow\" directive is not allowed here...

1. Using Separate Location Blocks

The most straightforward solution is to create multiple location blocks:

location ~ ^/(admin|api|private) {
    allow 192.168.1.0/24;
    deny all;
    
    root /var/www/docs;
    proxy_pass http://backend;
    # Other proxy directives...
}

location / {
    root /var/www/docs;
    proxy_pass http://backend;
    # Other proxy directives...
}

While this works, it violates DRY principles by repeating configuration.

2. Using a Map Variable

A more elegant approach uses the map directive outside your server block:

map $uri $is_restricted {
    default         0;
    ~^/(admin|api)  1;
}

server {
    location / {
        if ($is_restricted) {
            set $block 1;
        }
        
        if ($remote_addr ~ ^(192\.168\.1\.)) {
            set $block 0;
        }
        
        if ($block = 1) {
            return 403;
        }
        
        root /var/www/docs;
        proxy_pass http://backend;
        # Other directives...
    }
}

3. Geo Module with Variables

For complex scenarios, combine geo and map:

geo $block_access {
    default        0;
    192.168.1.0/24 1;
    10.0.0.0/8     1;
}

map $uri $restricted_uri {
    default         0;
    ~^/(admin|api)  1;
}

server {
    location / {
        if ($restricted_uri = 1) {
            set $check 1;
        }
        
        if ($block_access = 0) {
            set $check "${check}1";
        }
        
        if ($check = 11) {
            return 403;
        }
        
        # Normal configuration...
    }
}

The map-based solutions add minimal overhead compared to multiple location blocks. For high-traffic sites:

  • Maps are evaluated only once per request
  • Regex complexity affects performance more than the map itself
  • Location-based matching is generally faster for simple cases

Here's a complete implementation for protecting admin areas:

map $uri $admin_area {
    default         0;
    ~^/(wp-admin|admin|backend) 1;
}

server {
    location / {
        if ($admin_area) {
            satisfy any;
            allow 192.168.1.0/24;
            allow 10.10.10.5;
            deny all;
            auth_basic "Admin Area";
            auth_basic_user_file /etc/nginx/.htpasswd;
        }
        
        try_files $uri $uri/ @proxy;
    }
    
    location @proxy {
        proxy_pass http://backend;
        # Other proxy settings...
    }
}

When working with Nginx configurations, we often encounter situations where we need to restrict access to specific URIs based on client IP addresses. The naive approach would be to create separate location blocks, but this leads to code duplication and maintenance headaches.

The Nginx configuration parser strictly enforces directive placement rules. The allow/deny directives are part of the access module and can only be placed in certain contexts. The error message clearly indicates this limitation:

"allow" directive is not allowed here in /etc/nginx/sites-enabled/mysite:20

We can leverage Nginx's map directive to create conditional logic without violating the configuration rules. Here's how to implement it:

http {
    map $uri $restricted_uri {
        default         0;
        ~^/(abc|def|ghi) 1;
    }

    server {
        location / {
            if ($restricted_uri) {
                set $deny_access 1;
            }

            if ($remote_addr ~ ^10\.0\.0\..*) {
                set $deny_access 0;
            }

            if ($remote_addr = 1.2.3.4) {
                set $deny_access 0;
            }

            if ($deny_access) {
                return 403;
            }

            root /var/www/docs;
            proxy_pass http://backend;
            proxy_buffering on;
            proxy_buffer_size 64k;
            proxy_buffers 256 64k;
        }
    }
}

Another clean solution involves using Nginx variables to control access:

geo $restricted_ips {
    default 0;
    10.0.0.0/8 1;
    1.2.3.4 1;
}

server {
    location / {
        if ($uri ~ ^/(abc|def|ghi)) {
            set $require_whitelist 1;
        }

        if ($restricted_ips) {
            set $require_whitelist 0;
        }

        if ($require_whitelist) {
            return 403;
        }

        # Original configuration continues here
        root /var/www/docs;
        proxy_pass http://backend;
        # ... other proxy settings
    }
}

Both solutions maintain good performance because:

  • Map directives are evaluated at configuration load time
  • Geo blocks are optimized for IP matching
  • The variable approach adds minimal processing overhead

Always validate your Nginx config after making changes:

nginx -t

And reload the configuration:

nginx -s reload