$linuxjunkies
>

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.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

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 for ctl:ruleRemoveById and similar.
  • RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf — runs after; use for SecRuleUpdateTargetById.

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.

tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

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