In this tutorial, we're implementing a WAF like ModSecurity on Apache.
Implementing a Web Application Firewall (WAF) like ModSecurity on Apache is one of the most effective ways we can shield our applications from common attacks—everything from SQL injection and XSS to more complex zero-day exploits. In this tutorial, we’ll walk through every step: from installing ModSecurity to crafting custom rules and tuning for performance. Let’s dive in.
Why a WAF Matters
Web applications face a constant barrage of automated bots and attackers probing for vulnerabilities. Traditional network firewalls inspect packets, but they can’t understand HTTP semantics. A WAF lives at the HTTP layer, giving us insight into requests and responses—allowing us to block malicious payloads before they ever reach our code.
Prerequisites
Before we begin:
- Linux disto installed dedicated server or KVM VPS.
- Apache installed and running.
- Root or sudo privileges.
- Familiarity with Apache configuration and basic command-line usage.
Implementing a WAF like ModSecurity on Apache
1. Installing ModSecurity
On Debian/Ubuntu:
sudo apt update
sudo apt install libapache2-mod-security2
On CentOS/RHEL/AlmaLinux/Rocky Linux:
sudo dnf install mod_security mod_security_crs
This installs the ModSecurity engine and Apache connector. By default, ModSecurity runs in “Detection Only” mode—logging suspicious traffic but not blocking.
2. Enabling Core Rule Set (CRS)
The OWASP Core Rule Set (CRS) is a community-maintained set of generic attack detection rules. We’ll use v4.x, which includes hundreds of rules tuned for modern web apps.
Download CRS:
sudo git clone https://github.com/coreruleset/coreruleset.git /etc/modsecurity/crs
cd /etc/modsecurity/crs
sudo cp crs-setup.conf.example crs-setup.conf
Activate it in Apache. Edit /etc/apache2/mods-enabled/security2.con
f (Ubuntu) or /etc/httpd/conf.d/mod_security.conf
(CentOS) and add:
IncludeOptional /etc/modsecurity/crs/crs-setup.conf
IncludeOptional /etc/modsecurity/crs/rules/*.conf
3. Configuring ModSecurity
Open the main configuration—typically /etc/modsecurity/modsecurity.conf
—and adjust:
# Enable blocking (default is “DetectionOnly”)
SecRuleEngine On
# Ensure request body inspection
SecRequestBodyAccess On
# Audit log settings
SecAuditEngine RelevantOnly
SecAuditLogParts ABIJDEFHZ
SecAuditLogType Serial
SecAuditLog /var/log/apache2/modsec_audit.log
SecRuleEngine On: switches ModSecurity into “Prevention” mode.
SecRequestBodyAccess On: inspects POST payloads.
SecAuditEngine RelevantOnly: logs transactions that triggered a rule.
4. Testing Our Setup
We can verify that ModSecurity is active by sending a known payload:
curl -i "http://our-server/anything.php?foo=<script>alert(1)</script>"
In blocking mode, Apache should return HTTP 403 Forbidden. Inspect /var/log/apache2/error.log
and /var/log/apache2/modsec_audit.log
to see which rule fired.
5. Creating Custom Rules
Out of the box, CRS covers most vectors, but our application may have unique patterns. We can write custom rules in /etc/modsecurity/custom_rules.conf
:
# Block attempts to access /.git directories
SecRule REQUEST_URI "@beginsWith /.git" \
"id:100001,phase:1,deny,status:403,msg:'Git folder access attempt',log"
id: unique numeric identifier > 100000.
phase: when to evaluate (1 = request header).
deny/status: immediately block with 403.
Include this file in the main config:
IncludeOptional /etc/modsecurity/custom_rules.conf
6. Anomaly Scoring Mode
Instead of outright blocking on first match, we can switch to an anomaly scoring approach—assigning points per rule and blocking once a threshold is exceeded:
SecDefaultAction "phase:1,log,pass,tag:'application-multi',tag:'OWASP_CRS'"
SecAction "id:900130,phase:1,pass,nolog,initcol:ip=%{REMOTE_ADDR},initcol:global=global"
SecAction "id:900120,phase:1,pass,nolog,setvar:tx.anomaly_score=0"
SecAction "id:900110,phase:1,pass,nolog,setvar:tx.inbound_anomaly_score_threshold=5"
Here, we accumulate tx.anomaly_score; once it exceeds 5, CRS will block.
7. Tuning for Performance
WAFs introduce overhead. To optimize:
Disable body inspection for static assets:
<LocationMatch "\.(css|js|jpg|png|gif)$">
SecRuleRemoveByTag "application-multi"
</LocationMatch>
- Use regex optimizations: avoid overly generic patterns in custom rules.
- Batch log writes: on high-volume sites, consider SecAuditLogType Concurrent or send logs to a remote collector.
8. Logging & Monitoring
We can integrate ModSecurity logs with SIEM tools:
JSON audit logs: switch to JSON format for easier parsing:
SecAuditLogFormat JSON
- Remote logging: forward logs via rsyslog or Filebeat to Elasticsearch/Kibana.
- Dashboarding: visualize top rule triggers, source IPs, and attack types.
9. Handling False Positives
Every WAF generates some false positives. Our process:
Review logs daily to identify legitimate requests being blocked.
Whitelist known-safe patterns:
SecRule REQUEST_URI "@beginsWith /api/internal" \
"id:100002,phase:1,pass,nolog,ctl:ruleEngine=DetectionOnly"
Adjust CRS configuration: CRS includes exclusions for common frameworks—make sure we enable the right SecRuleRemoveById entries.
10. Advanced Integrations
- Threat Intelligence Feeds: block IPs associated with known malicious actors.
- Geo-blocking: using SecRule REMOTE_ADDR "@geoLookup" to deny traffic from high-risk regions.
- Dynamic learning: integrate machine learning models (e.g., ModSecurity’s ModSecurity-ML plugin) to adapt to changing traffic patterns.
Geo-blocking
Here’s a practical example of how we can geo-block traffic from high-risk countries using ModSecurity’s GeoLookup feature. In this snippet, we’ll:
Tell ModSecurity where to find our GeoIP database.
Perform a GeoLookup on every incoming request.
Deny requests when the resolved country code matches one of our “high-risk” regions (e.g., IR, KP).
# 1. Point ModSecurity at our GeoIP database
# (make sure you have the GeoIP Legacy database installed)
SecGeoLookupDb /usr/share/GeoIP/GeoIP.dat
# 2. Phase 1: do the GeoLookup on the client IP
# We use REMOTE_ADDR as the lookup target, and populate GEO variables
SecRule REMOTE_ADDR "@geoLookup" \
"id:100010,phase:1,pass,nolog,ctl:ruleEngine=On"
# 3. Phase 1: block if country_code is one of our high-risk list
SecRule GEO:COUNTRY_CODE "@rx ^(CN|RU|IR|KP)$" \
"id:100011,\
phase:1,\
deny,\
status:403,\
log,\
msg:'Block request from high-risk country %{GEO:COUNTRY_CODE}'"
Explanation of each directive:
SecGeoLookupDb
- Specifies the path to the GeoIP database file.
- ModSecurity will use this to map IP addresses to geographic data.
SecRule REMOTE_ADDR "@geoLookup"
- In phase:1 (request header phase), applies the @geoLookup operator to the client’s IP.
- Populates built-in variables like GEO:COUNTRY_CODE so we can reference them in subsequent rules.
SecRule GEO:COUNTRY_CODE "@rx ^(IR|KP)$"
- Checks if the
GEO:COUNTRY_CODE
matches any of the ISO country codes in our regex list. - If there’s a match, deny the request immediately with a 403 Forbidden.
- Logs the event with a clear message indicating which country was blocked.
With this in place, our Apache server will automatically refuse connections from IPs geolocated to Iran (IR), and North Korea (KP). We can easily extend the regex to include or exclude any country codes as needed.
Conclusion
By layering ModSecurity on Apache, we gain a powerful, flexible defense against web threats. Starting with basic installation and CRS activation, we progressed to custom rules, anomaly scoring, and advanced integration. As we’ve seen, a WAF is not “set and forget”—it requires ongoing tuning, monitoring, and adjustments to stay effective without disrupting legitimate traffic. With these steps, our applications will be far more resilient, and we can focus on building features rather than firefighting attacks.