Every web application faces a barrage of automated attacks. SQL injection probes, cross-site scripting payloads, credential stuffing bots, and volumetric HTTP floods arrive around the clock. While NGINX handles raw traffic efficiently, it has no built-in awareness of application-layer threats. The NGINX WAF module (ngx_waf) solves this by embedding a lightweight web application firewall directly into NGINX’s request processing pipeline.
You could deploy ModSecurity — the industry-standard WAF — but its OWASP Core Rule Set is heavy. It parses every request through hundreds of regular expressions, consumes significant memory, and requires careful tuning to avoid false positives. For many deployments, that level of complexity is overkill.
The NGINX WAF module is a practical alternative. It ships with ready-to-use rule files for SQL injection, XSS, and malicious user-agent detection. Additionally, it includes a rate limiter (CC defense), an IP trie for fast CIDR matching, libinjection-based SQL/XSS fingerprinting, a custom rule language for complex logic, and a Cloudflare-style “Under Attack” challenge mode — all in a single dynamic module.
How the NGINX WAF Module Works
The NGINX WAF module registers a handler in NGINX’s access phase. Every incoming request passes through a configurable chain of inspections before reaching your application:
- IP check — The client IP is matched against prefix tries for whitelisted and blacklisted IPv4/IPv6 CIDR ranges. Trie-based matching is O(prefix length), not O(n), so performance remains constant regardless of how many IP ranges you configure.
-
CC defense — A shared-memory counter tracks request rates per IP. When a client exceeds the configured threshold, it is temporarily banned and receives a
429 Too Many Requestsresponse with aRetry-Afterheader. -
Under Attack mode — An optional JavaScript challenge (similar to Cloudflare’s “I’m Under Attack” mode) that issues a 303 redirect with verification cookies. Legitimate browsers follow the redirect automatically; simple bots cannot.
-
URL, query string, and header inspection — The request URI, query string arguments,
User-Agent,Referer, andCookieheaders are matched against regex-based blacklists and whitelists. -
libinjection fingerprinting — Query string parameters, cookies, and POST body content are analyzed using the libinjection library, which detects SQL injection and XSS payloads through lexical fingerprinting rather than regex alone. This catches obfuscated attacks that regex rules miss.
-
Advanced rules — A custom DSL (domain-specific language) compiled to bytecode at configuration time enables complex conditional logic: combine IP ranges, URL patterns, headers, and injection detection in a single rule.
-
POST body inspection — Request body content is checked against regex blacklists and libinjection. This check always runs last and cannot be reordered.
A whitelisted match at any stage immediately allows the request, skipping all remaining checks. A blacklist match returns the configured HTTP status code (default 403). The inspection order is configurable via the waf_priority directive.
Installing the NGINX WAF Module
RHEL, CentOS, AlmaLinux, Rocky Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-waf
Then load the NGINX WAF module in /etc/nginx/nginx.conf (must be in the main context, before any http block):
load_module modules/ngx_http_waf_module.so;
The package also installs default rule files to /etc/nginx/waf-rules/.
For the full list of available builds, see the RPM module page.
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-waf
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
For the full list of available builds, see the APT module page.
Verifying the Installation
After installing, confirm the NGINX WAF module is loaded:
nginx -t
If the test passes with waf directives in your configuration, the module is loaded and working. You can also verify the module file exists:
ls /usr/lib64/nginx/modules/ngx_http_waf_module.so
Basic Configuration
Here is a minimal configuration that enables the NGINX WAF module with standard protections:
load_module modules/ngx_http_waf_module.so;
http {
server {
listen 80;
server_name example.com;
location / {
waf on;
waf_rule_path /etc/nginx/waf-rules/;
waf_mode STD;
waf_cc_deny rate=100r/m duration=1h size=20m;
waf_cache capacity=50;
proxy_pass http://backend;
}
}
}
This configuration enables IP blacklisting, URL/argument/user-agent inspection, CC rate limiting (100 requests per minute per IP, 1-hour ban), libinjection SQL detection, and result caching.
Important: The
waf_rule_pathvalue must end with a trailing slash (/). All 15 rule files must exist in the directory, even if empty.
Directive Reference
waf
Enables or disables the NGINX WAF module for a given context.
waf on;
- Context: http, server, location
- Default: off
- Values:
on|off
waf_rule_path
Specifies the directory containing rule files. The module loads all 15 rule files from this directory at configuration time.
waf_rule_path /etc/nginx/waf-rules/;
- Context: http, server, location
- Default: none (required when
wafison)
The trailing slash is mandatory. If any rule file is missing, nginx -t fails with a “No such file or directory” error.
waf_mode
Controls which inspection checks are active. Accepts one or more mode keywords. Prefix a keyword with ! to disable it.
waf_mode STD;
- Context: http, server, location
- Default: none (no checks enabled)
Preset modes:
| Preset | Included checks |
|---|---|
STD |
IP, URL, RBODY, ARGS, UA, CMN-METH, CC, CACHE, LIB-INJECTION-SQLI |
STATIC |
IP, URL, UA, GET, HEAD, CC, CACHE |
DYNAMIC |
IP, URL, RBODY, ARGS, UA, COOKIE, CMN-METH, CC, CACHE, LIB-INJECTION-SQLI, ADV |
FULL |
All checks including ALL-METH, REFERER, LIB-INJECTION (both SQLi and XSS) |
Individual mode keywords:
| Category | Keywords |
|---|---|
| HTTP methods | GET, HEAD, POST, PUT, DELETE, MKCOL, COPY, MOVE, OPTIONS, PROPFIND, PROPPATCH, LOCK, UNLOCK, PATCH, TRACE |
| Method presets | CMN-METH (GET+POST+HEAD), ALL-METH (all 15 methods) |
| Inspection targets | IP, URL, RBODY (request body), ARGS (query string), UA (user-agent), COOKIE, REFERER, CC (rate limiting), ADV (advanced rules) |
| Extras | CACHE (enable result caching), LIB-INJECTION (both SQLi+XSS), LIB-INJECTION-SQLI, LIB-INJECTION-XSS |
Example combining a preset with overrides:
waf_mode STD !CC LIB-INJECTION-XSS;
This enables standard protections, disables CC rate limiting, and adds XSS detection via libinjection.
waf_cc_deny
Configures CC (Challenge Collapsar) rate limiting. This directive tracks request counts per IP in shared memory and bans IPs that exceed the threshold.
waf_cc_deny rate=100r/m duration=1h size=20m;
- Context: http, server, location
- Default: none (CC disabled unless configured)
Parameters:
| Parameter | Format | Required | Default | Description |
|---|---|---|---|---|
rate |
Nr/m |
Yes | — | Maximum N requests per minute |
duration |
N + s/m/h/d |
No | 1h |
Ban duration |
size |
N + k/m/g |
No | 20m |
Shared memory zone size (minimum 20 MB) |
The r/m suffix is mandatory in the rate value: 100r/m means 100 requests per minute. The duration and size values require a unit suffix.
When a client is rate-limited, the response includes a Retry-After header with the remaining ban time in seconds.
waf_cache
Enables caching of inspection results. When the same URL, query string, user-agent, referer, or cookie value is seen again, the cached result is returned without re-running regex matching.
waf_cache capacity=50;
- Context: http, server, location
- Default: none (caching disabled)
Parameters:
| Parameter | Required | Description |
|---|---|---|
capacity |
Yes | Maximum number of cached results per inspection type |
Each location maintains separate LRU caches for URL, ARGS, UA, Referer, Cookie, white-URL, and white-Referer checks. POST body inspection results are never cached. The minimum capacity is 50.
waf_under_attack
Enables “Under Attack” mode — a JavaScript challenge that verifies clients are real browsers before allowing access. This feature of the NGINX WAF module works similarly to Cloudflare’s “I’m Under Attack” mode.
waf_under_attack on uri=/under-attack;
- Context: http, server, location
- Default: off
When enabled, new visitors without valid verification cookies receive a 303 redirect to the specified URI. The module sets three cookies:
__waf_under_attack_time— timestamp__waf_under_attack_uid— random 64-character string__waf_under_attack_verification— SHA-256 HMAC
The challenge page should display a “please wait” message and redirect back after a brief delay. Here is an example location block for the challenge page:
location /under-attack {
default_type text/html;
return 200 '<!DOCTYPE html>
<html>
<head><title>Checking your browser</title>
<meta http-equiv="refresh" content="6;url=$arg_target">
</head>
<body>
<h1>Verifying you are human...</h1>
<p>This process is automatic. You will be redirected shortly.</p>
</body>
</html>';
}
Cookies expire after 30 minutes. The verification hash is bound to the client IP address, so cookies cannot be reused from a different IP.
waf_priority
Customizes the order in which inspection checks run. By default, whitelists are checked before blacklists and CC runs early.
waf_priority "W-IP IP CC UNDER-ATTACK W-URL URL ARGS UA W-REFERER REFERER COOKIE ADV";
- Context: http, server, location
- Default: W-IP, IP, CC, UNDER-ATTACK, W-URL, URL, ARGS, UA, W-REFERER, REFERER, COOKIE, ADV
All 12 check names must be listed. POST body inspection always runs last and cannot be reordered.
| Check name | What it does |
|---|---|
W-IP |
White IP check (allow) |
IP |
Black IP check (block) |
CC |
Rate limiting |
UNDER-ATTACK |
JavaScript challenge |
W-URL |
White URL check (allow) |
URL |
Black URL check (block) |
ARGS |
Black query string check |
UA |
Black user-agent check |
W-REFERER |
White referer check (allow) |
REFERER |
Black referer check (block) |
COOKIE |
Black cookie check |
ADV |
Advanced rules (VM) |
waf_http_status
Configures HTTP status codes returned when the NGINX WAF module blocks requests.
waf_http_status general=403 cc_deny=429;
- Context: http, server, location
- Default:
general=403 cc_deny=503
Setting general=444 causes NGINX to drop the connection without sending a response — useful for stealth blocking of scanners.
Variables
The NGINX WAF module exports six variables for use in logging and conditional logic:
| Variable | Description | Example value |
|---|---|---|
$waf_blocked |
"true" if blocked, "false" if inspected but not blocked |
true |
$waf_blocking_log |
"true" only for blocked requests |
true |
$waf_log |
"true" if the module inspected this request |
true |
$waf_rule_type |
The rule category that matched | BLACK-ARGS |
$waf_rule_details |
The specific rule or pattern that matched | (?i)(?:(?:union(.*?)select)) |
$waf_spend |
Inspection time in milliseconds | 0.07900 |
WAF-Specific Access Log
Use these variables in a custom log format to monitor WAF activity:
log_format waf_log '$remote_addr - [$time_local] "$request" $status '
'waf_blocked=$waf_blocked rule_type=$waf_rule_type '
'rule_details="$waf_rule_details" spend=${waf_spend}ms';
server {
access_log /var/log/nginx/waf-access.log waf_log;
# ...
}
Example output:
127.0.0.1 - [03/Apr/2026:17:06:28 +0800] "GET /?id=1+UNION+SELECT+1 HTTP/1.1" 403 waf_blocked=true rule_type=BLACK-ARGS rule_details="(?i)(?:(?:union(.*?)select))" spend=0.07900ms
Logging Only Blocked Requests
To avoid filling logs with legitimate traffic, combine $waf_blocking_log with a map block and conditional logging:
map $waf_blocking_log $waf_log_enabled {
"true" 1;
default 0;
}
server {
access_log /var/log/nginx/waf-blocked.log waf_log if=$waf_log_enabled;
# ...
}
This logs only requests blocked by the NGINX WAF module, making it easy to review attacks without noise.
Rule Files
The NGINX WAF module ships with 15 rule files in /etc/nginx/waf-rules/. Each file serves a specific purpose:
Blacklist Files
| File | Format | What it blocks |
|---|---|---|
ipv4 |
CIDR notation, one per line | IPv4 addresses/ranges |
ipv6 |
CIDR notation, one per line | IPv6 addresses/ranges |
url |
PCRE regex, one per line | URL path patterns |
args |
PCRE regex, one per line | Query string patterns |
user-agent |
PCRE regex, one per line | User-Agent patterns |
referer |
PCRE regex, one per line | Referer header patterns |
cookie |
PCRE regex, one per line | Cookie value patterns |
post |
PCRE regex, one per line | POST body patterns |
Whitelist Files
| File | Format | What it allows |
|---|---|---|
white-ipv4 |
CIDR notation | Whitelisted IPv4 ranges |
white-ipv6 |
CIDR notation | Whitelisted IPv6 ranges |
white-url |
PCRE regex | Whitelisted URL patterns |
white-referer |
PCRE regex | Whitelisted referer patterns |
Advanced Rules File
| File | Format | Purpose |
|---|---|---|
advanced |
Custom DSL | Complex conditional rules |
Default Rule Coverage
The shipped rules detect:
- SQL injection:
UNION SELECT,information_schema,INTO DUMPFILE/OUTFILE,benchmark(),sleep(),base64_decode(), PHP superglobals - XSS:
<script>,<iframe>,<img>with event handlers likeonerror=,onload= - Path traversal:
../sequences,.svn,.htaccess,.bash_historyaccess - Scanner tools: sqlmap, Nikto, nmap, dirbuster, hydra, zgrab, HTTrack, w3af, and 20+ other known scanners
- Dangerous files:
.bak,.sql,.backup,.mdb,.oldextensions - Protocol abuse:
gopher://,ldap://,phar://,data://protocol handlers - Java exploits:
java.lang,xwork.MethodAccessor(Struts2) - Template injection:
${and:$patterns
Customizing Rules
To add a custom IP blacklist, append entries to the rule files:
# Block a specific IP
echo "192.168.1.100/32" >> /etc/nginx/waf-rules/ipv4
# Block an entire subnet
echo "10.0.0.0/8" >> /etc/nginx/waf-rules/ipv4
# Whitelist your monitoring service
echo "203.0.113.50/32" >> /etc/nginx/waf-rules/white-ipv4
To add custom regex rules:
# Block requests containing "wp-login.php" in the URL
echo '(?i)wp-login\.php' >> /etc/nginx/waf-rules/url
# Whitelist your API endpoints from WAF inspection
echo '^/api/v[0-9]+/' >> /etc/nginx/waf-rules/white-url
After modifying rule files, reload NGINX to apply changes:
sudo systemctl reload nginx
Advanced Rules (DSL)
For scenarios where simple regex blacklists are insufficient, the NGINX WAF module provides a custom domain-specific language. Advanced rules are compiled to bytecode at configuration time and executed by a stack-based virtual machine at request time. You must enable ADV in waf_mode (included in DYNAMIC and FULL presets).
Rule Syntax
Each rule consists of three lines:
id: rule_name
if: condition
do: action
Separate multiple rules with exactly one blank line (two newlines). The file must not end with a trailing newline.
Available Operands
| Operand | Description |
|---|---|
url |
Request URI path |
query_string[key] |
Specific query parameter value |
user_agent |
User-Agent header |
referer |
Referer header |
client_ip |
Client IP address |
header_in[key] |
Any request header value |
cookie[key] |
Specific cookie value |
"string" or 'string' |
String literal |
Available Operators
| Operator | Syntax | Description |
|---|---|---|
contains |
operand contains "value" |
Substring match |
matches |
operand matches "regex" |
PCRE regex match |
equals |
operand equals "value" |
Exact string match |
belong_to |
client_ip belong_to "CIDR" |
IP belongs to CIDR range |
sqli_detn |
sqli_detn operand |
libinjection SQL injection detection (prefix operator) |
xss_detn |
xss_detn operand |
libinjection XSS detection (prefix operator) |
Note:
sqli_detnandxss_detnare prefix operators — they go before the operand, not between two operands. All other operators are infix (placed between two operands).
Logic Operators
Use and, or, not, and parentheses to build complex conditions.
Actions
| Action | Description |
|---|---|
return(N) |
Block with HTTP status code N |
allow |
Allow the request, skip remaining checks |
Example: Block Non-Local Admin Access
id: protect_admin
if: url matches "^/admin" and not (client_ip belong_to "10.0.0.0/8")
do: return(403)
Example: Detect SQL Injection in User-Agent
The sqli_detn operator uses libinjection to detect SQL injection payloads through lexical fingerprinting. Note the prefix syntax — the operator comes before the operand:
id: detect_sqli_in_ua
if: sqli_detn user_agent
do: return(403)
Example: Allow Trusted API Clients
When using allow rules to bypass WAF checks for specific clients, place ADV before other checks in waf_priority so the allow rule runs first:
waf_priority "ADV W-IP IP CC UNDER-ATTACK W-URL URL ARGS UA W-REFERER REFERER COOKIE";
Then in the advanced rule file:
id: trusted_api
if: header_in[X-API-Key] equals "your-secret-key" and url matches "^/api/"
do: allow
With this configuration, requests to /api/ with the correct X-API-Key header bypass all remaining WAF checks — even if the request body contains patterns that would normally trigger blocking.
Example: Multi-Rule File
id: block_bad_bots
if: user_agent contains "BadBot"
do: return(403)
id: protect_admin
if: url matches "^/admin" and not (client_ip belong_to "127.0.0.0/8")
do: return(404)
Testing Your NGINX WAF Module Configuration
Verify Syntax
Always test configuration changes before reloading:
sudo nginx -t
Test SQL Injection Blocking
curl -s -o /dev/null -w "%{http_code}" "http://localhost/?id=1+UNION+SELECT+1,2,3"
# Expected: 403
Test XSS Blocking
curl -s -o /dev/null -w "%{http_code}" "http://localhost/?q=<script>alert(1)</script>"
# Expected: 403
Test Scanner Blocking
curl -s -o /dev/null -w "%{http_code}" -A "sqlmap/1.0" http://localhost/
# Expected: 403
Test CC Rate Limiting
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code} " http://localhost/
done
# Expected: 200s followed by 429s (or 503s with default cc_deny status)
Verify Retry-After Header
When CC deny triggers, the response includes a Retry-After header:
curl -s -D- -o /dev/null http://localhost/ | grep Retry-After
# Expected: Retry-After: 3595 (remaining seconds of ban)
Test IP Whitelist Bypass
# Add your IP to the whitelist, reload, and verify attacks are allowed through
echo "127.0.0.1/32" >> /etc/nginx/waf-rules/white-ipv4
sudo systemctl reload nginx
curl -s -o /dev/null -w "%{http_code}" "http://localhost/?id=UNION+SELECT"
# Expected: 200 (whitelist bypasses all checks)
Performance Considerations
The NGINX WAF module is designed for low overhead:
- IP matching uses a prefix trie structure, giving O(prefix length) lookup time regardless of how many IP ranges are configured. This is significantly faster than linear scanning.
-
Enable caching with
waf_cache capacity=Nto avoid redundant regex matching. The module maintains per-location LRU caches for each inspection type. For high-traffic sites, increase the capacity value. -
libinjection fingerprinting is faster than regex for SQL/XSS detection because it uses lexical analysis rather than backtracking regex engines.
-
Advanced rules are compiled to bytecode at configuration time, so there is no parsing overhead at request time.
-
POST body inspection is the most expensive check because it reads the entire request body into memory. For upload-heavy endpoints, consider whitelisting those URLs to skip body inspection.
-
Shared memory for CC defense is allocated once at startup. The 20 MB minimum accommodates approximately 80,000 IP tracking entries. Increase the
sizeparameter for servers handling more concurrent clients.
Recommended Mode Selection
| Use case | Recommended mode |
|---|---|
| Static file server | STATIC — minimal overhead, no body inspection |
| Standard web app | STD — balanced protection with SQL injection detection |
| API server | DYNAMIC — includes cookie and advanced rule inspection |
| Under active attack | FULL — maximum protection including XSS detection |
Comparing the NGINX WAF Module to ModSecurity and NAXSI
If you are evaluating WAF options for NGINX, here is how the NGINX WAF module compares to the other two major choices:
| Feature | ngx_waf | ModSecurity | NAXSI |
|---|---|---|---|
| Configuration complexity | Low | High | Medium |
| Built-in rules | Yes (ships with defaults) | Via OWASP CRS (separate) | Minimal (learning mode) |
| SQL injection detection | Regex + libinjection | Regex + libinjection | Regex scoring |
| XSS detection | Regex + libinjection | Regex + libinjection | Regex scoring |
| Rate limiting | Built-in (CC defense) | Not built-in | Not built-in |
| Under Attack mode | Built-in | Not built-in | Not built-in |
| Custom rule language | Yes (DSL) | SecRule language | NAXSI rules |
| Memory footprint | Low | High | Low |
| False positive risk | Low (simple rules) | Moderate (complex CRS) | Low (whitelist model) |
Choose the NGINX WAF module when you want a single module that covers IP blocking, rate limiting, injection detection, and bot challenges without external dependencies. Choose ModSecurity when you need PCI-DSS compliance or the full OWASP Core Rule Set. Choose NAXSI when you prefer a deny-by-default (learning mode) approach.
Security Best Practices
1. Start with STD Mode, Then Expand
Begin with waf_mode STD and monitor the WAF log for false positives before enabling DYNAMIC or FULL:
waf_mode STD;
Review /var/log/nginx/waf-blocked.log regularly. If legitimate requests are blocked, add appropriate whitelist entries.
2. Whitelist Your API Endpoints
APIs often send payloads that trigger WAF rules (JSON with SQL-like strings, HTML content, etc.). Whitelist API paths to prevent false positives:
echo '^/api/' >> /etc/nginx/waf-rules/white-url
echo '^/webhook/' >> /etc/nginx/waf-rules/white-url
3. Whitelist Monitoring and Health Checks
Ensure your monitoring tools are not blocked by the NGINX WAF module:
echo "10.0.0.0/8" >> /etc/nginx/waf-rules/white-ipv4
4. Use Custom HTTP Status Codes
Return 429 for rate limiting (standard “Too Many Requests”) and 444 to silently drop connections from scanners:
waf_http_status general=444 cc_deny=429;
5. Keep Rule Files Under Version Control
Track your customized rule files in git to maintain an audit trail:
cd /etc/nginx/waf-rules
git init && git add -A && git commit -m "Initial WAF rules"
Troubleshooting the NGINX WAF Module
“No such file or directory” on nginx -t
All 15 rule files must exist in the waf_rule_path directory. Create any missing files as empty files:
for f in ipv4 ipv6 url args user-agent referer cookie post white-ipv4 white-ipv6 white-url white-referer advanced; do
touch /etc/nginx/waf-rules/$f
done
“invalid value” on nginx -t
Check the waf_cc_deny format. The rate value must include the r suffix and /m time unit. Duration and size values must include unit suffixes:
# Correct
waf_cc_deny rate=100r/m duration=1h size=20m;
# Wrong: missing 'r' in rate, missing suffix on duration
waf_cc_deny rate=100/m duration=3600 size=20m;
Rule File Syntax Error
Advanced rule files require precise formatting. Ensure:
– Each rule has exactly three lines: id:, if:, do:
– Rules are separated by exactly one blank line
– The file does not end with a trailing newline
– String values are quoted with double or single quotes
– sqli_detn and xss_detn are prefix operators (e.g., sqli_detn user_agent, not user_agent sqli_detn "1")
Legitimate Traffic Being Blocked
Check the WAF log to identify which rule triggered the block:
tail -f /var/log/nginx/waf-blocked.log
The rule_type and rule_details fields show exactly what matched. Add the legitimate pattern to the appropriate whitelist file, or whitelist the client IP.
High Memory Usage
If CC defense is consuming too much memory, reduce the shared memory size (minimum 20 MB) or shorten the ban duration so entries expire faster:
waf_cc_deny rate=100r/m duration=10m size=20m;
Conclusion
The NGINX WAF module provides a practical, low-overhead web application firewall that handles the most common attack vectors without the complexity of a full ModSecurity deployment. Its combination of regex rules, libinjection fingerprinting, IP tries, CC defense, and an advanced rule DSL covers a broad range of threats in a single, easy-to-configure module.
For the full source code and to report issues, visit the ngx_waf repository on GitHub. The NGINX WAF module is available as a pre-built package from the GetPageSpeed RPM repository and the GetPageSpeed APT repository.

