Logo

ModSecurity + OWASP CRS: WAF for Nginx

ModSecurity + OWASP CRS: WAF for Nginx

Put a WAF (Web Application Firewall) in front of your websites to block SQLi, XSS, RFI, scans, and CVE exploits. ModSecurity v3 + OWASP Core Rule Set, the industry-standard open-source pair.

Introduction

A website faces hundreds of automated attacks continuously: SQL injections, XSS, bots scanning for WordPress CVEs, etc.

ModSecurity intercepts HTTP/S requests and evaluates them against a rule set before passing them to your application. Paired with the OWASP Core Rule Set (CRS), you block:

  • SQLi (SQL injection)
  • XSS (Cross-site scripting)
  • RFI / LFI (Remote / Local File Inclusion)
  • Command injection
  • Scanners (Nikto, sqlmap, etc.)
  • Known CVE exploits (Log4Shell, Heartbleed, etc.)

Prerequisites

  • Nginx installed (1.18+)
  • VPS Debian / Ubuntu
  • Root access

Step 1: Installation

sudo apt update
sudo apt install -y libmodsecurity3 libmodsecurity-dev nginx-module-modsecurity

Enable module in /etc/nginx/nginx.conf:

load_module modules/ngx_http_modsecurity_module.so;

http {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;
}

Step 2: Base configuration

sudo mkdir -p /etc/nginx/modsec
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
sudo nano /etc/nginx/modsec/modsecurity.conf

Set SecRuleEngine:

SecRuleEngine DetectionOnly

⚠️ Start in DetectionOnly (log only) to identify false positives before switching to On (blocking).

Step 3: Install OWASP CRS

cd /etc/nginx/modsec
sudo git clone https://github.com/coreruleset/coreruleset.git
sudo cp coreruleset/crs-setup.conf.example coreruleset/crs-setup.conf

Step 4: main.conf

sudo nano /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/nginx/modsec/coreruleset/crs-setup.conf
Include /etc/nginx/modsec/coreruleset/rules/*.conf

Step 5: Test and activate

sudo nginx -t
sudo systemctl reload nginx

SQLi test:

curl "http://your-site.com/?id=1' OR '1'='1"

Check logs:

sudo tail -f /var/log/modsec_audit.log

Step 6: Tune false positives

Let it run in DetectionOnly for a few days then analyze:

sudo grep -E "MATCHES|Detected" /var/log/modsec_audit.log | head

Exclude a rule for a specific site:

server {
    location /admin/ {
        modsecurity_rules '
            SecRuleRemoveById 941100
            SecRuleRemoveById 942100
        ';
    }
}

Or globally, create /etc/nginx/modsec/coreruleset/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:

SecRule REQUEST_URI "@beginsWith /api/" "id:1001,phase:1,nolog,allow,ctl:ruleRemoveById=920420"

Step 7: Enable blocking mode

Once false positives sorted:

SecRuleEngine On
sudo systemctl reload nginx

ModSecurity now blocks with HTTP 403.

Step 8: Paranoia levels

CRS supports 4 levels (1-4):

  • PL1: essential rules, few false positives (default)
  • PL2: stricter
  • PL3: strict, many FPs likely
  • PL4: paranoid, critical contexts only
SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=2"

Step 9: Anomaly scoring

SecAction "id:900110,phase:1,nolog,pass,t:none,
    setvar:tx.inbound_anomaly_score_threshold=5,
    setvar:tx.outbound_anomaly_score_threshold=4"

Lower the threshold for stricter blocking.

Step 10: Custom rules

Block an IP:

SecRule REMOTE_ADDR "@ipMatch 1.2.3.4" "id:1000,phase:1,deny,status:403"

Block a User-Agent:

SecRule REQUEST_HEADERS:User-Agent "@contains sqlmap" "id:1001,phase:1,deny,status:403"

Rate-limit on /login:

SecAction "id:1002,phase:1,nolog,pass,initcol:ip=%{REMOTE_ADDR},chain"
SecRule REQUEST_URI "@beginsWith /login" "chain"
SecAction "id:1003,setvar:ip.attempts=+1,expirevar:ip.attempts=60"
SecRule IP:attempts "@gt 5" "id:1004,deny,status:429"

Step 11: Performance

ModSecurity adds 5-15ms per request on average:

SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
location / {
    modsecurity on;
}

location /static/ {
    modsecurity off;
}

Step 12: Monitoring

Daily block count:

sudo grep "Access denied" /var/log/nginx/error.log | wc -l

Top triggered rules:

sudo grep "Matched" /var/log/modsec_audit.log | grep -oP 'id "\d+"' | sort | uniq -c | sort -rn | head

Troubleshooting

"modsecurity: SecRuleEngine On - failed"

Invalid rule syntax. Check:

sudo nginx -t

Too many false positives

Go back to DetectionOnly, analyze patterns, tune with targeted exclusions.

Performance degraded

Disable body scan for large uploads:

location /upload {
    modsecurity_rules '
        SecRequestBodyAccess Off
    ';
}

Logs too verbose

SecAuditLogParts ABCFHZ
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"

Useful commands

sudo nginx -t
sudo systemctl reload nginx
sudo tail -f /var/log/modsec_audit.log
sudo tail -f /var/log/nginx/error.log
sudo grep "id \"941100\"" /var/log/modsec_audit.log

# Update CRS
cd /etc/nginx/modsec/coreruleset
sudo git pull
sudo systemctl reload nginx

Conclusion

ModSecurity + OWASP CRS blocks most automated web attacks without modifying your application. Pros:

  • Coverage of common web CVEs
  • Free OWASP updates
  • Detailed logs for analysis
  • Acceptable performance (<15ms)

Going further:

  • Combine with Fail2ban to persistently block repeat offenders
  • Use CrowdSec for collaborative intelligence
  • For SaaS, also consider NAXSI (lighter alternative)

Resources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members