Enabling ModSecurity in front of Apache adds a request-inspection layer that can catch common attack patterns before they reach the application. That gives the server a chance to reject suspicious traffic at the web tier instead of relying on the application to detect every payload on its own.
On current Ubuntu and Debian packaging, the libapache2-mod-security2 package loads security2_module for Apache and the module configuration in /etc/apache2/mods-available/security2.conf includes local files from /etc/modsecurity/*.conf plus the packaged OWASP ModSecurity Core Rule Set loader from /usr/share/modsecurity-crs/*.load. The recommended base policy file is /etc/modsecurity/modsecurity.conf-recommended, and the packaged CRS setup file lives at /etc/modsecurity/crs/crs-setup.conf.
CRS rules can block real login flows, uploads, JSON APIs, or admin forms until exclusions are tuned, so the safest first pass keeps SecRuleEngine in DetectionOnly mode and confirms matches in the audit log before enforcement is turned on. A local test rule makes it possible to prove that logging works, then prove that blocking works, and then remove the temporary rule before leaving the server in production service.
Related: How to enable or disable Apache modules
Related: How to test Apache configuration
$ sudo apt update Hit:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease Hit:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease Hit:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease Hit:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease Reading package lists... $ sudo apt install --assume-yes libapache2-mod-security2 modsecurity-crs Reading package lists... Building dependency tree... Reading state information... The following additional packages will be installed: liblua5.1-0 libyajl2 ##### snipped ##### Setting up libapache2-mod-security2 (2.9.7-1build3) ... apache2_invoke: Enable module security2
Current Ubuntu releases already pull in modsecurity-crs when libapache2-mod-security2 is installed, but installing both package names keeps the intent explicit.
$ sudo a2enmod security2 Considering dependency unique_id for security2: Module unique_id already enabled Module security2 already enabled
The package usually enables security2 during installation. Re-running a2enmod confirms the state and restores the module if it had been disabled earlier. A separate headers step is not required for the base ModSecurity and CRS enablement path.
$ sudo cp --update=none /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
Using --update=none keeps an existing tuned file in place instead of overwriting it.
$ sudo grep -n '^SecRuleEngine' /etc/modsecurity/modsecurity.conf 7:SecRuleEngine DetectionOnly
If the copied file shows On or Off instead, change it before the first reload: sudo sed --in-place 's/^SecRuleEngine .*/SecRuleEngine DetectionOnly/' /etc/modsecurity/modsecurity.conf.
$ sudo sed -n '1,20p' /etc/apache2/mods-available/security2.conf
<IfModule security2_module>
# Default Debian dir for modsecurity's persistent data
SecDataDir /var/cache/modsecurity
# Include all the *.conf files in /etc/modsecurity.
# Keeping your local configuration in that directory
# will allow for an easy upgrade of THIS file and
# make your life easier
IncludeOptional /etc/modsecurity/*.conf
# Include OWASP ModSecurity CRS rules if installed
IncludeOptional /usr/share/modsecurity-crs/*.load
</IfModule>
$ sudo ls -l /etc/modsecurity/crs/crs-setup.conf -rw-r--r-- 1 root root 35016 Oct 1 2023 /etc/modsecurity/crs/crs-setup.conf
Keep the packaged CRS defaults for the first pass. Application-specific exclusion packages and other baseline tuning live in /etc/modsecurity/crs/crs-setup.conf.
$ sudo apache2ctl -t Syntax OK
Related: How to test Apache configuration
$ sudo systemctl restart apache2
$ sudo apache2ctl -M | grep security2 security2_module (shared)
$ sudo tee /etc/modsecurity/local-test.conf >/dev/null <<'EOF' SecRule ARGS:modsectest "@streq 1" "id:10001,phase:2,deny,status:403,log,msg:'ModSecurity local test rule hit'" EOF
Keep one-off local rule IDs unique. The local reservation range is 1 through 99999, so do not reuse IDs from packaged rule sets or third-party vendors.
$ sudo apache2ctl -t Syntax OK
Related: How to test Apache configuration
$ sudo systemctl reload apache2
$ sudo grep -n '^SecAuditLog ' /etc/modsecurity/modsecurity.conf 205:SecAuditLog /var/log/apache2/modsec_audit.log
$ curl --silent --show-error -i 'http://127.0.0.1/?modsectest=1' HTTP/1.1 200 OK Date: Thu, 09 Apr 2026 04:25:11 GMT Server: Apache/2.4.58 (Ubuntu) Last-Modified: Thu, 09 Apr 2026 04:25:05 GMT ETag: "29af-64eff6697728d" Accept-Ranges: bytes Content-Length: 10671 Vary: Accept-Encoding Content-Type: text/html ##### snipped #####
DetectionOnly logs the match but still serves the response.
$ sudo grep -n "ModSecurity local test rule hit" /var/log/apache2/modsec_audit.log | tail --lines=1 386:Apache-Error: [file "apache2_util.c"] [line 275] [level 3] [client 127.0.0.1] ModSecurity: Warning. String match "1" at ARGS:modsectest. [file "/etc/modsecurity/local-test.conf"] [line "1"] [id "10001"] [msg "ModSecurity local test rule hit"] [hostname "127.0.0.1"] [uri "/"] [unique_id "adcqJ1IlXFSWFJk8REUQcAAAAEA"]
Application-specific exclusion packages can be enabled from /etc/modsecurity/crs/crs-setup.conf, while site-specific local overrides can stay in separate files under /etc/modsecurity.
$ sudo sed --in-place 's/^SecRuleEngine .*/SecRuleEngine On/' /etc/modsecurity/modsecurity.conf
Turning the engine on can block legitimate requests until exclusions are tuned. Keep a quick rollback path available.
$ sudo grep -n '^SecRuleEngine' /etc/modsecurity/modsecurity.conf 7:SecRuleEngine On
$ sudo apache2ctl -t Syntax OK
Related: How to test Apache configuration
$ sudo systemctl reload apache2
$ curl --silent --show-error -i 'http://127.0.0.1/?modsectest=1' HTTP/1.1 403 Forbidden Date: Thu, 09 Apr 2026 04:25:14 GMT Server: Apache/2.4.58 (Ubuntu) Content-Length: 274 Content-Type: text/html; charset=iso-8859-1 ##### snipped #####
$ sudo rm --force /etc/modsecurity/local-test.conf
$ sudo systemctl reload apache2
$ curl --silent --show-error -i 'http://127.0.0.1/?modsectest=1' HTTP/1.1 200 OK Date: Thu, 09 Apr 2026 04:25:16 GMT Server: Apache/2.4.58 (Ubuntu) Last-Modified: Thu, 09 Apr 2026 04:25:05 GMT ETag: "29af-64eff6697728d" Accept-Ranges: bytes Content-Length: 10671 Vary: Accept-Encoding Content-Type: text/html ##### snipped #####
After the temporary rule is removed, only the packaged CRS rules and any real local policy files remain active. That is the right time to keep tuning exclusions before relying on enforcement for production traffic.