ModSecurity is the most widely used open-source web application firewall (WAF) engine. Combined with the OWASP Core Rule Set (CRS), it inspects HTTP requests and blocks common attacks such as SQL injection, cross-site scripting, path traversal, remote code execution attempts and scanner traffic before they reach your application. This guide sets it up on Nginx.
Read WAF explained first if you’re deciding whether you need a WAF at all.
Components
- libmodsecurity (v3) — the engine.
- ModSecurity-nginx connector — an Nginx dynamic module that passes requests to the engine.
- OWASP CRS — the rules.
Install on Ubuntu / Debian
Recent Debian and Ubuntu versions package the connector:
sudo apt install libnginx-mod-http-modsecurity modsecurity-crs
That installs the module (loaded via /etc/nginx/modules-enabled/), libmodsecurity and a packaged CRS. Check that your Nginx comes from the same distribution. Modules must match the Nginx build exactly. With Nginx from the official nginx.org repository, compile the connector as a dynamic module against that version (./configure --with-compat --add-dynamic-module=…).
Basic configuration
Start from the recommended config:
sudo mkdir -p /etc/nginx/modsec
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
sudo cp /usr/share/modsecurity-crs/crs-setup.conf.example /etc/nginx/modsec/crs-setup.conf 2>/dev/null || true
Create the main include file:
# /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/nginx/modsec/crs-setup.conf
Include /usr/share/modsecurity-crs/rules/*.conf
The CRS path differs between packages and versions. Check with ls /usr/share/modsecurity-crs/. Ubuntu’s modsecurity-crs package may already provide an include file under /etc/modsecurity/.
Then enable it in a server block:
server {
…
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
}
sudo nginx -t && sudo systemctl reload nginx
Start in detection-only mode
In /etc/nginx/modsec/modsecurity.conf:
SecRuleEngine DetectionOnly
SecAuditEngine RelevantOnly
SecAuditLog /var/log/nginx/modsec_audit.log
DetectionOnly logs what would be blocked without blocking it. Run in this mode for days or weeks of real traffic, and review the audit log. Every WAF produces false positives at first, especially with CMS admin areas, rich text editors and APIs that accept JSON or HTML.
Test that it sees attacks:
curl -s "https://example.com/?id=1%20UNION%20SELECT%20password%20FROM%20users" -o /dev/null -w '%{http_code}\n'
grep -c 'UNION' /var/log/nginx/modsec_audit.log
Tune false positives
Find which rules trigger on legitimate requests (rule IDs appear in the audit log, e.g. id "942100"). Then exclude them narrowly, for a path or a parameter, rather than disabling them globally:
# /etc/nginx/modsec/exclusions.conf (include it after the CRS rules)
# WordPress admin: allow HTML in post content
SecRule REQUEST_URI "@beginsWith /wp-admin/post.php" \
"id:1000,phase:1,pass,nolog,ctl:ruleRemoveTargetById=941100;ARGS:content"
# An API endpoint that legitimately receives SQL-like text
SecRule REQUEST_URI "@beginsWith /api/reports" \
"id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"
CRS also ships application exclusion packages (WordPress, Drupal, Nextcloud…). Enable the ones you need in crs-setup.conf or as plugins in CRS 4.
Paranoia level
The CRS paranoia level (PL1–PL4) trades protection against false positives. Start at PL1 (the default). Higher levels catch more but need much more tuning.
Anomaly scoring
CRS doesn’t block on a single rule match. Each match adds to an anomaly score, and the request is blocked when the score exceeds a threshold (default 5 for inbound). Raising the threshold temporarily (tx.inbound_anomaly_score_threshold) is a gentle way to switch to blocking mode.
Switch to blocking
When the logs show only real attacks:
SecRuleEngine On
Reload Nginx and watch the logs closely for a few days. Blocked requests get a 403.
Performance
ModSecurity adds CPU work to every request. On small VPS plans, a few milliseconds per request matters at high traffic. Exclude static files to reduce the cost:
location ~* \.(css|js|png|jpg|jpeg|gif|svg|webp|woff2?)$ {
modsecurity off;
expires 30d;
}
Limits
A WAF reduces risk but doesn’t replace patching. A vulnerable plugin is still vulnerable, and the WAF just makes some exploit attempts harder. Keep software updated, and combine the WAF with rate limiting and Fail2Ban. See Fail2Ban for Nginx and Nginx rate limiting.
Quick reference
| Task | Setting / command |
|---|---|
| Install (Debian/Ubuntu) | apt install libnginx-mod-http-modsecurity modsecurity-crs |
| Enable per site | modsecurity on; modsecurity_rules_file …; |
| Learning mode | SecRuleEngine DetectionOnly |
| Blocking mode | SecRuleEngine On |
| Audit log | SecAuditLog /var/log/nginx/modsec_audit.log |
| Remove rule for a path | ctl:ruleRemoveById=ID |
Related: Nginx security headers and WordPress hardening.