Apache Mod_security WAF Rules For OWASP Top 10 Protection
Apache mod_security WAF Rules for OWASP Top 10 Protection
If you're running Apache in production without a Web Application Firewall, you're essentially leaving your front door unlocked. mod_security is the industry-standard WAF module for Apache, and combined with the OWASP Core Rule Set (CRS), it gives you solid baseline protection against the most common attack vectors. Let's set it up properly.
What We're Building
By the end of this article, you'll have:
- mod_security installed and running in Apache
- OWASP CRS configured and tuned
- Custom rules for common attack patterns
- A monitoring strategy so you can actually see what's being blocked
Installation
On Ubuntu/Debian:
apt-get install libapache2-mod-security2
a2enmod security2
systemctl restart apache2
On RHEL/CentOS:
yum install mod_security mod_security_crs
systemctl restart httpd
Verify the module loaded:
apache2ctl -M | grep security
# Should output: security2_module (shared)
Initial Configuration
The default config lives at /etc/modsecurity/modsecurity.conf. The most critical setting is the engine mode:
# /etc/modsecurity/modsecurity.conf
# Start in DetectionOnly - NEVER jump straight to On in production
SecRuleEngine DetectionOnly
# Log everything to understand your traffic first
SecAuditEngine RelevantOnly
SecAuditLog /var/log/apache2/modsec_audit.log
SecAuditLogParts ABIJDEFHZ
# Request body handling
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
# Response body inspection (be careful with this - it has overhead)
SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml application/json
SecResponseBodyLimit 524288
# Temporary file storage
SecTmpDir /tmp/
SecDataDir /tmp/
Start in DetectionOnly mode. I cannot stress this enough. Running SecRuleEngine On immediately in production will break things. You need to understand your false positive rate first.
Installing OWASP CRS
cd /etc/modsecurity
wget https://github.com/coreruleset/coreruleset/archive/v3.3.5.tar.gz
tar -xvzf v3.3.5.tar.gz
mv coreruleset-3.3.5 crs
# Copy the example config
cp crs/crs-setup.conf.example crs/crs-setup.conf
Then include the rules in your Apache config:
# /etc/apache2/mods-enabled/security2.conf
IncludeOptional /etc/modsecurity/*.conf
Include /etc/modsecurity/crs/crs-setup.conf
Include /etc/modsecurity/crs/rules/*.conf
Configuring CRS for OWASP Top 10 Coverage
The CRS paranoia level controls how aggressively rules are applied. PL1 is the default; PL4 will break almost everything:
# /etc/modsecurity/crs/crs-setup.conf
# Paranoia Level 1 = sensible default, PL2 for higher security
SecAction \
"id:900000,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.paranoia_level=1"
# Anomaly scoring thresholds - lower = more sensitive
# Default: inbound 5, outbound 4
SecAction \
"id:900110,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.inbound_anomaly_score_threshold=5,\
setvar:tx.outbound_anomaly_score_threshold=4"
Custom Rules for OWASP Top 10
The CRS covers most bases, but here are targeted custom rules for the big hitters:
SQL Injection (A03)
# Detect common SQL injection patterns
SecRule ARGS "@detectSQLi" \
"id:1001,\
phase:2,\
deny,\
status:403,\
log,\
msg:'SQL Injection Attack Detected',\
logdata:'Matched Data: %{MATCHED_VAR} found within %{MATCHED_VAR_NAME}',\
tag:'attack-sqli',\
severity:'CRITICAL'"
# Block UNION SELECT attacks specifically
SecRule ARGS|ARGS_NAMES|REQUEST_URI "(?i)(\bunion\b.+\bselect\b)" \
"id:1002,\
phase:2,\
deny,\
status:403,\
log,\
msg:'UNION SELECT SQL Injection',\
tag:'attack-sqli'"
Cross-Site Scripting (A03)
# XSS detection using libinjection
SecRule ARGS "@detectXSS" \
"id:1010,\
phase:2,\
deny,\
status:403,\
log,\
msg:'XSS Attack Detected via libinjection',\
tag:'attack-xss',\
severity:'CRITICAL'"
# Block script tags in user input
SecRule ARGS|REQUEST_HEADERS:Referer "@rx (?i)<script[^>]*>" \
"id:1011,\
phase:2,\
deny,\
status:403,\
log,\
msg:'Script Tag XSS Attempt',\
tag:'attack-xss'"
Path Traversal (A01)
# Block directory traversal attempts
SecRule REQUEST_URI|ARGS "@rx (?:\.{2}[/\\]){2,}" \
"id:1020,\
phase:1,\
deny,\
status:403,\
log,\
msg:'Path Traversal Attack',\
tag:'attack-lfi'"
# Block null bytes
SecRule ARGS|REQUEST_URI "@rx \x00" \
"id:1021,\
phase:1,\
deny,\
status:400,\
log,\
msg:'Null Byte Injection Attempt'"
Remote/Local File Inclusion
# Block common LFI patterns
SecRule ARGS "@rx (?i)(?:etc/passwd|etc/shadow|proc/self/environ)" \
"id:1030,\
phase:2,\
deny,\
status:403,\
log,\
msg:'Local File Inclusion Attempt',\
tag:'attack-lfi'"
# Block PHP wrappers (php://filter, php://input, etc.)
SecRule ARGS|REQUEST_URI "@rx (?i)php://(?:filter|input|stdin|stdout|memory|temp)" \
"id:1031,\
phase:2,\
deny,\
status:403,\
log,\
msg:'PHP Wrapper LFI Attempt',\
tag:'attack-lfi'"
Rate Limiting for Brute Force (A07)
# Rate limit login attempts
SecAction \
"id:1040,\
phase:1,\
nolog,\
pass,\
initcol:ip=%{REMOTE_ADDR}"
SecRule REQUEST_URI "@streq /wp-login.php" \
"id:1041,\
phase:1,\
pass,\
nolog,\
setvar:ip.login_attempts=+1,\
expirevar:ip.login_attempts=60"
SecRule IP:LOGIN_ATTEMPTS "@gt 5" \
"id:1042,\
phase:1,\
deny,\
status:429,\
log,\
msg:'Brute Force Login Attempt - Rate Limit Exceeded'"
Managing False Positives
This is where WAF deployments live or die. After running in DetectionOnly for a week, analyze your logs:
# Find the most triggered rule IDs
grep "ModSecurity" /var/log/apache2/error.log | \
grep -oP 'id "\K[0-9]+' | \
sort | uniq -c | sort -rn | head -20
# Audit log analysis - find false positives by URI
grep "REQUEST_URI" /var/log/apache2/modsec_audit.log | \
awk '{print $2}' | sort | uniq -c | sort -rn
To whitelist specific rules for legitimate traffic:
# Whitelist a rule for a specific URI (API endpoint that uses JSON with SQL-like syntax)
SecRule REQUEST_URI "@beginsWith /api/v1/query" \
"id:9001,\
phase:1,\
pass,\
nolog,\
ctl:ruleRemoveById=942100"
# Whitelist for specific IP (internal monitoring)
SecRule REMOTE_ADDR "@ipMatch 10.0.1.50" \
"id:9002,\
phase:1,\
pass,\
nolog,\
ctl:ruleEngine=Off"
# Remove a specific argument from inspection (known safe parameter)
SecRuleUpdateTargetById 942100 "!ARGS:safe_html_content"
Switching to Enforcement Mode
Once your false positive rate is acceptable (I target < 0.1% of legitimate traffic):
# Update the main config
sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' \
/etc/modsecurity/modsecurity.conf
systemctl reload apache2
Keep monitoring. The first 48 hours after switching to On are critical.
Observability: What to Measure
Running mod_security without monitoring is like having a security guard who never files reports. Here's what I track:
# Custom log format for WAF events
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \
mod_security_id:%{UNIQUE_ID}e" modsec_combined
# Parse blocked requests per minute
tail -f /var/log/apache2/error.log | \
grep "ModSecurity: Access denied" | \
awk '{print $1, $2}' | uniq -c
Key metrics to ship to your monitoring stack:
| Metric | What It Tells You |
|---|---|
modsec_blocked_total | Attack volume over time |
modsec_anomaly_score_distribution | How close legitimate traffic is to threshold |
modsec_rule_triggers_by_id | Which rules are most active (false positive hunting) |
modsec_blocked_by_country | Geographic attack patterns |
If you're using Prometheus, the apache_exporter can pull these metrics. Combine with a Grafana dashboard and set alerts when blocked request rate spikes > 3x baseline.
Production Checklist
Before you call this done:
-
SecRuleEngineisOn(not DetectionOnly) - Audit logs are shipping to centralized log management
- Alert configured for sudden spike in 403s
- False positive rate measured and documented
- Whitelist rules documented with justification
- CRS version pinned and update process defined
- Response body inspection disabled if causing latency issues
The Honest Truth About WAF Limitations
mod_security is not a silver bullet. Sophisticated attackers using legitimate-looking requests, business logic abuse, and authenticated attacks will slip through. Your WAF is one layer in a defense-in-depth strategy, not a replacement for secure application code, dependency patching, and proper authentication.
But as a first line of defense? Configured correctly, mod_security + OWASP CRS will block the vast majority of automated attacks, script kiddies, and opportunistic scanners before they ever touch your application logic. That's worth the setup time.
The logs it generates are also genuinely valuable — half my incident investigations start with "let me check what mod_security saw."
Was this article helpful?
SRE & Observability Engineer
If it's not measured, it doesn't exist. SLO-driven, metrics-obsessed, and the person who gets paged at 3 AM so you don't have to. Observability isn't optional.
Related Articles
Apache 301 Redirect Loop: Diagnosing And Fixing Infinite Redirect Chains In VirtualHost And .htaccess Configurations
Few things are more frustrating than deploying what you think is a clean redirect configuration, only to have your browser throw an `ERR_TOO_MANY_REDIRECTS...
Apache 413 Request Entity Too Large: Fixing File Upload Failures With LimitRequestBody And PHP Configuration
The 413 error means Apache is rejecting your upload before it even hits PHP. Here's exactly what to change and where. Two layers can block large uploads: 1...
Apache 500 Internal Server Error: Diagnosing .htaccess Syntax Failures And Module Conflicts Step By Step
If you've been running web infrastructure long enough, you know the 500 Internal Server Error is Apache's way of saying "something's broken, and I'm not go...
Apache Mod_status Real-Time Server Monitoring With Grafana And Prometheus Integration
Monitoring Apache without proper observability tooling is like flying blind. You might catch a problem after your users start complaining, but by then, you...
Apache 403 Forbidden Error After Directory Permissions Fix: Troubleshooting SELinux And File Contexts
You've just spent an hour wrestling with Apache file permissions, carefully setting ownership to `apache:apache` and permissions to `755` on your directori...
Fix Apache 'AH00558: Could not reliably determine server's FQDN'
Resolve the Apache AH00558 warning about not being able to determine the server's fully qualified domain name by setting ServerName correctly.
More in Apache
View all →Fix Apache mod_rewrite Not Working
Diagnose and fix Apache mod_rewrite rules not being applied, covering module loading, AllowOverride settings, .htaccess issues, and rewrite logging.
Apache Performance Tuning: MPM, KeepAlive, and Caching
Tune Apache performance by choosing the right MPM, configuring KeepAlive correctly, and enabling mod_cache for significant throughput gains.
Apache as a Reverse Proxy: mod_proxy Configuration Guide
Configure Apache as a reverse proxy using mod_proxy to route traffic to backend applications with load balancing and health checks.
Apache SSL/TLS Hardening: Get an A+ on SSL Labs
Harden Apache SSL/TLS configuration to achieve an A+ rating on Qualys SSL Labs with modern cipher suites and security headers.