DevOpsil

Apache Mod_security WAF Rules For OWASP Top 10 Protection

Riku TanakaRiku Tanaka6 min read

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:

MetricWhat It Tells You
modsec_blocked_totalAttack volume over time
modsec_anomaly_score_distributionHow close legitimate traffic is to threshold
modsec_rule_triggers_by_idWhich rules are most active (false positive hunting)
modsec_blocked_by_countryGeographic 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:

  • SecRuleEngine is On (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."

Share:

Was this article helpful?

Riku Tanaka
Riku Tanaka

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

More in Apache

View all →
ApacheQuick RefBeginnerNeeds Review

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.

Aareez Asif·
3 min read

Discussion