Introduction: Installing ModSecurity Is the Easy Part
Getting ModSecurity and the OWASP Core Rule Set running takes about fifteen minutes. Running it in blocking mode, in production, without breaking your own application is where most deployments actually fail — a WAF that blocks legitimate checkout requests gets disabled by week two, and a disabled WAF protects nothing.
This guide assumes ModSecurity is already installed and focuses on the part that determines whether it survives contact with real traffic: choosing a paranoia level, reading anomaly scores correctly, writing targeted exclusions instead of disabling entire rule categories, and using virtual patching to close a vulnerability before you have shipped a real fix. If you have not installed ModSecurity yet, see our ModSecurity WAF setup guide first and come back here for the tuning phase.
ModSecurity is the world's most widely deployed open-source WAF, and the OWASP Core Rule Set is what turns it from a request inspector into an actual attack-blocking system. The rest of this guide is everything that happens after SecRuleEngine On.
1. What Is ModSecurity?
ModSecurity started in 2002 as an Apache module written by Ivan Ristic. It was originally designed to add request inspection to Apache, giving administrators a programmable layer to block attacks that HTTP servers had no visibility into. Over the following decade it became the de-facto open-source WAF.
Today ModSecurity exists in two major versions:
- ModSecurity v2 — The original C code, deeply integrated into Apache as a module. Mature, battle-tested, but Apache-only. Most servers running ModSecurity on Apache still use v2.
- ModSecurity v3 (libmodsecurity) — A complete rewrite as a standalone C++ library with a connector architecture. The core engine is server-agnostic; connectors exist for Nginx (libmodsecurity3-nginx), Apache, and IIS. This is the modern, actively maintained version.
How ModSecurity Works
ModSecurity intercepts HTTP traffic in five processing phases. Understanding these phases is essential for writing effective rules and understanding why certain rules must target specific phases:
- Phase 1 — Request Headers: Fires after request headers are received but before the body is read. Ideal for IP-based rules, User-Agent checks, and header inspection.
- Phase 2 — Request Body: Fires after the full request body is parsed. This is where POST parameter inspection, JSON body scanning, and file upload checks happen. Most CRS rules target this phase.
- Phase 3 — Response Headers: Fires after response headers are generated but before the body is sent. Useful for removing sensitive server information headers.
- Phase 4 — Response Body: Fires on the response body. Expensive — disabled by default in production for performance reasons.
- Phase 5 — Logging: Fires at the end of the transaction for audit logging. Rules here cannot block requests, only log.
The processing flow for each request is: receive → parse → phase 1 rules → read body → phase 2 rules → forward to app → receive response headers → phase 3 rules → buffer response body → phase 4 rules → send response → phase 5 logging.
2. OWASP Top 10 — What You Are Defending Against
The OWASP Top 10 is the authoritative list of the most critical web application security risks. ModSecurity with the OWASP Core Rule Set provides direct protection against most of these categories. Understanding what each attack category means in practice makes rule tuning and log analysis much more effective.
| # | Category | Real-World Example | ModSecurity Coverage |
|---|---|---|---|
| A01 | Broken Access Control | Path traversal (../../etc/passwd), IDOR (accessing user ID 1001 when logged in as 1002) |
URI normalization rules, path traversal detection |
| A02 | Cryptographic Failures | Sensitive data in URLs, unencrypted cookie values, exposed API keys in responses | Response header inspection, limited coverage (primarily app-level) |
| A03 | Injection (SQL, XSS, Command) | ' OR 1=1 --, <script>document.cookie</script>, ; cat /etc/passwd |
Core Rule Set — primary strength, deep pattern matching with @detectSQLi and @detectXSS operators |
| A04 | Insecure Design | Business logic flaws, missing rate limiting, insecure password reset flows | Rate limiting rules available, but structural logic flaws require app-level fixes |
| A05 | Security Misconfiguration | Default credentials, directory listing, exposed stack traces, permissive CORS | Response header rules, block default error pages, remove server version headers |
| A06 | Vulnerable and Outdated Components | Known CVEs in WordPress plugins, outdated PHP libraries, unpatched CMS versions | Virtual patching — write rules targeting specific CVE patterns without updating the app |
| A07 | Identification and Authentication Failures | Credential stuffing, brute force login, session fixation | Rate limiting rules for login endpoints, session token pattern inspection |
| A08 | Software and Data Integrity Failures | Insecure deserialization, unsigned software updates, malicious serialized objects | Body inspection for serialized PHP objects, Java deserialization patterns |
| A09 | Security Logging and Monitoring Failures | No audit trail, undetected breaches, missing alerting | ModSecurity provides comprehensive audit logging — directly addresses this category |
| A10 | Server-Side Request Forgery (SSRF) | App fetches attacker-controlled URLs, accessing cloud metadata endpoints (169.254.169.254) |
Rules blocking internal IP ranges in request parameters, cloud metadata URL patterns |
3. OWASP Core Rule Set (CRS)
The OWASP Core Rule Set (CRS) is a set of generic attack detection rules that works with ModSecurity. It is maintained by the OWASP CRS project — a community of security researchers who continuously update rules based on real attack traffic, CVE disclosures, and threat intelligence.
CRS is not a list of IPs or signatures. It is a collection of pattern-matching rules targeting attack techniques that work regardless of which specific tool or payload an attacker uses. A new SQL injection tool with novel payloads will typically still trigger CRS rules because CRS targets the structural characteristics of the attack, not specific known strings.
CRS v4 Highlights
- Complete rewrite of many rule groups for accuracy improvement
- New early blocking rules that reject obviously malicious traffic at phase 1
- Improved false positive rates compared to CRS v3
- Plugin architecture — optional rule packs for specific technologies (WordPress, Drupal, Next.js)
- Better handling of modern API formats (JSON, XML body inspection)
Paranoia Levels
CRS uses a paranoia level system to let you choose your security-to-false-positive tradeoff. Rules are tagged PL1 through PL4 and are activated based on your configured level:
| Level | Name | Description | Recommended For |
|---|---|---|---|
| PL1 | Balanced | Default rules, low false positive rate. Covers the most impactful attack patterns. | Most production environments, shared hosting, WordPress sites |
| PL2 | Moderate | Adds stricter rules. Some false positives with complex applications using rich text editors or binary uploads. | SaaS applications, APIs with validation, e-commerce |
| PL3 | Strict | Very strict pattern matching. Requires significant tuning for most applications. High false positive risk. | High-security environments with dedicated tuning resources |
| PL4 | Extreme | Maximum sensitivity. Expect significant false positives. Nearly unusable without extensive exclusions. | Specialized high-security deployments, after extensive tuning |
Anomaly Scoring
CRS does not block requests based on individual rule matches by default. Instead, it uses an anomaly scoring system. Each triggered rule adds points to a transaction score. The request is only blocked if the cumulative score exceeds a threshold.
This approach dramatically reduces false positives. A legitimate request might contain a word that looks like part of a SQL keyword — that gets 1 point. An actual SQL injection attempt might trigger 15+ rules and score 50+ points, well above the blocking threshold.
# In crs-setup.conf
# Inbound anomaly score threshold — block requests scoring above this
SecAction "id:900110,phase:1,nolog,pass,t:none,setvar:tx.inbound_anomaly_score_threshold=5"
# Outbound threshold — block responses scoring above this
SecAction "id:900120,phase:1,nolog,pass,t:none,setvar:tx.outbound_anomaly_score_threshold=4"
A threshold of 5 (the default) means a single critical rule match (score=5) blocks the request, but a single low-severity match (score=1) does not. Increase the threshold to reduce false positives; lower it for stricter blocking.
4. Installation on Nginx (Detailed)
Nginx does not have a built-in module system like Apache, so ModSecurity v3 with the Nginx connector is the modern approach.
Step 1: Install libmodsecurity3
apt update
apt install -y libmodsecurity3 libmodsecurity-dev
Step 2: Install the Nginx Connector Module
# Check your Nginx version first
nginx -v
# Install the Nginx modsecurity connector
apt install -y libnginx-mod-http-modsecurity
# If not available in apt, compile manually:
# git clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git
# nginx -V 2>&1 | grep configure arguments
# (then compile with those same arguments + --add-dynamic-module=../ModSecurity-nginx)
Step 3: Download OWASP Core Rule Set
cd /etc/modsecurity
wget https://github.com/coreruleset/coreruleset/archive/v4.7.0.tar.gz
tar -xzf v4.7.0.tar.gz
mv coreruleset-4.7.0 crs
cp crs/crs-setup.conf.example crs/crs-setup.conf
Step 4: Configure modsecurity.conf
cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
# Edit the key directives:
nano /etc/modsecurity/modsecurity.conf
Key changes to make in modsecurity.conf:
# Change from DetectionOnly to enforcement
SecRuleEngine On
# Enable request body inspection
SecRequestBodyAccess On
# Disable response body inspection for performance (enable if you need it)
SecResponseBodyAccess Off
# Maximum request body size (13MB default — adjust for your app)
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
# Audit log settings
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLog /var/log/modsecurity/audit.log
Step 5: Create Main Rules Include File
cat > /etc/modsecurity/main.conf << 'EOF'
Include /etc/modsecurity/modsecurity.conf
Include /etc/modsecurity/crs/crs-setup.conf
Include /etc/modsecurity/crs/rules/*.conf
EOF
Step 6: Enable in Nginx
# In your nginx.conf or server block:
modsecurity on;
modsecurity_rules_file /etc/modsecurity/main.conf;
Or in a specific server block to enable WAF for a single domain:
server {
listen 443 ssl;
server_name example.com;
# Enable ModSecurity for this vhost only
modsecurity on;
modsecurity_rules_file /etc/modsecurity/main.conf;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
Step 7: Test and Reload
# Test configuration
nginx -t
# Reload
systemctl reload nginx
# Verify WAF is working — this should return 403
curl -I "http://localhost/?q=1'+OR+'1'='1"
5. Installation on Apache
Apache with ModSecurity v2 is a mature, well-documented combination. The package is available in standard repositories.
# Install
apt install -y libapache2-mod-security2
# Enable the module
a2enmod security2
# The default config lands at:
# /etc/modsecurity/modsecurity.conf
# Download OWASP CRS (same as Nginx steps above)
# Then configure the Include path in:
/etc/apache2/mods-enabled/security2.conf
In security2.conf, add your CRS includes:
<IfModule security2_module>
SecDataDir /var/cache/modsecurity
IncludeOptional /etc/modsecurity/*.conf
IncludeOptional /etc/modsecurity/crs/crs-setup.conf
IncludeOptional /etc/modsecurity/crs/rules/*.conf
</IfModule>
# Test and restart
apache2ctl configtest
systemctl restart apache2
6. Essential Configuration Directives Explained
Every production ModSecurity deployment needs these directives correctly configured. Each one has real implications for security and performance.
# ENFORCEMENT MODE
# DetectionOnly = log but never block (good for initial testing)
# On = inspect and block matching requests
# Off = completely disabled
SecRuleEngine On
# REQUEST BODY INSPECTION
# Required to inspect POST parameters, JSON bodies, file uploads
SecRequestBodyAccess On
# Maximum body size in bytes (13MB = 13 * 1024 * 1024)
# Requests exceeding this get a 413 if SecRequestBodyLimitAction is Reject
SecRequestBodyLimit 13107200
# Maximum non-file body size (protects against large JSON/form payloads)
SecRequestBodyNoFilesLimit 131072
# What to do when body exceeds limit
SecRequestBodyLimitAction Reject
# RESPONSE BODY INSPECTION
# Off by default — enabling adds latency as responses must be buffered
# Only enable if you need to inspect response content
SecResponseBodyAccess Off
# AUDIT LOGGING
# On = log all transactions
# Off = log nothing
# RelevantOnly = log only transactions matching SecAuditLogRelevantStatus
SecAuditEngine RelevantOnly
# Which HTTP status codes trigger audit log entry
# This regex logs all 4xx except 404, and all 5xx
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
# Log format: Serial writes to one file, Concurrent writes per-transaction files
SecAuditLogType Serial
SecAuditLog /var/log/modsecurity/audit.log
# AUDIT LOG PARTS
# A=request headers, B=request body, C=response headers,
# F=response headers sent, H=audit log trailer, Z=final boundary
# Omit C/D/E/F for performance (response body)
SecAuditLogParts ABHZ
# TEMPORARY FILES
# For storing large request bodies during inspection
SecTmpDir /tmp
SecDataDir /tmp/modsecurity-data
7. Understanding and Writing Rules
ModSecurity rules follow a consistent syntax that, once understood, makes writing custom rules straightforward. Every rule has three components: what to inspect, how to inspect it, and what to do when a match is found.
Rule Syntax
SecRule VARIABLE OPERATOR "id:XXXX,phase:N,ACTION,msg:'Description'"
Common Variables
ARGS— All GET and POST parametersREQUEST_URI— Full URI including query stringREQUEST_HEADERS— All request headers (or a specific one:REQUEST_HEADERS:User-Agent)REMOTE_ADDR— Client IP addressREQUEST_BODY— Raw request bodyREQUEST_COOKIES— All cookie valuesRESPONSE_HEADERS— Response headers (phase 3+)FILES— Uploaded file information
Common Operators
@rx— Regular expression match (default if no operator specified)@streq— Exact string equality@contains— Substring match@beginsWith— String prefix match@detectSQLi— Built-in SQL injection detection (LibInjection)@detectXSS— Built-in XSS detection (LibInjection)@ipMatch— IP address or CIDR match@gt,@ge,@lt,@le— Numeric comparisons@pm— Parallel multi-pattern match (high performance for large pattern lists)
Practical Rule Examples
# Block SQL injection in any parameter (using LibInjection)
SecRule ARGS "@detectSQLi" \
"id:1001,phase:2,deny,status:403,log,\
msg:'SQL Injection Attempt Detected',\
tag:'OWASP_CRS/WEB_ATTACK/SQL_INJECTION'"
# Block XSS in request body
SecRule ARGS "@detectXSS" \
"id:1002,phase:2,deny,status:403,log,\
msg:'XSS Attempt Detected',\
tag:'OWASP_CRS/WEB_ATTACK/XSS'"
# Block scanner User-Agents
SecRule REQUEST_HEADERS:User-Agent "@pm nikto sqlmap nmap masscan dirbuster gobuster" \
"id:1003,phase:1,deny,status:403,log,\
msg:'Malicious Scanner User-Agent'"
# Rate limit WordPress login page (max 10 attempts per IP per 60 seconds)
SecRule REQUEST_URI "@streq /wp-login.php" \
"id:1004,phase:1,chain,nolog,pass"
SecRule &IP:wp_login_count "@ge 10" \
"deny,status:429,log,msg:'WordPress Login Rate Limit Exceeded'"
SecAction "id:1005,phase:1,nolog,pass,\
initcol:ip=%{REMOTE_ADDR},\
setvar:ip.wp_login_count=+1,\
expirevar:ip.wp_login_count=60"
# Block path traversal attempts
SecRule REQUEST_URI "@rx (?:\.\./|\.\.\\)" \
"id:1006,phase:1,deny,status:403,log,\
msg:'Path Traversal Attempt'"
# Virtual patch for a specific CVE
SecRule REQUEST_URI "@contains /wp-content/plugins/vulnerable-plugin/upload.php" \
"id:1007,phase:1,deny,status:403,log,\
msg:'Virtual Patch: CVE-2024-XXXX Blocked'"
# Block access to sensitive files
SecRule REQUEST_URI "@rx \.(git|env|sql|bak|backup|log|htpasswd)$" \
"id:1008,phase:1,deny,status:403,log,\
msg:'Access to Sensitive File Blocked'"
# Remove server version header from responses
SecRule RESPONSE_HEADERS:Server "@rx nginx/[\d\.]+" \
"id:1009,phase:3,pass,nolog,\
setvar:'!response.RESPONSE_HEADERS.Server'"
Common Actions
deny— Block the request (return specified status code)allow— Allow and skip remaining rulespass— Continue to next rule (used for counting, logging without blocking)redirect:https://example.com— Redirect to URLlog— Write to audit lognolog— Do not write to audit logauditlog— Force audit log entrystatus:403— HTTP response status for deny actionschain— Link this rule with the next rule (both must match)setvar:tx.score=+5— Set/increment a variableexpirevar:ip.counter=60— Set variable expiry in seconds
8. Handling False Positives
False positives are the primary challenge in WAF operations. A legitimate request from your application or its users gets blocked because it happens to contain a pattern that looks like an attack. Poorly handled false positives mean either blocking real users or disabling rules and reducing security. Handling them systematically is essential.
Step 1: Start in Detection Mode
Before enabling blocking, run ModSecurity in DetectionOnly mode for at least one to two weeks. This builds a baseline of what your application generates and what rules would fire if blocking were enabled — without disrupting any users.
SecRuleEngine DetectionOnly
Step 2: Read the Audit Log
Every potential block generates an audit log entry. A typical entry looks like this:
--a1b2c3d4-A--
[2026-03-28 14:23:11] 192.168.1.100 example.com 443
--a1b2c3d4-B--
POST /api/content HTTP/1.1
Host: example.com
Content-Type: application/json
{"content": "Use <strong>bold</strong> for emphasis"}
--a1b2c3d4-H--
Message: Warning. detected XSS using libinjection.
[file "crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf"]
[line "37"] [id "941100"] [msg "XSS Attack Detected via libinjection"]
[data "Matched Data: XSS data found within ARGS:content: ..."]
[severity "CRITICAL"] [ver "OWASP_CRS/4.0.0"]
This tells you: rule 941100 triggered on the content parameter because the value contained HTML tags — which is actually legitimate for a rich text editor.
Step 3: Write Targeted Exclusions
Never disable entire rule IDs globally. Always target exclusions as narrowly as possible:
# Method 1: Exclude specific parameter from specific rule
# When a rich text field triggers XSS rules
SecRuleUpdateTargetById 941100 "!ARGS:content"
SecRuleUpdateTargetById 941110 "!ARGS:content"
# Method 2: Disable rule entirely for a specific URI (use with care)
SecRule REQUEST_URI "@beginsWith /api/webhook" \
"id:9001,phase:1,nolog,allow,ctl:ruleEngine=Off"
# Method 3: Whitelist specific IP for all rules
SecRule REMOTE_ADDR "@ipMatch 203.0.113.10" \
"id:9002,phase:1,nolog,allow"
# Method 4: Remove rule for entire server (last resort)
SecRuleRemoveById 920350
False Positive Best Practices
- Keep exclusions in a separate file (e.g.,
/etc/modsecurity/exclusions.conf) - Document why each exclusion exists with a comment
- Review exclusions quarterly — applications change, and old exclusions may no longer be needed
- Use
SecRuleUpdateTargetByIdbefore consideringSecRuleRemoveById - Never whitelist entire IP ranges unless absolutely necessary (office IPs are not threat-free)
9. Virtual Patching
Virtual patching is one of the most powerful use cases for a WAF. When a vulnerability is disclosed in a CMS, plugin, or library you run — but you cannot immediately update the software — a virtual patch rule blocks exploit attempts at the WAF layer while you plan the proper update.
This is especially valuable for:
- Zero-day vulnerabilities — A patch doesn't exist yet, but the attack pattern is known
- Unpatched legacy applications — Systems that cannot be updated due to compatibility constraints
- Managed customer sites — You can protect hundreds of sites without touching each one
- Post-breach containment — Immediately block further exploitation while you investigate
Writing a Virtual Patch
When CVE-2024-XXXXX is disclosed for a WordPress plugin that has a file upload bypass at a specific endpoint, you can block it within minutes:
# Virtual patch: CVE-2024-XXXXX — Vulnerable Plugin arbitrary file upload
# Vulnerability: plugin accepts PHP files via /wp-content/plugins/bad-plugin/upload.php
# Temporary patch until plugin is updated to version 2.1.4
SecRule REQUEST_URI "@rx /wp-content/plugins/bad-plugin/upload\.php" \
"id:5001,phase:1,deny,status:403,log,\
msg:'Virtual Patch: CVE-2024-XXXXX Arbitrary File Upload Blocked',\
tag:'VIRTUAL_PATCH/CVE-2024-XXXXX'"
# Also block known payload pattern regardless of endpoint
SecRule REQUEST_BODY "@rx \.php[357]?\s*$" \
"id:5002,phase:2,deny,status:403,log,\
msg:'PHP File Upload Attempt Blocked'"
10. Performance Optimization
ModSecurity adds CPU overhead to every request. On high-traffic servers, misconfigured ModSecurity can become a bottleneck. These optimizations reduce that overhead significantly.
Most Impactful Optimizations
# 1. Disable response body inspection (biggest performance gain)
# Only enable if you specifically need to inspect response content
SecResponseBodyAccess Off
# 2. Skip static assets — images, CSS, JS don't need WAF inspection
SecRule REQUEST_URI "@rx \.(jpg|jpeg|png|gif|webp|ico|css|js|woff2?|ttf|svg|mp4|pdf)$" \
"id:2001,phase:1,nolog,allow"
# 3. Skip upload endpoint inspection if file scanning is handled elsewhere
# (Be careful with this — only do it if you have a separate scanner)
# 4. Tune PCRE limits for very long request values
SecPcreMatchLimit 100000
SecPcreMatchLimitRecursion 100000
# 5. Use the @pm operator for large pattern lists (much faster than @rx with alternation)
# Instead of: SecRule ARGS "@rx (word1|word2|word3|...100 words...)"
# Use: SecRule ARGS "@pm word1 word2 word3 ...100 words..."
Memory and Worker Considerations
Each Nginx or Apache worker process loads ModSecurity and CRS rules into memory. On servers with many worker processes, CRS can add 10-20MB per worker. Keep your worker count aligned with CPU cores, not RAM, when ModSecurity is enabled.
11. Logging and Monitoring
ModSecurity generates two types of logs. Understanding the difference and knowing how to query audit logs is essential for ongoing WAF management.
Debug Log vs Audit Log
- Audit Log — Records of transactions that triggered rules. This is what you read for security analysis. Configured with
SecAuditLog. - Debug Log — Verbose internal processing information. Only useful for troubleshooting rule development. Always disable in production:
SecDebugLogLevel 0
Audit Log Parts Reference
# SecAuditLogParts ABCFHZ
# A = Request headers
# B = Request body
# C = Response headers (from app)
# D = Intermediate response headers
# E = Intermediate response body
# F = Final response headers (sent to client)
# H = Audit log trailer (rule matches, scores)
# I = Multipart request body (alternative to B)
# J = Uploaded files
# K = List of all matched rules
# Z = Final boundary marker
# Recommended for production (captures what you need without response bodies):
SecAuditLogParts ABHZ
Useful Log Analysis Commands
# Count total blocked requests today
grep "$(date +%d/%b/%Y)" /var/log/modsecurity/audit.log | grep -c "CRITICAL\|ERROR"
# Find top 10 attacking IPs
grep "REMOTE_ADDR" /var/log/modsecurity/audit.log | \
grep -oP '\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn | head 10
# Find most frequently triggered rules
grep "id \"" /var/log/modsecurity/audit.log | \
grep -oP 'id "\K[0-9]+' | sort | uniq -c | sort -rn | head 20
# Find potential false positives (rules triggering on legitimate URIs)
grep -E "(GET|POST).*/api/" /var/log/modsecurity/audit.log | \
grep "CRITICAL" | awk '{print $1}' | sort | uniq -c | sort -rn
# Real-time monitoring of audit log
tail -f /var/log/modsecurity/audit.log | grep --line-buffered "id\|REMOTE"
Log Rotation
# /etc/logrotate.d/modsecurity
/var/log/modsecurity/audit.log {
daily
rotate 30
compress
delaycompress
notifempty
create 0640 root adm
postrotate
/usr/sbin/nginx -s reopen
endscript
}
12. ModSecurity vs Commercial WAFs
ModSecurity is the right tool for most self-hosted deployments. Understanding how it compares to commercial offerings helps you make the right choice for different use cases.
| Feature | ModSecurity + CRS | Cloudflare WAF | AWS WAF | Imperva |
|---|---|---|---|---|
| Price | Free | $20/zone/mo (Pro) | $5/rule group/mo + request fees | Custom enterprise pricing |
| Rule Updates | Manual (git pull CRS) | Automatic | Manual managed rule groups | Automatic |
| Latency Added | 1-5ms (same server) | 0ms (edge network) | 0ms (edge) | 0ms (edge) |
| DDoS Protection | Limited (rate limiting rules) | Excellent | Good (with Shield) | Excellent |
| Bot Management | Basic (User-Agent rules) | Advanced | Basic | Advanced |
| Customization | Complete (write any rule) | Limited (Firewall Rules) | Medium | Good |
| Data Privacy | All traffic stays on your server | Traffic routed through Cloudflare | Traffic through AWS | Traffic through Imperva |
| Learning Curve | High | Low | Medium | Low-Medium |
| Virtual Patching | Excellent (instant, custom) | Good | Good | Excellent |
ModSecurity wins on cost, customization, and privacy. Commercial solutions win on automation, DDoS protection depth, and ease of use. For many deployments, the right answer is both: ModSecurity at the server layer for application-specific rules and virtual patching, Cloudflare at the edge for DDoS and bot management.
13. Complete Production Configuration
Here is a complete, production-ready configuration that you can use as a starting point. This is what a properly tuned ModSecurity deployment looks like on day one, before application-specific tuning.
modsecurity.conf (Production)
# ModSecurity Production Configuration
# Based on OWASP ModSecurity Core Rule Set v4
# ENFORCEMENT
SecRuleEngine On
# REQUEST INSPECTION
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyLimitAction Reject
# RESPONSE INSPECTION (disabled for performance)
SecResponseBodyAccess Off
SecResponseBodyMimeType text/plain text/html text/xml
SecResponseBodyLimit 524288
# TMPDIR AND DATA STORAGE
SecTmpDir /tmp/modsecurity
SecDataDir /tmp/modsecurity-data
# AUDIT LOG
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLog /var/log/modsecurity/audit.log
SecAuditLogParts ABHZ
SecAuditLogType Serial
# DEBUG LOG (disabled in production)
SecDebugLogLevel 0
# UNICODE MAP
SecUnicodeMapFile unicode.mapping 20127
# SECURITY POLICY
SecDefaultAction "phase:1,log,auditlog,deny,status:403"
SecDefaultAction "phase:2,log,auditlog,deny,status:403"
crs-setup.conf (Production Starter)
# CRS Production Setup
# Paranoia Level 1 is the right starting point for most deployments
SecAction \
"id:900000,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.paranoia_level=1"
# Anomaly scoring thresholds
# Score of 5 = one critical rule hit (block), or 5 low-severity hits (block)
SecAction \
"id:900110,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.inbound_anomaly_score_threshold=5,\
setvar:tx.outbound_anomaly_score_threshold=4"
# HTTP methods — restrict to what your application actually uses
SecAction \
"id:900200,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:'tx.allowed_methods=GET HEAD POST PUT PATCH DELETE OPTIONS'"
# Content types that ModSecurity will inspect request bodies for
SecAction \
"id:900220,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:'tx.allowed_request_content_type=application/x-www-form-urlencoded|multipart/form-data|application/json|application/xml|text/xml'"
# Max file upload size (0 = unlimited, override with actual limit)
SecAction \
"id:900290,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.max_file_size=1048576,\
setvar:tx.combined_file_sizes=1048576"
Quick Reference Tables
Troubleshooting Commands
| Problem | Command |
|---|---|
| Check ModSecurity is loaded | nginx -V 2>&1 | grep ModSecurity |
| Test configuration syntax | nginx -t |
| View live blocked requests | tail -f /var/log/modsecurity/audit.log |
| Check which rule triggered | grep "id \"941" /var/log/modsecurity/audit.log | tail -20 |
| Temporarily disable rule | Add SecRuleRemoveById RULE_ID to exclusions.conf, reload nginx |
| Test a specific attack pattern | curl -sk "http://localhost/?q=1'+OR+'1'='1" (expect 403) |
| Check anomaly score for request | Set SecAuditLogParts ABKZ, request audit log part K |
| Update CRS rules | cd /etc/modsecurity/crs && git pull && nginx -s reload |
Rule ID Ranges to Know
| Range | Category |
|---|---|
| 900000-900999 | CRS initialization and settings |
| 901000-901999 | Common exceptions and exclusions |
| 911000-911999 | Method enforcement |
| 913000-913999 | Scanner and crawler detection |
| 920000-920999 | Protocol enforcement |
| 921000-921999 | Protocol attack detection |
| 930000-930999 | Local file inclusion (LFI) |
| 931000-931999 | Remote file inclusion (RFI) |
| 932000-932999 | Remote code execution (RCE) |
| 933000-933999 | PHP injection |
| 941000-941999 | XSS attacks |
| 942000-942999 | SQL injection |
| 949000-949999 | Blocking evaluation (anomaly score) |
| 980000-980999 | Correlation rules |
Frequently Asked Questions
What paranoia level should a new deployment start at?
PL1 (Paranoia Level 1) for almost everything, including WordPress sites and most SaaS applications. It covers the highest-impact attack patterns with the lowest false-positive rate. Move to PL2 only for APIs with strict input validation, and reserve PL3/PL4 for environments with dedicated staff to tune the resulting false positives.
How long should ModSecurity stay in DetectionOnly mode before blocking?
One to two weeks of real production traffic is the practical minimum. This builds a baseline of what your application actually sends — rich text fields, file uploads, API payloads — so you can write exclusions for real false positives before a legitimate user ever gets blocked.
What is virtual patching and when should I use it?
Virtual patching is a WAF rule that blocks exploitation of a specific known vulnerability at the request layer, before the underlying application code is fixed. It is most useful for a disclosed CVE in a plugin or CMS you cannot update immediately, or for protecting many customer sites from the same vulnerability at once.
Should I ever use SecRuleRemoveById to fix a false positive?
Only as a last resort. Removing a rule entirely disables that protection server-wide. Prefer SecRuleUpdateTargetById to exclude a specific parameter from a specific rule — it keeps the rule active for every other request while fixing the one legitimate case that triggered it.
ModSecurity in Panelica
Panelica ships with ModSecurity pre-installed and pre-configured on every server. When you enable ModSecurity for a domain through the Security section, Panelica:
- Activates ModSecurity on the domain's Nginx vhost
- Applies the OWASP Core Rule Set at Paranoia Level 1
- Configures per-domain audit logging with automatic rotation
- Provides a web interface to add rule exclusions without editing config files
- Offers a Security Advisor check that flags sites without WAF protection
You do not need to install, configure, or maintain ModSecurity separately. It is part of the security stack that comes with every Panelica installation, alongside Fail2ban for rate limiting and nftables for firewall management.
Conclusion
ModSecurity with the OWASP Core Rule Set is one of the most effective security layers you can add to a web server. It is not a silver bullet — it does not replace secure application code, proper authentication, or network-level defenses. But it provides a consistent, auditable layer of protection that catches the vast majority of automated attack traffic before it reaches your applications.
The key to successful WAF operation is patience with the initial tuning phase. Start in DetectionOnly mode, run it for a week or two, read the audit logs, write targeted exclusions for legitimate traffic, then switch to blocking mode. A WAF that fires false positives on real users and gets disabled is worth less than no WAF at all. A properly tuned WAF running at Paranoia Level 1 will block thousands of attack attempts daily with minimal impact on legitimate traffic.
If you want ModSecurity running today without manual configuration, Panelica includes it as part of the standard security stack — one click to enable per domain, with the full OWASP CRS and audit logging built in.