ModSecurity is an open-source Web Application Firewall (WAF) that runs as an Apache or Nginx module. It analyzes HTTP traffic in real time and blocks SQL injection, XSS, RFI, and other attacks.
Installing ModSecurity on Ubuntu/Apache
sudo apt update
sudo apt install -y libapache2-mod-security2
# Enable the module
sudo a2enmod security2
sudo systemctl restart apache2
# Check
apachectl -M | grep security
Basic configuration
# Copy the config
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
# Change the mode from DetectionOnly to On
sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/modsecurity/modsecurity.conf
Installing OWASP CRS (Core Rule Set)
sudo apt install -y modsecurity-crs
# Or manually:
cd /etc/modsecurity
sudo git clone https://github.com/coreruleset/coreruleset.git
cd coreruleset
sudo cp crs-setup.conf.example crs-setup.conf
Operating modes
| Mode | Config | Action |
|---|---|---|
| Detection Only | SecRuleEngine DetectionOnly | Logs, doesn't block |
| Active (On) | SecRuleEngine On | Blocks requests |
| Off | SecRuleEngine Off | Completely disabled |
Managing false positives
# Disable the rule for a specific URL
SecRule REQUEST_URI "@beginsWith /wp-admin/post.php" "id:1000001,phase:1,pass,nolog,ctl:ruleRemoveById=941100"
# Disable the rule globally (not recommended)
SecRuleRemoveById 941100
ModSecurity logs
# View blocked requests
sudo tail -f /var/log/apache2/modsec_audit.log | grep "HTTP 403"
# Attack statistics
sudo grep "OWASP" /var/log/apache2/modsec_audit.log | awk '{print $NF}' | sort | uniq -c | sort -rn
Real ModSecurity messages explained
AH00526: Syntax error on line X ...: Invalid command 'SecRule', perhaps misspelled or defined by a module not included in the server configuration— this is exactly whatapachectl configtestreports when rule files are included but thesecurity2module itself is not enabled, or the server was never restarted aftera2enmod.ModSecurity: Access denied with code 403 (phase 2). Matched "Operator ... to variable ARGS:..." [id "941100"] [msg "XSS Attack Detected via libinjection"]— a typical audit log line when a rule fires; theidvalue is the specific rule number used to disable just that one rule withctl:ruleRemoveById.ModSecurity: Warning. ... [id "200002"] [msg "Failed to parse request body."]— a common false positive during a multipart file upload, usually when the request body exceedsSecRequestBodyLimitor uses a nonstandard boundary.ModSecurity: Rule ... execution error - PCRE limit exceeded— a regular expression from one of the CRS rules did not finish matching against an overly long field value; fixed by raisingSecPcreMatchLimitor limiting the field's length at the application level.- A rule is disabled with
SecRuleEngine Offinside one specific<VirtualHost>, while the global config hasOn— Apache applies the more specific directive, so that one site stays unprotected while logs for every other domain show the WAF working normally.
OWASP CRS paranoia levels
| Paranoia Level | What it covers | False positives | Recommendation |
|---|---|---|---|
| PL1 | Basic rules against the most common attacks: SQL injection and XSS with clear signatures | Minimal | The starting level for most sites |
| PL2 | Adds broader heuristics and checks for nonstandard encodings | Occasional, on unusual forms | For sites with higher security requirements |
| PL3 | Strict checks, many additional patterns | Frequent, needs manual exception tuning | Only after weeks of testing in DetectionOnly mode |
| PL4 | The most conservative rule set available | Very frequent on any nonstandard input | Specialized environments like payments or government, almost always with manual exceptions |
When ModSecurity is not the right fit
- A site with a millisecond response budget and very high load — inspecting every request against every CRS rule adds noticeable latency; such projects more often put a WAF at the edge (such as Cloudflare) instead of on every backend server.
- A project under active development with forms that change often — at a paranoia level above PL1, you end up regularly triaging new false positives instead of configuring the rules once and moving on.
- Shared hosting with no root access, only PHP over FTP — ModSecurity runs at the web server level and needs administrative access; on shared hosting without that, it cannot be enabled, and only the provider's built-in WAF, if any, is available.
- Protection against business-logic attacks — tampering with an order's price, bypassing paid content through requests that are technically well-formed — ModSecurity looks for signs of known attacks in the request itself, not application logic; that needs code on the application side, not WAF rules.
Verifying that the WAF actually blocks attacks
# Confirm the module is actually loaded, not just present in the config
apachectl -M | grep security
# Send a harmless test payload and confirm the WAF catches it
curl "https://example.com/?test=<script>alert(1)</script>"
# A 403 response is expected if the XSS rule is active
# Check which exact rule fired
sudo tail -20 /var/log/apache2/modsec_audit.log | grep -E "id \"|msg "
If curl returns a normal page instead of 403, either the engine is in DetectionOnly mode, or the specific XSS rule is disabled or overridden at the virtual host level; in that case, go back to the first item in this section.
Frequently asked questions
Does ModSecurity replace antivirus or malware scanning on the server?
No, these are different jobs. ModSecurity analyzes incoming HTTP requests in real time and looks for signs of known attacks in the request itself, not the content of files on disk. Finding malicious scripts or web shells already uploaded to the server requires a separate filesystem antivirus scanner — ModSecurity does not replace that and does not perform that function.
If a site already sits behind Cloudflare's WAF, is ModSecurity on the server still needed?
That is sensible redundancy, not overkill. Cloudflare filters traffic at the network edge and catches most mass attacks before they reach the server, but it does not protect against requests that hit the server's IP directly, bypassing the proxy. ModSecurity on the server remains the last line of defense for exactly that kind of bypass.
Can ModSecurity rules be set per individual site rather than as one shared set for the whole server?
Yes, rules can be set both globally for the whole server and inside the configuration of a specific VirtualHost — a more specific setting in Apache always overrides the general one. This is useful when one site on the server needs a stricter level of checking while another regularly triggers false positives on the same rule set.
Does enabling ModSecurity noticeably slow down the site for regular visitors?
There is some slowdown, but on most sites it goes unnoticed: the rules check a request in a fraction of a second. Delay becomes noticeable only on very high-traffic projects or with the strictest Paranoia level enabled, adding dozens of extra checks per request — then a WAF is placed at the network edge rather than on every backend.