NGINX ModSecurity Setup on Debian and Ubuntu: WAF with OWASP Core Rule Set

ModSecurity is the Web Application Firewall that sits in front of your NGINX-served app and blocks attacks before they ever reach your PHP, your database, or your unsuspecting plugins. SQL injection. Cross-site scripting. Path traversal. File inclusion. Scanner traffic. Bad bots. They all have known HTTP fingerprints, and ModSecurity NGINX setup with the OWASP Core Rule Set blocks the vast majority of them automatically. This is the NGINX modsecurity setup guide for the Debian and Ubuntu way — packaged, signed, and ready in about ten minutes.

The myguard APT repository ships ModSecurity v3 as a pre-built dynamic NGINX module. No compiling from source, no dependency hell, no “wait, which version of libcre2 did you say?” energy. Add the repo, install one package, configure, done.

If you’d rather follow a more conversational version of this with longer explanations, our step-by-step ModSecurity tutorial walks through the same install with a friendlier tone. This page is the no-nonsense Debian/Ubuntu reference.

Why You Actually Need a WAF in 2026

Honest truth: your application has vulnerabilities you don’t know about yet. Every WordPress plugin, every Composer dependency, every random JavaScript package — they all have bugs that get discovered, exploited, and published faster than you can read the CVE feed. On any given day, real exploit attempts are running against your server right now, probing for known weaknesses.

A WAF doesn’t replace patching. But it buys you time. When a WordPress plugin CVE drops on a Monday and the patch lands on Wednesday, a properly configured NGINX web application firewall blocks the exploit attempts during those 48 hours — often because the attack pattern matches a generic OWASP CRS rule, not a CVE-specific one. That is genuinely the difference between a quiet week and a 3am incident bridge.

The OWASP Core Rule Set is maintained by a small army of security researchers, covers the OWASP Top 10 attack categories, and is the industry-standard baseline for HTTP-layer protection. ModSecurity owasp crs nginx is the combination most production sites end up with, regardless of whether they admit it.

ModSecurity v3 vs v2: What’s Different in the NGINX World

ModSecurity v2 was an Apache module. ModSecurity v3 (libmodsecurity3) is a standalone library with a connector model — the WAF logic lives in the library, and thin connector modules bridge it to your web server. This means the same engine works with NGINX, Angie, Apache, IIS, and more. One library, many doors.

What changed in v3 that matters for an NGINX WAF Debian Ubuntu deployment:

  • Performance — v3 evaluates rules more efficiently, with lower per-request overhead.
  • Dynamic rule loading — rules can be refreshed without restarting NGINX (with the right config).
  • Better request body inspection — JSON, XML, multipart are all properly parsed.
  • Custom rule compatibility — most v2 rules port, but not all directives survived the rewrite.

Step 1 — Install ModSecurity on Debian or Ubuntu

# Add the myguard repository (if you haven't yet — takes about 30 seconds)
# Full setup: /how-to-use/

# Install the ModSecurity v3 library and the NGINX connector module
sudo apt update
sudo apt install libmodsecurity3 libnginx-mod-http-modsecurity

# Verify the NGINX module loaded
nginx -V 2>&1 | grep -i modsecurity || ls /etc/nginx/modules-enabled/

Step 2 — Confirm the Module Is Loaded

NGINX needs to actually load the ModSecurity dynamic module. The myguard package drops a config snippet in /etc/nginx/modules-enabled/ automatically:

cat /etc/nginx/modules-enabled/50-mod-http-modsecurity.conf
# Should contain:
# load_module modules/ngx_http_modsecurity_module.so;

If you ever uninstall and reinstall, double-check this file exists. If it doesn’t, NGINX won’t know ModSecurity is there.

Step 3 — Configure ModSecurity Itself

# ModSecurity config directory
sudo mkdir -p /etc/nginx/modsec

# Copy the recommended base config
sudo cp /usr/share/modsecurity-crs/modsecurity.conf-recommended         /etc/nginx/modsec/modsecurity.conf

# IMPORTANT: leave the engine in DetectionOnly mode for the first week
# (this is the default in the recommended config)
grep ^SecRuleEngine /etc/nginx/modsec/modsecurity.conf
# Expected: SecRuleEngine DetectionOnly

Do not skip the DetectionOnly week. Log what would be blocked, tune out false positives, then switch to SecRuleEngine On. Flipping straight to blocking on a live WordPress site is how you get an angry phone call from the marketing team.

Step 4 — Install the OWASP Core Rule Set

# Install CRS via apt (recommended on Debian and Ubuntu)
sudo apt install modsecurity-crs

# Or pull the latest directly from upstream
sudo git clone https://github.com/coreruleset/coreruleset /etc/modsecurity-crs
sudo cp /etc/modsecurity-crs/crs-setup.conf.example         /etc/modsecurity-crs/crs-setup.conf

# Build the ModSecurity main include file
sudo tee /etc/nginx/modsec/main.conf > /dev/null <<'EOF'
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/modsecurity-crs/crs-setup.conf
Include /etc/modsecurity-crs/rules/*.conf
EOF

Step 5 — Turn On ModSecurity in Your Server Block

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    # Activate the NGINX WAF
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    # ... rest of your config (SSL, root, fastcgi_pass, etc.)
}

Reload NGINX:

sudo nginx -t && sudo systemctl reload nginx

CRS Paranoia Levels: The One Setting That Matters Most

The OWASP Core Rule Set has four paranoia levels. They control how aggressively the WAF flags requests. This is the single most important tuning decision you will make:

  • Level 1 (default) — only obvious attacks. Almost no false positives. Suitable for most websites out of the box.
  • Level 2 — more comprehensive. Some false positives on complex apps. The recommended target for public-facing APIs and WordPress installs.
  • Level 3 — aggressive. Significant false positives. Only run this if you have time to whitelist a long tail of edge cases.
  • Level 4 — paranoid. Unsuitable for most production traffic without sustained tuning effort.

Set the paranoia level in /etc/modsecurity-crs/crs-setup.conf by uncommenting the appropriate setvar:tx.paranoia_level line. Most production NGINX modsecurity setup deployments land on level 2.

WordPress-Specific CRS Tuning

WordPress generates HTTP traffic that CRS level 2 sometimes mis-flags. Large media uploads, base64-encoded Gutenberg block data, complex admin AJAX — all legitimate, all noisy. The CRS project maintains an official WordPress exclusion plugin to suppress these false positives without dropping your overall protection:

# In main.conf, the WordPress exclusions must come BEFORE the CRS rules
sudo tee /etc/nginx/modsec/main.conf > /dev/null <<'EOF'
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/modsecurity-crs/crs-setup.conf
Include /etc/modsecurity-crs/plugins/wordpress-rule-exclusions-before.conf
Include /etc/modsecurity-crs/rules/*.conf
Include /etc/modsecurity-crs/plugins/wordpress-rule-exclusions-after.conf
EOF

If the exclusion files aren’t present yet, grab them from the CRS plugins repository on GitHub. They are independently versioned and shouldn’t be edited in place.

Reading the Audit Log Without Going Insane

# Tail the audit log live
tail -f /var/log/modsec_audit.log

# Or watch the NGINX error log for ModSecurity messages
tail -f /var/log/nginx/error.log | grep ModSecurity

# Aggregate top triggering rule IDs (great for triage)
grep -oE 'id "[0-9]+' /var/log/modsec_audit.log | sort | uniq -c | sort -rn | head -20

Focus on the top triggers first. If rule 920350 (Host header is an IP address) keeps firing on your uptime monitor, whitelist the monitor’s IP. If rule 942100 (SQL injection via libinjection) lights up on a legitimate API call, investigate — sometimes it really is a false positive, and sometimes you have found yourself a genuine vulnerability you didn’t know about.

Whitelisting False Positives Cleanly

When you confirm a legitimate request is being blocked, whitelist surgically. Don’t disable entire rule groups; whitelist specific rule IDs for specific URIs or IP ranges:

# /etc/nginx/modsec/exceptions.conf
# Trust the monitoring IPs entirely for rule 920350
SecRule REMOTE_ADDR "@ipMatch 192.168.1.10,10.0.0.5" \
  "id:1000,phase:1,pass,nolog,ctl:ruleRemoveById=920350"

# Allow the /api/data endpoint to receive SQL-looking strings
SecRule REQUEST_URI "@beginsWith /api/data" \
  "id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"

# Include BEFORE the CRS rules block in main.conf

Per-Location WAF Tuning

You can disable the NGINX web application firewall entirely on safe locations (such as health checks) or apply stricter rules on sensitive ones (like login pages):

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

    # No need for WAF on the health check (no user input)
    location = /health {
        modsecurity off;
        return 200 'OK';
    }

    # Stricter rules for the WordPress login endpoint
    location /wp-login.php {
        modsecurity on;
        modsecurity_rules_file /etc/nginx/modsec/strict.conf;
    }
}

Keeping CRS Updated

The OWASP CRS team ships meaningful updates every few months. If you installed via apt, your package manager does the work:

sudo apt update && sudo apt upgrade modsecurity-crs
sudo nginx -t && sudo systemctl reload nginx

If you installed via git, pull the upstream repository:

cd /etc/modsecurity-crs
sudo git pull
sudo nginx -t && sudo systemctl reload nginx

Performance Impact of ModSecurity on NGINX

ModSecurity v3 with CRS at paranoia level 1-2 adds roughly 1-3ms per request on a modern Debian or Ubuntu server. On a server handling 1,000 requests per second, that translates to about 3% extra CPU at level 2 — almost always invisible to users, almost never a hosting bottleneck.

At paranoia level 3-4 the overhead climbs significantly because more rules run per request. For most sites running an NGINX WAF Debian Ubuntu stack, level 2 is the practical ceiling without dedicated WAF hardware. Easy ways to shave overhead:

  • Disable ModSecurity on static-asset locations (images, CSS, JS — no attack surface).
  • Use SecRequestBodyAccess Off in locations that never receive a request body.
  • Skip the WAF entirely for trusted internal IPs (monitoring, load balancer health checks).

ModSecurity Plus PHP-Snuffleupagus = Defence in Depth

ModSecurity lives in NGINX and filters at the HTTP layer — that’s everything that arrives over the wire. PHP-Snuffleupagus lives inside PHP-FPM and controls what PHP is allowed to do once a request has been allowed through. Run both. ModSecurity stops the obvious. Snuffleupagus catches what slips past. Together they cover the two layers an attacker has to defeat to do real damage.

Frequently Asked Questions

What is the difference between detection mode and blocking mode?
In detection mode (SecRuleEngine DetectionOnly), ModSecurity logs every rule match but passes every request through unchanged. In blocking mode (SecRuleEngine On), matches result in an immediate 403. Always start in detection mode, review logs for a week to tune out false positives, then flip to blocking.
Will ModSecurity break my WordPress site?
At paranoia level 1, almost certainly not. At level 2, the WordPress-specific exclusion rules handle the most common false positives — base64 block data, large uploads, admin AJAX. The safe path: install in DetectionOnly, run for a week, fix the false positives you see, then enable blocking.
Does ModSecurity protect against zero-day vulnerabilities?
Often yes. Most exploits for newly disclosed PHP application vulnerabilities use HTTP patterns that already match generic CRS rules. A SQLi attempt for a brand-new WordPress plugin CVE will usually match the generic SQL injection rule even if no CVE-specific signature exists yet. That virtual-patching window is one of the WAF’s biggest practical wins.
How do I check which rule blocked a request?
Check /var/log/modsec_audit.log for the full transaction record (the matching rule ID, the request data, and the anomaly score). The NGINX error log also surfaces ModSecurity messages: tail -f /var/log/nginx/error.log | grep ModSecurity.
Can I use ModSecurity with Angie instead of NGINX?
Yes. The myguard repository ships an Angie ModSecurity connector module alongside the NGINX one. Install angie-module-http-modsecurity instead of libnginx-mod-http-modsecurity — the rule configuration is identical.
Is ModSecurity v3 compatible with all v2 rules?
Mostly, but not fully. Stock OWASP CRS rules work cleanly in both versions. However, some v2 custom rules use directives that were removed or changed in v3 (the @inspectFile operator and some transformation functions, for example). Audit each custom rule before porting from v2 to v3.
How often should I update the OWASP CRS?
The OWASP CRS project releases new versions every few months. If you installed via apt on Debian or Ubuntu, apt-get upgrade catches the updates. Subscribe to the OWASP CRS release announcements so you can apply security-critical updates immediately rather than at the next maintenance window.

Related Posts