ModSecurity and OWASP CRS 4 on Apache httpd: setup and tuning
ModSecurity is an open-source web application firewall (WAF) engine. On Apache httpd you run version 2 (the mod_security2 module) together with the OWASP Core Rule Set (CRS), version 4. Start in DetectionOnly mode, tune away the false alarms, and only then let it block.
- 2.9.15ModSecurity v2 for Apache, 2026-09-29 (as of November 2026)
- 4.30.0Latest OWASP CRS, 2026-10-02
- 4.25.2CRS long-term support (LTS) release, 2026-10-02
- 3.3.xCRS 3: end of life
Project status after Trustwave
ModSecurity was started in 2002, and Trustwave sponsored its development for many years. Trustwave announced the end of sale of its ModSecurity support on August 1, 2021, and the end of life of that support on July 1, 2024. After that date it stopped providing fixes and commercial rules, and maintenance of the code went back to the open-source community.
The project itself did not end. In 2024 its stewardship moved to OWASP (the Open Worldwide Application Security Project). The code now lives at github.com/owasp-modsecurity/ModSecurity, and OWASP lists ModSecurity as a production project. Both branches still get releases. As of November 2026 the latest are v2.9.15 and v3.0.17, both published on 2026-09-29. v2.9.15 fixes several security advisories in the engine, so the engine needs updating just like Apache.
The rules are a separate OWASP project, the OWASP CRS. As of November 2026 the latest release is 4.30.0 (2026-10-02). 4.25.2 is the current LTS release for sites that want fewer, slower changes. The CRS project lists 3.3.x as end of life. The final 3.3 release was 3.3.10 (2026-07-02).
v2 or v3?
| ModSecurity v2 | ModSecurity v3 (libmodsecurity) | |
|---|---|---|
| Form | Apache module mod_security2 | A library plus a connector for each web server |
| Main use | Apache httpd (also IIS) | nginx, through the ModSecurity-nginx connector |
| With Apache | Supported and recommended | Not supported by the CRS project |
The CRS documentation is explicit: Apache with ModSecurity v3 and nginx with ModSecurity v2 are configurations the project does not support. v2.9.13 also removed nginx support from the v2 code. For Apache, use v2.
Install the engine
Debian and Ubuntu
sudo apt install libapache2-mod-security2
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
sudo apache2ctl -t && sudo systemctl restart apache2
The package enables the module (and mod_unique_id, which it needs). /etc/apache2/mods-available/security2.conf includes every /etc/modsecurity/*.conf file and, if installed, the Debian CRS package. The recommended file sets SecRuleEngine DetectionOnly, so nothing is blocked yet. It also writes an audit log to /var/log/apache2/modsec_audit.log.
Debian 13 ships ModSecurity 2.9.11 and a modsecurity-crs package based on CRS 3.3.7 (as of November 2026). The CRS project lists CRS 3 as end of life, so install CRS 4 from the project instead, as shown below. Do not install both. If modsecurity-crs is already installed, remove it with sudo apt remove modsecurity-crs before you add CRS 4.
RHEL, AlmaLinux, Rocky, Fedora
# Fedora; on RHEL-compatible systems enable EPEL first if dnf cannot find it
sudo dnf install mod_security
sudo apachectl configtest && sudo systemctl restart httpd
The package installs /etc/httpd/conf.d/mod_security.conf, which also includes /etc/httpd/modsecurity.d/. Check its SecRuleEngine line. Fedora's file sets SecRuleEngine On, which means blocking from the start. Change it to DetectionOnly for now. Remember that distribution ModSecurity packages may lag behind upstream releases. The CRS documentation warns about exactly this.
To confirm that the module loaded:
sudo apachectl -M | grep security2
sudo grep -m1 'ModSecurity for Apache' /var/log/apache2/error.log # /var/log/httpd/error_log on RHEL
Install CRS 4
Download the release, verify its signature, and unpack it. Replace 4.30.0 with the version you want. The LTS release works the same way.
cd /tmp
wget https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.30.0.tar.gz
wget https://github.com/coreruleset/coreruleset/releases/download/v4.30.0/coreruleset-4.30.0.tar.gz.asc
gpg --fetch-key https://coreruleset.org/security.asc
gpg --verify coreruleset-4.30.0.tar.gz.asc v4.30.0.tar.gz
sudo mkdir /etc/crs4
sudo tar -xzf v4.30.0.tar.gz --strip-components 1 -C /etc/crs4
cd /etc/crs4
sudo cp crs-setup.conf.example crs-setup.conf
sudo cp rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
sudo cp rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
The signature check must report Good signature from "OWASP CRS <security@coreruleset.org>", and the key fingerprint must be 3600 6F0E 0BA1 6783 2158 8211 38EE ACA1 AB8A 6E72. The two exclusion files are where your own tuning goes. They are not overwritten when you update CRS.
Now include CRS after the engine configuration. The order must be: the engine settings (modsecurity.conf), then crs-setup.conf, then the rules.
<IfModule security2_module>
IncludeOptional /etc/crs4/crs-setup.conf
IncludeOptional /etc/crs4/plugins/*-config.conf
IncludeOptional /etc/crs4/plugins/*-before.conf
IncludeOptional /etc/crs4/rules/*.conf
IncludeOptional /etc/crs4/plugins/*-after.conf
</IfModule>
- Debian, Ubuntu: save it as
/etc/apache2/conf-available/crs4.confand runsudo a2enconf crs4. Debian loadsmods-enabled(with the engine settings) beforeconf-enabled, so the order is right. - RHEL family: save it as
/etc/httpd/conf.d/zz-crs4.conf. Thezz-prefix makes it load aftermod_security.conf. If a distribution CRS package is installed, remove it or its include lines, so that only one copy of CRS is loaded.
sudo apachectl configtest && sudo systemctl restart apache2 # httpd on RHEL
Check that it works
curl -s -o /dev/null -w '%{http_code}\n' 'https://www.example.com/?foo=/etc/passwd&bar=/bin/sh'
sudo grep -c 'OWASP_CRS' /var/log/apache2/error.log
In DetectionOnly mode the request still returns 200, but the error log and the audit log gain ModSecurity entries naming the rules that matched. Once you switch to blocking, the same request returns 403. If you see no log entries at all, CRS is not loaded. Check the include lines and apachectl -t -D DUMP_INCLUDES.
Paranoia levels and anomaly scores
CRS does not block on the first suspicious match. Each matching rule adds points to an anomaly score, and the request is blocked when the score reaches a threshold. The defaults are 5 for requests and 4 for responses. A single critical match scores 5, so it blocks on its own.
The paranoia level (PL) decides how many rules run:
| PL | Meant for | False positives |
|---|---|---|
| 1 | Every site on the internet (the default) | Rare |
| 2 | Sites with real user data, such as an online shop | Expect some; you must tune |
| 3 | High-security sites such as online banking | Many, by design |
| 4 | The "crown jewels", at your own risk | A great many |
You set the level in crs-setup.conf by uncommenting rule 900000 and changing tx.blocking_paranoia_level. To try a higher level safely, uncomment rule 900001 and set tx.detection_paranoia_level one higher. The extra rules then run and log, but do not count toward blocking. Fix the new false positives, then raise the blocking level. The CRS team advises keeping the inbound threshold at 5 and raising the paranoia level, rather than raising the threshold.
Tune false positives
A false positive is a legitimate request that matches a rule. Common examples are a CMS editor posting HTML, or a password that happens to contain /bin/bash. Never edit the CRS rule files: the next update would undo your changes. Write rule exclusions instead. First find the rule id and the variable in the log:
sudo grep -o '\[id "[0-9]*"\]' /var/log/apache2/error.log | sort | uniq -c | sort -rn | head
That gives the rules that fire most often. For one rule, see which parameter and URL trigger it:
sudo grep '\[id "941160"\]' /var/log/apache2/error.log | grep -o 'at [A-Z_]*:[^ .]*\|\[uri "[^"]*"\]' | sort | uniq -c | sort -rn
Then add the narrowest exclusion that fixes it. Put runtime exclusions (the ctl: actions) in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf, and removals at configure time in RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf:
# REQUEST-900-...: ignore the "body" field of the editor form for one XSS rule
SecRule REQUEST_URI "@beginsWith /admin/post-edit" \
"id:10010,\
phase:1,\
pass,\
t:none,\
nolog,\
ctl:ruleRemoveTargetById=941160;ARGS:body"
# RESPONSE-999-...: the site has no SQL database, drop all SQLi rules everywhere
SecRuleRemoveByTag attack-sqli
Give your own rules ids that do not clash with CRS. Excluding one parameter from one rule on one URL is far safer than removing a rule for the whole site. For common applications such as WordPress or Nextcloud, CRS 4 offers exclusion plugins, which replaced the CRS 3 exclusion packages. Put them in /etc/crs4/plugins/.
Switch to blocking
When the logs have stayed free of false positives for real traffic (a week or two covers most sites, longer for complex applications), set the engine to block:
# /etc/modsecurity/modsecurity.conf (Debian) or mod_security.conf (RHEL)
SecRuleEngine On
Test and restart, then watch the error log closely for the first days. Two more things to know:
- CRS also inspects responses by default. If your pages echo user input, an attacker can post content that makes the WAF block a page for everyone. The CRS project calls this RFDoS. Since CRS 4.9.0 you can switch response checks off by uncommenting rule 900500 in
crs-setup.conf. - ModSecurity can log sensitive data, such as the password in the example above. Protect the audit log and keep it only as long as you need it.
Keep it updated
- Update the engine with your distribution's security updates, or rebuild for each upstream v2 release.
- Watch CRS releases. To update, unpack the new release over
/etc/crs4, then compare yourcrs-setup.confwith the newcrs-setup.conf.example. - Run the LTS line if you cannot retest every month. Run the latest line if you want new detections first.
- Remember that a WAF is an extra layer. It does not replace patching Apache and the application (Vulnerabilities (CVEs)).
Sources
- OWASP ModSecurity project page, OWASP Foundation
- ModSecurity repository and README (stewardship transfer note), OWASP ModSecurity on GitHub
- ModSecurity releases (v2.9.15, v3.0.17), GitHub
- End-of-Sale and Trustwave Support for ModSecurity Web Application Firewall, Trustwave (now LevelBlue)
- OWASP CRS project home page
- CRS releases (4.30.0, 4.25.2 LTS, 3.3.10), GitHub
- CRS Installation, CRS documentation
- Extended Install, CRS documentation
- Anomaly Scoring, CRS documentation
- Paranoia Levels, CRS documentation
- False Positives and Tuning, CRS documentation
- Debian modsecurity-apache 2.9.11 security2.conf, sources.debian.org
- mod_security.conf, Fedora package sources
- modsecurity-crs package versions, Debian packages