Skip to main content

NGINX / Security

NGINX WAF Module: Lightweight Web Application Firewall

by , , revisited on


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.

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:

  1. 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.

  2. 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 Requests response with a Retry-After header.

  3. 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.

  4. URL, query string, and header inspection — The request URI, query string arguments, User-Agent, Referer, and Cookie headers are matched against regex-based blacklists and whitelists.

  5. 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.

  6. 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.

  7. 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_module directive 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_path value 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 waf is on)

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 like onerror=, onload=
  • Path traversal: ../ sequences, .svn, .htaccess, .bash_history access
  • Scanner tools: sqlmap, Nikto, nmap, dirbuster, hydra, zgrab, HTTrack, w3af, and 20+ other known scanners
  • Dangerous files: .bak, .sql, .backup, .mdb, .old extensions
  • 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_detn and xss_detn are 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=N to 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 size parameter for servers handling more concurrent clients.

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.

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.