How to Install ModSecurity and OWASP CRS on NGINX (Step-by-Step)

Imagine your website is a nightclub. Right now, the door is wide open. Anyone can wander in — paying guests, sure, but also the guy who wants to spray-paint the walls, the one rifling through the coat check, and a suspicious number of robots all wearing the same trench coat trying the back door 4,000 times a minute. You need a bouncer. A good one. One who knows every troublemaker trick in the book and stops them on the pavement, before they ever set foot inside.

That bouncer is a Web Application Firewall (WAF), and in the open-source world the gold-standard combo is ModSecurity (the bouncer) plus the OWASP Core Rule Set (the bouncer’s encyclopaedic memory of every known troublemaker). This guide walks you through how to install ModSecurity and the OWASP CRS on NGINX, step by step, from absolutely zero. No prior WAF experience needed. If you can copy and paste into a terminal and not panic when text scrolls past, you can do this.

What Is ModSecurity, and What Is the OWASP CRS?

Let’s get the vocabulary sorted, because these two things get confused constantly and they are not the same.

ModSecurity is the engine. It’s a piece of software that plugs into your web server (NGINX, Apache, or via the modern Coraza/libmodsecurity builds) and inspects every single HTTP request before your application ever sees it. By itself, ModSecurity is a bouncer with no memory — it can stop people, but it doesn’t know who to stop. It needs rules.

The OWASP Core Rule Set (CRS) is that memory. It’s a free, community-maintained, battle-tested collection of detection rules — thousands of them — covering SQL injection, cross-site scripting (XSS), path traversal, remote code execution, protocol abuse, and basically every entry in the OWASP Top 10. The OWASP Foundation maintains it, real security professionals contribute to it, and it’s the de-facto standard ruleset for ModSecurity worldwide.

Put simply: ModSecurity is the muscle, the CRS is the brain. You need both. Installing one without the other is like hiring a bouncer and never telling him who’s banned.

Before You Start: What You Need

  • A server running Debian or Ubuntu with NGINX installed (this guide assumes Debian/Ubuntu paths).
  • Root or sudo access. You’ll be editing system config and reloading NGINX.
  • An NGINX build with the ModSecurity module. This is the part most tutorials hand-wave. Stock distro NGINX does not include ModSecurity — you’d normally have to compile NGINX from source with --add-module, which is a multi-hour adventure in dependency pain. Our optimized NGINX builds for Debian and Ubuntu ship the http-modsecurity module precompiled, so this becomes a one-line apt install. We’ll assume that route.
  • About 20 minutes and a cup of something warm.

Step 1 — Install NGINX With the ModSecurity Module

If you’re using our repository, the ModSecurity-enabled NGINX and the dynamic module come straight from apt:

# Install NGINX and the ModSecurity dynamic module
sudo apt update
sudo apt install nginx nginx-module-modsecurity

# Verify the module file exists
ls -l /usr/lib/nginx/modules/ | grep modsecurity

You should see something like ngx_http_modsecurity_module.so. That .so file is the bridge between NGINX and the ModSecurity engine. If you compiled NGINX yourself instead, the module lives wherever your --modules-path pointed — adjust accordingly.

Step 2 — Load the ModSecurity Module in NGINX

NGINX won’t use the module until you tell it to. Open your main config (/etc/nginx/nginx.conf) and add this line at the very top, before the events {} block — load_module directives have to come first:

load_module modules/ngx_http_modsecurity_module.so;

Then, inside the http {} block, switch ModSecurity on and point it at a rules file we’ll create in a moment:

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

    # ... your existing http config ...
}

modsecurity on; arms the engine globally. modsecurity_rules_file tells it which rulebook to read. You can also place modsecurity on; inside a specific server {} or location {} block if you only want to protect part of your site — but for a WordPress site, protecting everything is the right call.

Step 3 — Install the ModSecurity Base Configuration

ModSecurity ships a recommended baseline config. We’ll set up a clean directory and wire it together:

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

# Grab the recommended base config
sudo wget -O /etc/nginx/modsec/modsecurity.conf 
  https://raw.githubusercontent.com/owasp-modsecurity/ModSecurity/v3/master/modsecurity.conf-recommended

# Grab the unicode mapping file it references
sudo wget -O /etc/nginx/modsec/unicode.mapping 
  https://raw.githubusercontent.com/owasp-modsecurity/ModSecurity/v3/master/unicode.mapping

The single most important setting in that base file is the engine mode. Open /etc/nginx/modsec/modsecurity.conf and find this line:

SecRuleEngine DetectionOnly

Leave it as DetectionOnly for now. This is the golden rule of WAF deployment that everyone ignores and then regrets. In DetectionOnly mode, ModSecurity watches everything and logs what it would have blocked — but doesn’t actually block anything. This lets you discover false positives (legitimate traffic the rules mistakenly flag) before they take your site down. We’ll flip it to full blocking in Step 6, after we’ve checked the logs. Patience now saves a 3 a.m. outage later.

Step 4 — Install the OWASP Core Rule Set

Now the brain. We’ll pull the latest CRS 4.x release and put it where ModSecurity can find it:

# Clone the OWASP CRS (v4 branch)
cd /etc/nginx/modsec
sudo git clone https://github.com/coreruleset/coreruleset.git owasp-crs

# Activate the CRS setup file (it ships as .example so you can customise safely)
sudo cp owasp-crs/crs-setup.conf.example owasp-crs/crs-setup.conf

The crs-setup.conf file is where you tune the CRS’s behaviour — most importantly the Paranoia Level. Quick explainer: paranoia level is a dial from 1 to 4 that controls how suspicious the rules are. PL1 (the default) catches obvious attacks with very few false positives — the right starting point for almost everyone. PL4 catches everything including its own shadow and will absolutely flag legitimate traffic. Start at PL1. You can raise it later once you understand your traffic.

Step 5 — Wire It All Together

Remember modsecurity_rules_file /etc/nginx/modsec/main.conf; from Step 2? We need to create that main.conf. It’s the master include file that loads everything in the correct order — base config first, then CRS setup, then the CRS rules themselves:

sudo tee /etc/nginx/modsec/main.conf > /dev/null <<'EOF'
# 1. ModSecurity base configuration
Include /etc/nginx/modsec/modsecurity.conf

# 2. OWASP CRS setup (paranoia level, anomaly thresholds, etc.)
Include /etc/nginx/modsec/owasp-crs/crs-setup.conf

# 3. The actual OWASP CRS detection rules
Include /etc/nginx/modsec/owasp-crs/rules/*.conf
EOF

Order matters here. The base config sets the engine behaviour, crs-setup.conf configures the ruleset, and the rules/*.conf glob pulls in every detection rule. Get the order wrong and rules reference variables that don’t exist yet.

Now test the NGINX config and reload:

sudo nginx -t
sudo systemctl reload nginx

If nginx -t says syntax is ok and test is successful, congratulations — ModSecurity and the OWASP CRS are now running on your server in detection mode. The bouncer is at the door, watching, taking notes, not yet throwing anyone out.

Step 6 — Watch the Logs, Then Go Live

This is the step that separates people whose sites stay online from people who tweet “why is my site down” at midnight. Let detection mode run for a few days under real traffic. Then read the audit log to see what the CRS flagged:

# The audit log location is set in modsecurity.conf (SecAuditLog)
sudo tail -f /var/log/modsec_audit.log

You’re hunting for false positives — legitimate things on your site (a plugin’s AJAX call, a form with rich text, an admin action) that the rules flagged as attacks. For each one, you write a targeted exclusion rule so that specific legitimate request is allowed, without weakening protection everywhere else. For WordPress specifically, this is largely a solved problem — see the next section.

Once the logs are clean (or you’ve excluded the known false positives), flip the switch. Edit /etc/nginx/modsec/modsecurity.conf:

# Change this:
SecRuleEngine DetectionOnly
# To this:
SecRuleEngine On
sudo nginx -t && sudo systemctl reload nginx

The bouncer is now active. Attacks get blocked before they reach PHP, MySQL, or WordPress. You did it.

WordPress-Specific Tuning: Don’t Skip This

The stock CRS is a generalist — it protects a banking app and a recipe blog with the same rules. WordPress has very specific quirks (the Gutenberg editor sends complex POST bodies that can trip XSS rules; the REST API has its own patterns). Running raw CRS against WordPress without tuning will produce false positives in the admin area.

Two free CRS plugins solve this, and you should install both:

  • wordpress-rule-exclusions-plugin — official CRS plugin that suppresses the known WordPress false positives (Gutenberg, the customizer, etc.) so the admin area works normally.
  • wordpress-hardening-plugin — adds 25+ extra WordPress-specific protections on top of CRS: blocking xmlrpc.php, stopping user enumeration, rate-limiting wp-login.php brute force, GeoIP login restrictions, and IP reputation blocking. We wrote a full deep-dive on it — it turns generic CRS into a WordPress-aware fortress.

Both follow the CRS 4.0 plugin standard — drop them in the owasp-crs/plugins/ directory and they load automatically. Exclusions plugin first (removes false positives), hardening plugin second (adds protection). Together they give you a WAF that actually understands WordPress.

Related Reading

FAQ

Does ModSecurity slow down NGINX?

There’s a small per-request CPU cost for rule inspection, typically a few milliseconds at Paranoia Level 1. For most sites this is completely unnoticeable and is massively outweighed by the benefit: blocked malicious requests never reach PHP, MySQL or WordPress, which actually reduces total server load during an attack. Keep the paranoia level sensible (PL1 for most sites) and the overhead stays negligible.

What’s the difference between ModSecurity and the OWASP CRS?

ModSecurity is the WAF engine — the software that inspects requests. The OWASP Core Rule Set is the collection of detection rules that tells the engine what an attack looks like. ModSecurity without rules does nothing useful; the CRS without an engine is just text files. You install both together: the engine plus its rulebook.

Why should I start in DetectionOnly mode?

Because every site has unique traffic, and the CRS occasionally flags legitimate requests as attacks (false positives). If you go straight to blocking mode, those false positives become real outages — broken admin pages, failed form submissions, locked-out users. DetectionOnly lets you observe what would be blocked, fix the false positives with exclusion rules, and only then enable real blocking. It’s the difference between a smooth rollout and an emergency.

What is a Paranoia Level in the OWASP CRS?

It’s a sensitivity dial from 1 to 4. PL1 catches clear, unambiguous attacks with very few false positives — the recommended default. Higher levels add increasingly aggressive rules that catch more subtle attacks but also flag more legitimate traffic. Most production sites run at PL1 or PL2. Only raise it if you understand your traffic well and have a low tolerance for risk plus the time to manage exclusions.

Do I need ModSecurity if I already use a WordPress security plugin?

They work at different layers. A PHP security plugin (Wordfence, etc.) runs inside WordPress — which means WordPress and PHP already loaded before it can act. ModSecurity blocks malicious requests at the NGINX layer, before PHP even starts. That’s far more efficient under attack and removes a whole class of risk. For performance-conscious sites, a WAF is the stronger foundation; you can still run a PHP plugin on top for file-integrity monitoring and alerting.

Will this work on Apache instead of NGINX?

Yes. ModSecurity and the OWASP CRS are server-agnostic at the rules level. The engine installation differs (Apache uses mod_security2 with its own config paths) but Steps 3–6 — the base config, CRS install, main.conf wiring, and DetectionOnly-then-On rollout — are essentially identical. The CRS rules themselves are byte-for-byte the same.

How do I update the OWASP CRS later?

Since you installed it via git, updating is cd /etc/nginx/modsec/owasp-crs && sudo git pull, then sudo nginx -t && sudo systemctl reload nginx. Always test in DetectionOnly-equivalent caution after a major version bump — new rules can introduce new false positives. Subscribe to the CRS release notes so you know when security-relevant rules change.

Our optimized NGINX and Angie builds for Debian and Ubuntu ship the http-modsecurity module precompiled, turning the hardest part of this guide into a single apt install.