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-Keyheader) - 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:
- The variable or expression in the rule is evaluated
- If the result is empty or exactly “0”, the rule is skipped (considered false)
- If the result is non-empty and not “0”, the rule matches (considered true)
- For a matching
allowrule, the request passes and remaining rules are skipped - For a matching
denyrule, 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 anyaccessdirectives, parent rules are ignored. If the child defines noaccessdirectives, parent rules are inherited as-is.before: Parent rules are evaluated first, followed by child rules. This is useful when a parent-levelallowrule 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_paramor$http_header, this is extremely fast (nanosecond-level overhead). - Map lookups: When used with
mapdirectives, 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:
- Use rate limiting to prevent brute-force attempts against token-based access
- Add security headers to protect against client-side attacks
- Deploy JWT authentication for stateless token validation
- Consider honeypot traps to automatically ban malicious IPs
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:
- Module not loaded: Verify that
load_module modules/ngx_http_access_control_module.so;is present in your configuration andnginx -tpasses. returndirective short-circuiting: Thereturndirective runs before the access phase. Replacereturnwith static file serving or proxy handlers.-
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.
- 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, setaccess_rules_inherit beforeoraccess_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.
