Skip to main content

NGINX / Security

NGINX Access Control Module: Variable-Based Rules

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

The NGINX access control module (ngx_http_access_control_module) extends NGINX’s native allow/deny directives beyond IP addresses to any NGINX variable. It enables flexible access policies based on request headers, query parameters, geographic data, and custom maps — all evaluated in the proper access phase without resorting to error-prone if statements.

NGINX ships with a built-in access module that lets you allow or deny requests based on client IP addresses. However, modern access control often requires decisions based on request headers, query parameters, geographic location, or custom business logic. The native allow and deny directives simply cannot handle these scenarios.

Why Native Access Control Falls Short

NGINX’s built-in allow and deny directives only accept IP addresses and CIDR ranges:

location /admin {
    allow 10.0.0.0/8;
    allow 192.168.1.0/24;
    deny all;
}

This approach works well for simple IP-based restrictions. However, you cannot use it for:

  • Header-based access (e.g., API keys in X-Api-Key header)
  • Token-based authentication (e.g., query string parameters)
  • Geographic restrictions combined with other conditions
  • Custom business logic driven by map directives
  • Multi-factor conditions that combine several variables

Without this module, you would typically use if statements in the rewrite phase:

# The problematic approach - if is evil in NGINX
map $http_x_api_key $has_api_key {
    default 0;
    "my-secret-key" 1;
}

server {
    location /api {
        if ($has_api_key = 0) {
            return 403;
        }
        proxy_pass http://backend;
    }
}

The problem with this approach is that if in NGINX operates in the rewrite phase, which can interfere with other directives, produce unexpected behavior, and makes configurations difficult to reason about.

The NGINX access control module provides a clean, purpose-built alternative that operates correctly in the access phase.

How the NGINX Access Control Module Works

The module registers a handler in NGINX’s access phase — the same phase where native allow/deny and authentication modules operate. This means it integrates naturally with the request processing pipeline.

When a request arrives, the module evaluates each rule in order:

  1. The variable or expression in the rule is evaluated
  2. If the result is empty or exactly “0”, the rule is skipped (considered false)
  3. If the result is non-empty and not “0”, the rule matches (considered true)
  4. For a matching allow rule, the request passes and remaining rules are skipped
  5. For a matching deny rule, the request is rejected with the configured status code

If no rule matches, the request continues to the next handler. This “first match wins” behavior is consistent with how NGINX’s native access module processes rules.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux, and Fedora

Install the NGINX access control module from the GetPageSpeed RPM repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-access-control

After installation, load the module by adding this directive at the top of /etc/nginx/nginx.conf, before the events block:

load_module modules/ngx_http_access_control_module.so;

Alternatively, the package installs a convenience configuration file. You can load all installed modules at once:

include /usr/share/nginx/modules/*.conf;

Verify the module is loaded by testing your configuration:

sudo nginx -t

If the test passes and you have access directives in your configuration, the module is working correctly.

Configuration Reference

The NGINX access control module provides three directives. All directives were verified against the module source code and tested on Rocky Linux 10.

The access Directive

Syntax: access allow|deny <variable_or_expression>;
Default: none
Context: http, server, location, limit_except

The access directive defines a single access control rule. The first argument specifies the action (allow or deny), and the second argument is any valid NGINX variable or complex expression.

The variable is evaluated at request time. A value is considered true (matching) when it is:

  • Non-empty string (e.g., "1", "yes", "anything")
  • Any string that is not exactly "0"

A value is considered false (non-matching) when it is:

  • Empty string ""
  • The string "0"

Example — deny requests that contain a specific query parameter:

location /protected {
    access deny $arg_block;
}

When a client requests /protected?block=1, the $arg_block variable evaluates to "1" (truthy), so the request is denied. A request to /protected without the parameter evaluates to an empty string (falsy), so the request passes through.

The access_deny_status Directive

Syntax: access_deny_status <code>;
Default: access_deny_status 403;
Context: http, server, location

This directive sets the HTTP response status code returned when a deny rule matches. By default, denied requests receive a 403 Forbidden response.

You can customize this to return any HTTP status code. For example, returning 404 Not Found hides the existence of a protected resource:

location /secret {
    access_deny_status 404;
    access deny $arg_block;
}

Returning 451 Unavailable For Legal Reasons may be appropriate for content restricted by geographic region:

location /restricted-content {
    access_deny_status 451;
    access deny $is_blocked_country;
}

The access_rules_inherit Directive

Syntax: access_rules_inherit off|before|after;
Default: access_rules_inherit off;
Context: http, server, location

This directive controls whether access rules from a parent configuration level (e.g., server block) are inherited by a child level (e.g., location block).

The three modes work as follows:

  • off (default): Child rules completely replace parent rules. If the child level defines any access directives, parent rules are ignored. If the child defines no access directives, parent rules are inherited as-is.
  • before: Parent rules are evaluated first, followed by child rules. This is useful when a parent-level allow rule should always take priority (e.g., always allow admin users).
  • after: Child rules are evaluated first, followed by parent rules. This gives child-specific rules priority over parent rules.

Practical Examples

API Key Authentication via Headers

Use the map directive to validate API keys sent in request headers:

map $http_x_api_key $has_valid_api_key {
    default 0;
    "sk_live_abc123def456" 1;
    "sk_live_xyz789ghi012" 1;
}

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

    location /v1/ {
        access allow $has_valid_api_key;
        access deny 1;

        proxy_pass http://api_backend;
    }
}

The access deny 1; at the end acts as a “deny all” catch-all. Since the literal 1 is always truthy, any request that was not already allowed by the preceding rule is denied.

Country-Based Restrictions with GeoIP2

Combine the NGINX access control module with the GeoIP2 module to restrict content by country:

geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
    auto_reload 60m;
    $geoip2_country_code default=XX source=$remote_addr country iso_code;
}

map $geoip2_country_code $is_allowed_country {
    default 0;
    US 1;
    CA 1;
    GB 1;
    DE 1;
}

server {
    listen 80;
    server_name example.com;

    location /region-locked {
        access_deny_status 451;
        access allow $is_allowed_country;
        access deny 1;
    }
}

Combining IP Ranges with Variable Conditions

Use the NGINX geo directive to define IP-based variables, then combine them with other conditions:

geo $is_office_ip {
    default 0;
    10.0.0.0/8 1;
    172.16.0.0/12 1;
    192.168.0.0/16 1;
}

map $http_x_admin_token $has_admin_token {
    default 0;
    "super-secret-token" 1;
}

server {
    listen 80;
    server_name example.com;

    location /admin {
        # Allow office IPs without any token
        access allow $is_office_ip;
        # Also allow anyone with valid admin token
        access allow $has_admin_token;
        # Deny everyone else
        access deny 1;
    }
}

This creates an “OR” logic: requests from office IP ranges or requests with a valid admin token are allowed. Everyone else is denied.

Rule Inheritance for Multi-Tier Access

Use access_rules_inherit to build layered access policies:

server {
    listen 80;
    server_name example.com;

    # Server-level: always allow admin users
    access allow $is_admin;

    location /dashboard {
        # Inherit parent "allow admin" rule BEFORE local rules
        access_rules_inherit before;
        # Also allow regular authenticated users
        access allow $is_authenticated_user;
        # Deny everyone else
        access deny 1;
    }

    location /public-api {
        # No access directives here, inherits parent rules
        # Admin access inherited; non-admins pass through normally
        proxy_pass http://api_backend;
    }
}

With access_rules_inherit before, the server-level access allow $is_admin rule is checked first. If $is_admin is truthy, the request is immediately allowed regardless of whether $is_authenticated_user is set. This guarantees admin access to all protected locations.

Important: With access_rules_inherit after, child rules are evaluated first. If a child-level access deny 1; fires before the parent allow rule, the request is denied — even for admins. Choose before when parent rules should take priority.

Important Gotchas

The return Directive Bypasses Access Control

The return directive executes during the rewrite phase, which runs before the access phase. This means return short-circuits access control completely:

location /broken {
    access deny 1;
    return 200 "This is always accessible!\n";
    # The access deny rule NEVER runs
}

This is not a bug in the NGINX access control module — it is standard NGINX phase ordering. When using this module, serve content through static files, proxy_pass, fastcgi_pass, or other content-phase handlers instead of return.

Truthiness Rules

Understanding how the module evaluates variables is essential:

Variable value Truthy? Behavior
"" (empty) No Rule skipped
"0" No Rule skipped
"1" Yes Rule matches
"yes" Yes Rule matches
"no" Yes Rule matches (non-empty, not “0”)
"false" Yes Rule matches (non-empty, not “0”)

Watch out: Strings like "no", "false", and "off" are truthy because they are non-empty and not the string "0". Use map to normalize these to 0 or 1 before passing them to access directives.

Interaction with Native allow/deny

The module’s access directive and NGINX’s native allow/deny directives both run in the access phase, but they are independent handlers. Both sets of rules must pass for a request to succeed (unless satisfy any is configured).

However, the NGINX access control module returns NGX_DECLINED for allow rules rather than NGX_OK. This means it does not positively assert access for satisfy any configurations. For combining variable-based checks with other authentication modules, keep all logic within the access control module or use separate location blocks.

Performance Considerations

The NGINX access control module operates in the access phase, which executes for every request that matches the configured location. Here are the performance characteristics:

  • Variable evaluation: Each rule evaluates an NGINX complex value. For simple variables like $arg_param or $http_header, this is extremely fast (nanosecond-level overhead).
  • Map lookups: When used with map directives, performance depends on the map hash table size. NGINX’s map uses hash tables, so lookups are O(1) regardless of the number of entries.
  • Rule count: Rules are evaluated sequentially until a match is found. Keep the number of rules per location small (ideally under 10). Place the most likely matching rule first for early termination.
  • Memory: Each rule allocates a small struct with a compiled complex value. The overhead is negligible even with dozens of rules.

For high-traffic environments with large IP allowlists, consider using the geo directive to pre-compute a variable from IP addresses, then pass that variable to a single access allow rule. This is more efficient than having hundreds of individual access rules.

Security Best Practices

Always End with a Deny-All Rule

When defining access policies, always include access deny 1; as the last rule:

location /protected {
    access allow $is_authorized;
    access deny 1;
}

Without the deny-all rule, requests that don’t match any allow rule will pass through to the content handler. This fail-open behavior can lead to unauthorized access.

Validate Inputs with Map Directives

Never pass raw user-controlled variables directly to access allow. Always validate and normalize through a map:

# Bad: raw header value is truthy if non-empty
location /bad {
    access allow $http_x_admin;  # ANY non-empty value allows access
    access deny 1;
}

# Good: validate against known values
map $http_x_admin $is_admin {
    default 0;
    "expected-secret-value" 1;
}

location /good {
    access allow $is_admin;
    access deny 1;
}

Use Custom Status Codes Strategically

Returning 403 Forbidden confirms to attackers that a resource exists but is protected. Consider using 404 Not Found for sensitive endpoints to hide their existence:

location /admin-panel {
    access_deny_status 404;
    access allow $is_admin;
    access deny 1;
}

Combine with Other Security Layers

The NGINX access control module works alongside other NGINX security features for defense in depth:

Get the Client IP Right

If NGINX sits behind a load balancer or CDN, the $remote_addr variable contains the proxy’s IP, not the client’s. Configure real IP detection so that geo blocks and other IP-based variables resolve correctly.

Troubleshooting

Denied Requests Logged as Errors

When a deny rule matches, the module logs a message at the error level:

2026/02/25 14:54:46 [error] 2592#2592: *14 access denied by access_control rules,
client: 127.0.0.1, server: _, request: "GET /protected/ HTTP/1.1", host: "localhost"

Monitor these entries with:

grep "access denied by access_control" /var/log/nginx/error.log

Rules Not Taking Effect

If access control rules appear to have no effect, check these common causes:

  1. Module not loaded: Verify that load_module modules/ngx_http_access_control_module.so; is present in your configuration and nginx -t passes.

  2. return directive short-circuiting: The return directive runs before the access phase. Replace return with static file serving or proxy handlers.

  3. Variable is always empty: Use NGINX’s debug log to inspect variable values:

error_log /var/log/nginx/error.log debug;

Then check the log for the variable evaluation during request processing.

  1. Inheritance confusion: Remember that access_rules_inherit off (the default) discards parent rules when child rules exist. If you expect parent rules to apply alongside child rules, set access_rules_inherit before or access_rules_inherit after.

Verifying Module Installation

On RPM-based systems, confirm the module is installed and check available directives:

rpm -q nginx-module-access-control
strings /usr/lib64/nginx/modules/ngx_http_access_control_module.so | grep access

Conclusion

The NGINX access control module bridges the gap between NGINX’s IP-only native access control and the complex access policies that modern applications require. By evaluating any NGINX variable at runtime, it enables header-based authentication, geographic restrictions, token validation, and multi-condition access rules — all without resorting to fragile if statements.

The module operates in NGINX’s access phase, integrates cleanly with map and geo directives, and supports hierarchical rule inheritance for multi-tier configurations.

Install it from the GetPageSpeed repository and explore the source code on GitHub.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.