ModSecurity and OWASP CRS 4 on Apache httpd: setup and tuning

Updated November 2026

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.

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 branches
ModSecurity v2ModSecurity v3 (libmodsecurity)
FormApache module mod_security2A library plus a connector for each web server
Main useApache httpd (also IIS)nginx, through the ModSecurity-nginx connector
With ApacheSupported and recommendedNot 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>
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:

CRS paranoia levels, as described by the CRS project
PLMeant forFalse positives
1Every site on the internet (the default)Rare
2Sites with real user data, such as an online shopExpect some; you must tune
3High-security sites such as online bankingMany, by design
4The "crown jewels", at your own riskA 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:

Keep it updated

Sources