How to Configure ModSecurity as a Web Application Firewall
Install ModSecurity with OWASP CRS on Apache or Nginx, run it in detection mode to catch false positives, tune exclusions, then enforce blocking.
Before you start
- ▸A running Apache or Nginx web server with sudo/root access
- ▸git installed for cloning the OWASP CRS repository
- ▸Basic familiarity with editing web server configuration files
- ▸An understanding of your application's normal HTTP traffic patterns
ModSecurity is a battle-tested open-source web application firewall (WAF) that inspects HTTP traffic in real time, blocking SQL injection, XSS, command injection, and dozens of other attack classes. Paired with the OWASP Core Rule Set (CRS), it gives you a production-grade ruleset maintained by the security community. This guide covers installation on Apache and Nginx, enabling CRS, running in detection-only mode to baseline your traffic, and methodically tuning away false positives before you switch to enforcement.
Prerequisites and Architecture Overview
ModSecurity runs as a module inside your web server process. The module itself handles parsing and inspection; CRS supplies the rules. Starting with ModSecurity 3.x, the library is connector-based: libmodsecurity3 is server-agnostic, and thin connectors (mod_security2 for Apache, ModSecurity-nginx for Nginx) link it to the server. Debian/Ubuntu ship packaged connectors; on Fedora/RHEL you'll compile or use the EPEL/Remi repos.
Installation
Debian / Ubuntu (Apache)
sudo apt update
sudo apt install -y libapache2-mod-security2
sudo a2enmod security2
sudo systemctl restart apache2
Debian / Ubuntu (Nginx)
The packaged Nginx does not include the ModSecurity connector; install the dynamic module package or use nginx-extras:
sudo apt update
sudo apt install -y libnginx-mod-http-modsecurity
Then enable the module in your Nginx config:
echo 'load_module modules/ngx_http_modsecurity_module.so;' \
| sudo tee /etc/nginx/modules-enabled/50-mod-http-modsecurity.conf
Fedora / RHEL 9 / Rocky Linux 9 (Apache)
sudo dnf install -y mod_security mod_security_crs
sudo systemctl restart httpd
On RHEL/Rocky, enable EPEL first if the packages are missing:
sudo dnf install -y epel-release
Arch Linux
sudo pacman -S mod_security
# CRS is in the AUR
yay -S owasp-modsecurity-crs
Install and Configure OWASP CRS
The packaged CRS may lag behind the upstream release. Installing directly from the project gives you the latest stable version.
cd /etc
sudo git clone https://github.com/coreruleset/coreruleset.git /etc/modsecurity.d/owasp-crs
cd /etc/modsecurity.d/owasp-crs
sudo cp crs-setup.conf.example crs-setup.conf
Tell ModSecurity where to find the rules. On Debian/Ubuntu with Apache, create an include file:
sudo tee /etc/modsecurity/modsecurity-crs.conf <<'EOF'
Include /etc/modsecurity.d/owasp-crs/crs-setup.conf
Include /etc/modsecurity.d/owasp-crs/rules/*.conf
EOF
Verify that /etc/modsecurity/modsecurity.conf includes the above file, or add the directives directly to your Apache virtual host or Nginx server block.
Enable Detection-Only (Learning) Mode
Never go straight to blocking on a production site. Detection mode logs rule matches without dropping requests, letting you identify false positives safely.
In /etc/modsecurity/modsecurity.conf (or its distro equivalent), set:
SecRuleEngine DetectionOnly
For Nginx, add inside the relevant server {} or http {} block:
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
Restart your server and exercise the application normally — run your test suite, click through common workflows, submit forms. Let traffic accumulate for at least 48–72 hours (longer for low-traffic apps) before reviewing logs.
sudo systemctl restart apache2 # or nginx
Reading ModSecurity Audit Logs
By default, the audit log lives at /var/log/modsec_audit.log. Each triggered rule produces a multi-section entry. The most useful fields are id (rule ID), msg (human description), uri, and data (the matched payload).
sudo tail -f /var/log/modsec_audit.log
Filter for rule IDs to spot high-frequency false positives:
sudo grep 'id "' /var/log/modsec_audit.log \
| grep -oP 'id "\K[0-9]+' \
| sort | uniq -c | sort -rn | head -20
Output will vary; a typical false-positive-heavy line looks like:
# Example output (yours will differ):
# 342 920350
# 211 942100
# 98 941100
Tuning False Positives
CRS provides three main mechanisms for tuning: rule exclusions, rule disabling, and anomaly score thresholds. Use the least permissive method that resolves the issue.
Raise the Anomaly Score Threshold
CRS uses a scoring model: each matched rule adds to a request's score. Requests exceeding the inbound threshold (default 5) are blocked. Raising the threshold reduces false positives broadly but also weakens protection. Edit crs-setup.conf:
SecAction \
"id:900110,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.inbound_anomaly_score_threshold=10"
Disable a Specific Rule
If rule 942100 fires on a legitimate internal API that passes SQL-like query strings, disable it only for that URI. Create a file in the CRS exclusion directory:
sudo tee /etc/modsecurity.d/owasp-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf <<'EOF'
# Disable SQL injection rule for internal reporting API
SecRule REQUEST_URI "@beginsWith /api/v1/report" \
"id:1001,phase:1,pass,nolog,\
ctl:ruleRemoveById=942100"
EOF
Remove a Rule Tag or Target
To stop a rule from inspecting a specific parameter (e.g., a rich-text editor field named content that legitimately contains HTML):
SecRuleUpdateTargetById 941100 "!ARGS:content"
Place this directive after the rule include in your config, or in a post-CRS exclusion file.
Using the CRS Exclusion Template Files
CRS ships two placeholder files specifically for your exclusions:
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf— runs before CRS rules; use forctl:ruleRemoveByIdand similar.RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf— runs after; use forSecRuleUpdateTargetById.
These files are preserved across CRS upgrades when you manage CRS via git and keep your exclusions in them.
Switching to Enforcement Mode
Once the audit log is clean of legitimate traffic matches, switch the engine to blocking mode:
sudo sed -i 's/^SecRuleEngine DetectionOnly/SecRuleEngine On/' \
/etc/modsecurity/modsecurity.conf
sudo systemctl restart apache2 # or nginx
Set SecDefaultAction to control what happens on a block — returning a 403 is standard:
SecDefaultAction "phase:1,log,auditlog,deny,status:403"
Verification
Test that ModSecurity is actively blocking a trivial SQL injection attempt. From a safe test host (never a production client):
curl -i "https://yourdomain.example/index.php?id=1%20UNION%20SELECT%201,2,3--"
You should receive a 403 Forbidden response. Confirm the block appeared in the audit log:
sudo grep 'UNION SELECT' /var/log/modsec_audit.log | tail -5
Troubleshooting
Server fails to start after enabling ModSecurity
Check the error log first. A common cause is a syntax error in an exclusion conf file:
sudo apache2ctl configtest
# or for Nginx:
sudo nginx -t
Legitimate traffic blocked after enabling enforcement
Drop back to DetectionOnly, reproduce the blocked request, and check the audit log for the rule ID. Then add a targeted exclusion as shown in the tuning section above. Never disable entire rule groups (900–999 ranges) without understanding what they cover.
Audit log is empty
Verify SecAuditEngine is set to On or RelevantOnly in modsecurity.conf, and that the audit log path is writable by the web server process user:
sudo ls -la /var/log/modsec_audit.log
sudo grep SecAuditEngine /etc/modsecurity/modsecurity.conf
High CPU after enabling CRS
CRS at paranoia level 1 (default) has modest overhead. Paranoia levels 3–4 are very expensive on busy sites. Check your configured paranoia level in crs-setup.conf and consider whether level 2 meets your risk requirements before jumping to 3 or 4.
Frequently asked questions
- What is the difference between ModSecurity 2.x and 3.x?
- ModSecurity 2.x is tightly coupled to Apache. Version 3.x refactored the engine into a standalone library (libmodsecurity3) with thin server-specific connectors, enabling Nginx and other server support. CRS works with both, but 3.x has some feature gaps compared to 2.x for complex rule logic; check the CRS compatibility notes for your version.
- How does OWASP CRS paranoia level affect performance and security?
- Paranoia level 1 (default) enables the most broadly applicable rules with low false-positive rates. Levels 2–4 add increasingly strict and niche rules, raising both CPU overhead and false-positive likelihood. Start at level 1, tune it clean, then evaluate whether your threat model justifies moving higher.
- Can I run ModSecurity in front of a reverse-proxied application?
- Yes. If Nginx or Apache is acting as a reverse proxy, ModSecurity inspects traffic before it forwards to the backend. This is a common deployment: the WAF sits at the edge, and the backend application server (Node.js, Gunicorn, etc.) never sees malicious requests.
- How do I keep OWASP CRS updated without breaking my exclusions?
- Manage CRS via git and keep all your custom exclusions in the two provided exclusion template files (REQUEST-900 and RESPONSE-999). Running git pull in the CRS directory updates the core rules while leaving your exclusion files untouched. Always review the CRS changelog before upgrading across major versions.
- What should I do if ModSecurity blocks a valid file upload?
- File upload false positives commonly come from rules inspecting multipart body content. Identify the rule ID from the audit log, then use SecRuleUpdateTargetById to exclude FILES or FILES_NAMES from that specific rule, or use ctl:ruleRemoveById scoped to the upload URI.
Related guides
Manage Secrets with Ansible Vault
Encrypt Ansible secrets with AES-256 using ansible-vault: encrypt files and inline vars, automate with password files, and isolate group-level secrets with vault IDs.
AppArmor Explained
Learn how AppArmor profiles work, how to switch between enforce and complain mode, create new profiles, and diagnose access denials on Ubuntu, Debian, and Arch.
Apply CIS Benchmarks with OpenSCAP
Use OpenSCAP and scap-security-guide to evaluate, report on, and remediate Linux systems against CIS Benchmarks — covering install, eval, and automation.
How to Audit a Linux System with auditd
Set up auditd on Linux to track file access, syscalls, and privilege use. Covers persistent rules, file watches, ausearch, and aureport across major distros.