ModSecurity WAF Rules in Practice: OWASP CRS Core Rule Configuration
Integrate the open-source ModSecurity WAF and OWASP CRS on Nginx to automatically block SQL injection, XSS, command injection, and other web attacks. Includes full config, rule tuning, and verification commands.
This article shows how to integrate ModSecurity and OWASP CRS on Nginx. The stack automatically blocks SQL injection, XSS, command injection, and similar attacks. Full configs and verification commands are included.
Use cases
- Sites without a commercial WAF that need a low-cost self-hosted WAF
- Compliance requirements that mandate a WAF or web-attack protection
- Need to uniformly block SQL injection, XSS, command injection, and other OWASP Top 10 attacks
- Need web-layer attack protection without changing application code
Prerequisites
- Linux server (this tutorial uses Ubuntu 22.04 + Nginx)
- Nginx installed and the site reachable
- root or sudo access
- Domain resolved to the server (this tutorial uses example.com)
How it works
ModSecurity is an open-source WAF engine. It is rule-driven: each request is matched against a set of security rules. A hit runs the matching action (log, block, or pass). OWASP CRS (Core Rule Set) is the official recommended core rule set for ModSecurity. It contains hundreds of rules covering OWASP Top 10, grouped into injection, XSS, protocol violation, malicious file upload, and similar categories. ModSecurity embeds into Nginx via `ngx_http_modsecurity_module` and inspects/blocks in the `access` phase.
Steps
1. Build Nginx with the ModSecurity module
Official Nginx binaries do not ship ModSecurity. Compile it in. To simplify, use the libmodsecurity3 dynamic module:
# Install build dependencies
sudo apt update && sudo apt install -y build-essential libpcre3-dev libxml2-dev libcurl4-openssl-dev libyajl-dev liblua5.1-0-dev pkgconf
# Clone ModSecurity source and build the dynamic module
git clone --depth 1 -b v3.0.12 https://github.com/owasp-modsecurity/ModSecurity
cd ModSecurity
./build.sh
./configure --with-yajl
make -j4 && sudo make install
Then build the `modsecurity-nginx` connector dynamic module:
git clone --depth 1 https://github.com/owasp-modsecurity/modsecurity-nginx
cd modsecurity-nginx
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar xzf nginx-1.24.0.tar.gz && cd nginx-1.24.0
./configure --with-compat --add-dynamic-module=../modsecurity-nginx
make modules
sudo cp objs/ngx_http_modsecurity_module.so /usr/lib/nginx/modules/
2. Load the module and download OWASP CRS
# Load the dynamic module at the top of /etc/nginx/nginx.conf
echo 'load_module modules/ngx_http_modsecurity_module.so;' | sudo tee -a /etc/nginx/nginx.conf
# Download the OWASP CRS rule set
sudo mkdir -p /etc/nginx/modsecurity /etc/nginx/crs
cd /etc/nginx/modsecurity
sudo cp /usr/local/modsecurity/bin/modsecurity.conf-recommended modsecurity.conf
sudo cp /usr/local/modsecurity/unicode.mapping .
cd /etc/nginx/crs
sudo git clone --depth 1 -b v3.3.5 https://github.com/coreruleset/coreruleset .
sudo cp crs-setup.conf.example crs-setup.conf
sudo ln -s /etc/nginx/crs/rules /etc/nginx/crs/rules
3. Configure the ModSecurity main config
Edit `/etc/nginx/modsecurity/modsecurity.conf`. Set detection mode and audit logging:
# In production, start with DetectionOnly to watch false positives; switch to On once stable
SecRuleEngine DetectionOnly
# Request body limits (keep in sync with Nginx client_max_body_size)
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
# Audit log
SecAuditEngine RelevantOnly
SecAuditLog /var/log/nginx/modsec_audit.log
SecAuditLogType Serial
# Upload temp directory (required)
SecTmpDir /tmp/
SecDataDir /tmp/
4. Enable ModSecurity on the Nginx site
server {
listen 80;
server_name example.com;
# Enable ModSecurity and the CRS rule set
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
}
Also include CRS rules. Append these two lines at the end of `modsecurity.conf` to include the CRS config and rules:
Include /etc/nginx/crs/crs-setup.conf
Include /etc/nginx/crs/rules/*.conf
5. Tune key CRS parameters
Edit `/etc/nginx/crs/crs-setup.conf`. Focus on these parameters:
# Anomaly score threshold (default 5 severity levels; keep the default)
# Set to 100000 to disable blocking and log only (for troubleshooting)
SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.blocking_paranoia_level=1"
# Whitelist protected business paths (optional; avoid blocking admin backends)
SecRule REQUEST_URI "@beginsWith /wp-admin/" "id:900100,phase:1,nolog,pass,t:none,ctl:ruleRemoveById=942100"
If false positives are high, keep `SecRuleEngine DetectionOnly` for a few days. Watch the audit log for hits on legitimate traffic. Add targeted whitelist rules, then switch to `On`.
6. Reload Nginx and verify the module
sudo nginx -t
sudo systemctl reload nginx
# Verify the module is loaded
nginx -V 2>&1 | grep modsecurity
Verification
Send typical attack payloads with curl to verify blocking:
# Simulate SQL injection (should trigger 942-series rules)
curl -i "http://example.com/?id=1%27%20OR%201%3D1%20--%20"
# Simulate XSS (should trigger 941-series rules)
curl -i "http://example.com/?q=<script>alert(1)</script>"
# Check audit log for rule hits
tail -20 /var/log/nginx/modsec_audit.log | grep -E "id "9[0-9]{4}""
In `SecRuleEngine On` mode, attack requests should return 403. In `DetectionOnly` mode, requests pass but the audit log records matched rule IDs. Hits starting with `9` (e.g. 942100, 941100) mean CRS detection is working.
FAQ
FAQ 1: Legitimate traffic is blocked (false positives)
Run `SecRuleEngine DetectionOnly` first and watch. Use the audit log to find the false-positive rule IDs. Then add a targeted whitelist in `crs-setup.conf`: `SecRule REQUEST_URI "@beginsWith /api/v1/report" "id:900101,phase:1,pass,nolog,ctl:ruleRemoveById=942100"`. You can also lower sensitivity with a higher anomaly threshold (e.g. `setvar:tx.inbound_anomaly_score_threshold=100`). Tighten later once the rules are familiar.
FAQ 2: Nginx fails to start after loading the module
Two common causes. First: the `load_module` directive must sit before the `events` block in `nginx.conf`, and the path must match the compiled `.so`. Second: files referenced by `modsecurity.conf` such as `unicode.mapping` are missing. `sudo nginx -t` names the missing file; add it and reload. On CentOS and similar, SELinux may block the module from reading rule files. Use `setsebool -P httpd_can_network_connect 1` or fix the SELinux context.
Summary
Nginx + ModSecurity + OWASP CRS adds OWASP Top 10 protection without changing application code. Run DetectionOnly first. Use the audit log to pinpoint false positives. Whitelist and tune thresholds, then switch to blocking. ModSecurity rule config is a practical ops skill. Combined with CRS knowledge and log analysis, it raises defense against automated attacks.