I still remember the first time I watched mod_security‘s logs in real time on a client’s WordPress site — the sheer volume of automated SQL injection attempts, XSS probes, and vulnerability scanners hitting that server every single hour was honestly eye-opening. Most of that traffic never reaches your application logs because it gets blocked before it ever gets that far, which is exactly the point. mod_security is the closest thing Apache has to a proper web application firewall, and I consider it close to non-negotiable for any public-facing site handling user input.
Here’s how I set it up.
What Is mod_security
mod_security is an open-source Web Application Firewall (WAF) module for Apache (also available for Nginx and IIS). It inspects incoming HTTP requests — headers, body, query strings, cookies — against a set of rules, and blocks or logs requests that match known attack patterns: SQL injection, cross-site scripting, remote file inclusion, protocol violations, and more.
Most people pair it with the OWASP Core Rule Set (CRS), a community-maintained, regularly updated set of generic attack-detection rules. Writing your own rules from scratch is possible, but almost nobody does that as a starting point.
Prerequisites
- Apache installed and running
- Root or sudo access
- A staging environment — I never enable a new WAF ruleset directly in blocking mode on production first
- Basic familiarity with reading logs, since initial tuning requires reviewing false positives
Step 1: Install mod_security
Debian/Ubuntu
sudo apt update
sudo apt install libapache2-mod-security2
CentOS/RHEL
sudo dnf install mod_security
Enable it:
sudo a2enmod security2 # Debian/Ubuntu
sudo systemctl restart apache2
On CentOS/RHEL, the module typically loads automatically once installed; confirm with:
apachectl -M | grep security
Step 2: Enable the Default Configuration
Debian/Ubuntu ships a recommended config file that needs renaming to take effect:
cd /etc/modsecurity
sudo cp modsecurity.conf-recommended modsecurity.conf
Open modsecurity.conf and find the SecRuleEngine directive. This is the single most important setting in the whole file:
SecRuleEngine DetectionOnly
I always start in DetectionOnly mode. This logs what would have been blocked without actually blocking anything — critical for catching false positives before they start breaking legitimate traffic on a live site.
Step 3: Install the OWASP Core Rule Set
The bundled default rules are minimal. For real protection, install the OWASP CRS:
cd /etc/modsecurity
sudo git clone https://github.com/coreruleset/coreruleset.git owasp-crs
cd owasp-crs
sudo cp crs-setup.conf.example crs-setup.conf
Then include it in your Apache config (typically in /etc/apache2/mods-enabled/security2.conf on Debian/Ubuntu):
<IfModule security2_module>
SecDataDir /var/cache/modsecurity
IncludeOptional /etc/modsecurity/*.conf
IncludeOptional /etc/modsecurity/owasp-crs/crs-setup.conf
IncludeOptional /etc/modsecurity/owasp-crs/rules/*.conf
</IfModule>
Step 4: Test the Configuration
sudo apachectl configtest
sudo systemctl restart apache2 # Debian/Ubuntu
sudo systemctl restart httpd # CentOS/RHEL
Step 5: Monitor in Detection Mode
With SecRuleEngine DetectionOnly active, browse and use your site normally for a few days to a week, while watching the audit log:
sudo tail -f /var/log/apache2/modsec_audit.log
Or, if using the Apache error log for alerts:
sudo tail -f /var/log/apache2/error.log | grep -i modsecurity
I pay close attention to any legitimate actions (form submissions, admin panel usage, file uploads) that get flagged — these are false positives I need to tune out before going live in blocking mode.
Step 6: Tune Out False Positives
When I find a false positive, I don’t disable the whole rule globally if I can avoid it — I scope the exception as narrowly as possible. Each blocked request in the audit log includes a rule ID. For example, if rule 941100 is falsely triggering on a legitimate admin form field:
<LocationMatch "/wp-admin/post.php">
SecRuleRemoveById 941100
</LocationMatch>
Scoping exceptions to specific paths (rather than disabling rules site-wide) keeps your overall protection much stronger.
Step 7: Switch to Blocking Mode
Once I’m confident false positives are handled, I flip the switch:
SecRuleEngine On
Test and reload:
sudo apachectl configtest
sudo systemctl reload apache2
From this point on, matching requests are actually blocked (typically with a 403 response) rather than just logged.
Setting the Paranoia Level
The OWASP CRS has a “paranoia level” (1–4) controlling how aggressive detection is. Higher levels catch more attacks but also generate more false positives. In crs-setup.conf:
SecAction \
"id:900000,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.paranoia_level=1"
I stay at paranoia level 1 for most general-purpose sites — it’s the level the CRS project itself recommends as a safe default. I only go higher for genuinely high-security applications (banking, healthcare) where I have time budgeted for the extra tuning work that comes with it.
Real-World Use Cases
- WordPress and other CMS platforms: Constant targets for automated exploit scanners;
mod_securityblocks a large share of this traffic before it reaches PHP at all. - Login and form endpoints: Protects against SQL injection and XSS attempts in user-submitted data.
- API endpoints: Detects malformed or malicious payloads targeting your backend.
- Compliance requirements: Some PCI-DSS assessments specifically call for a WAF on any server processing payment data.
Troubleshooting Tips
- Legitimate requests getting blocked (403 errors): Check the audit log for the specific rule ID and scope an exception rather than disabling protection broadly.
- Performance concerns on high-traffic sites: Review paranoia level and rule count; the CRS project provides guidance on performance-tuning rule exclusions for busy sites.
- Audit log growing very large: Adjust
SecAuditLogPartsto log only what you actually need, and set up log rotation specifically for the ModSecurity audit log. - Rules not applying at all: Confirm
IncludeOptionalpaths actually point to where you cloned the CRS, and checkapachectl -M | grep securityto confirm the module itself loaded.
Common Mistakes to Avoid
- Enabling blocking mode immediately without a detection-only tuning period — this is the single most common way people break their own sites with
mod_security. - Globally disabling rules instead of scoping exceptions to specific paths or parameters.
- Never updating the CRS, missing detection improvements for newer attack patterns.
- Ignoring the audit log entirely once it’s set up — a WAF you never review is only half doing its job.
- Setting paranoia level too high too fast, generating an overwhelming number of false positives that lead to frustration and premature abandonment of the tool.
Security Best Practices
- Start in
DetectionOnly, tune carefully, then switch to blocking mode. - Keep the OWASP CRS updated regularly — attack patterns evolve constantly.
- Scope rule exceptions as narrowly as possible (specific paths/parameters, not global disables).
- Combine
mod_securitywith other layers: rate limiting, fail2ban, proper input validation in your application code. A WAF is a layer, not a replacement for secure coding practices. - Regularly review audit logs for patterns that might indicate targeted, persistent attacks worth escalating.
Performance Optimization
- Only enable the rule categories you actually need — the CRS lets you selectively include/exclude rule files.
- Use
SecAuditLogPartsto limit what gets logged, reducing I/O overhead on high-traffic sites. - Consider caching/exempting static asset requests from full rule processing, since they rarely carry attack payloads.
- Benchmark request latency before and after enabling
mod_securityunder realistic load to catch any unexpected performance regressions early.
Frequently Asked Questions
Q: Is mod_security free? A: Yes, mod_security itself and the OWASP Core Rule Set are both free and open source.
Q: Will mod_security slow down my site? A: There’s a small amount of overhead per request, but for most sites it’s negligible relative to the security benefit. High-traffic sites should benchmark and tune rule sets accordingly.
Q: What’s the difference between mod_security and a firewall like ufw or iptables? A: ufw/iptables operate at the network level (IPs, ports); mod_security operates at the HTTP application layer, inspecting actual request content for attack patterns.
Q: How do I know if mod_security is blocking a legitimate request? A: Check the audit log for a 403 response and the specific rule ID that triggered — that’s your starting point for tuning an exception.
Q: Should I run mod_security in DetectionOnly mode permanently? A: No — detection-only mode logs threats but doesn’t stop them. It’s meant as a temporary tuning phase before switching to full blocking mode.
Summary and Key Takeaways
mod_security is one of the highest-value security additions you can make to an Apache server, but it needs to be rolled out carefully. Here’s what to remember:
- Install
mod_securityand pair it with the OWASP Core Rule Set for meaningful protection out of the box. - Always start in
DetectionOnlymode and tune out false positives before switching to blocking. - Scope rule exceptions narrowly instead of disabling protection broadly.
- Keep the rule set updated and review audit logs periodically.
- Treat it as one layer of a broader security strategy, not a silver bullet on its own.
Done right, it quietly blocks an enormous amount of automated attack traffic without you ever having to think about it day to day.