Configure ModSecurity WAF on Ubuntu VPS: Step-by-Step

What ModSecurity Actually Does for a Hosting Server
A firewall blocks ports. ModSecurity inspects what's coming through them. It's a web application firewall (WAF) that sits in front of Apache and examines every HTTP request before it reaches your application — checking for SQL injection attempts, cross-site scripting payloads, path traversal tricks, and dozens of other attack patterns.
If you're running WordPress, WooCommerce, or any PHP application on a Hostperl VPS, ModSecurity with the OWASP Core Rule Set (CRS) is one of the most practical security layers you can add. It won't replace good passwords or timely updates, but it catches a lot of automated garbage that would otherwise reach your site's code.
This tutorial walks through a full installation on Ubuntu 22.04 or 24.04 LTS with Apache, including CRS setup and tuning to avoid blocking legitimate traffic.
Before You Begin
You'll need:
- Ubuntu 22.04 or 24.04 VPS with root or sudo access
- Apache 2.4 already installed and running
- At least one virtual host configured for your domain
- A basic familiarity with editing config files over SSH
If your firewall isn't locked down yet, run through the UFW firewall setup guide first. ModSecurity handles application-layer threats; UFW handles the network layer. Both matter.
Step 1: Install ModSecurity and the Apache Connector
Ubuntu's package repositories include ModSecurity 2.x, which is the stable, widely-deployed version compatible with Apache 2.4. Install it along with the Apache connector module:
sudo apt update
sudo apt install libapache2-mod-security2 -y
Once that completes, enable the module:
sudo a2enmod security2
sudo systemctl restart apache2
Confirm ModSecurity loaded without errors:
sudo apachectl -M | grep security
You should see security2_module (shared) in the output. If Apache threw an error on restart, check /var/log/apache2/error.log — the most common cause is a syntax issue in an existing config file unrelated to ModSecurity.
Step 2: Enable the Default Configuration
ModSecurity ships with a recommended config file, but it's named with a .recommended extension and won't load automatically. Copy it into place:
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
Open the file and change the engine mode from detection-only to active enforcement:
sudo nano /etc/modsecurity/modsecurity.conf
Find this line:
SecRuleEngine DetectionOnly
Change it to:
SecRuleEngine On
While you're in this file, also verify that SecRequestBodyAccess is set to On — this ensures POST body content (form submissions, API payloads) gets inspected, not just the URL and headers.
Save and close the file, but don't restart Apache yet. You'll add the OWASP rules first.
Step 3: Install the OWASP Core Rule Set
ModSecurity on its own has no rules — it's an engine. The OWASP Core Rule Set (CRS) is the standard ruleset that does the actual detection work. As of 2026, CRS 4.x is current and actively maintained.
Install it from the package repo:
sudo apt install modsecurity-crs -y
This places the rules in /usr/share/modsecurity-crs/. Now tell ModSecurity where to find them. Open the security2 module configuration:
sudo nano /etc/apache2/mods-enabled/security2.conf
Check that these lines are present and uncommented:
IncludeOptional /etc/modsecurity/*.conf
IncludeOptional /usr/share/modsecurity-crs/*.conf
IncludeOptional /usr/share/modsecurity-crs/rules/*.conf
If the CRS lines aren't there, add them. Save the file, then copy the CRS setup config:
sudo cp /usr/share/modsecurity-crs/crs-setup.conf.example /usr/share/modsecurity-crs/crs-setup.conf
Now restart Apache:
sudo systemctl restart apache2
If Apache starts cleanly, ModSecurity is running with OWASP rules active.
Step 4: Verify ModSecurity Is Blocking Threats
Run a quick test from your local machine or another server. The following curl command sends a classic SQL injection string in the URL:
curl -I "http://your-server-ip/?id=1+UNION+SELECT+1,2,3--"
ModSecurity should return a 403 Forbidden response. If you still get a 200 OK, the engine isn't enforcing — double-check that SecRuleEngine On is saved correctly and Apache was restarted afterward.
You can also confirm detections are being logged:
sudo tail -f /var/log/apache2/modsec_audit.log
Each blocked request gets a full entry here, including which rule triggered, the matched data, and the client IP. This log becomes useful later for tuning false positives.
Step 5: Handle False Positives Without Disabling Rules
The CRS is thorough, which means some legitimate requests will occasionally trigger it — especially in WordPress admin panels, e-commerce checkouts, or apps that accept rich text input. The mistake many admins make is lowering the paranoia level globally or disabling entire rule groups. That undermines the point.
The cleaner approach is targeted exclusions. Create a custom rules file to keep your changes separate from the packaged config:
sudo nano /etc/modsecurity/custom-rules.conf
To whitelist a specific IP (your office or agency IP) from all ModSecurity inspection:
SecRule REMOTE_ADDR "@ipMatch 203.0.113.45" \
"id:1001,phase:1,pass,nolog,ctl:ruleEngine=Off"
To disable a specific rule by ID for a particular path only — for example, rule 941100 (XSS detection) on the WordPress post editor:
SecRule REQUEST_URI "@beginsWith /wp-admin/post.php" \
"id:1002,phase:1,pass,nolog,ctl:ruleRemoveById=941100"
Find the offending rule ID in your audit log — it's listed under id in each rule match entry. This surgical approach lets you keep protection elsewhere while clearing the path for known-good traffic.
After adding exclusions, always test Apache config before restarting:
sudo apachectl configtest && sudo systemctl reload apache2
Step 6: Tune the Paranoia Level
CRS uses a paranoia level (PL) from 1 to 4 that controls how aggressively it applies rules. PL1 catches common attacks with minimal false positives. PL4 is very strict and will block edge-case payloads that most applications never send legitimately.
For a standard WordPress or PHP hosting environment, PL2 is a reasonable starting point — it adds more SQL injection and XSS patterns without becoming overwhelming. Open the CRS setup file:
sudo nano /usr/share/modsecurity-crs/crs-setup.conf
Find and set:
SecAction \
"id:900000,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
setvar:tx.paranoia_level=2"
Start at PL1 in production, review the audit log for a week, then move to PL2 once you've cleared known false positives. Jumping straight to PL3 on a live site is a reliable way to break things for real customers.
Step 7: Set Up Log Rotation for Audit Logs
The ModSecurity audit log grows quickly on a busy server. Without rotation, it can fill a disk partition in days on a site with moderate traffic. Ubuntu's logrotate handles this, but you need to add a config for the ModSecurity log specifically.
Create a logrotate config:
sudo nano /etc/logrotate.d/modsecurity
Add:
/var/log/apache2/modsec_audit.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
/usr/bin/systemctl reload apache2 > /dev/null 2>&1 || true
endscript
}
This keeps 14 days of compressed audit logs and rotates daily. If your server handles high request volumes, consider reducing to 7 days and adjusting SecAuditLogType in modsecurity.conf to Concurrent for better I/O performance. For a broader look at log management on Ubuntu, the logrotate setup guide for Ubuntu VPS covers the full configuration in detail.
Step 8: Enable Per-Virtual Host Control
If you're hosting multiple sites on one VPS, you may want ModSecurity on some virtual hosts but not others — or different paranoia levels per domain. Add ModSecurity directives directly inside a VirtualHost block:
<VirtualHost *:443>
ServerName example.com
# ... other directives ...
# Disable ModSecurity for this vhost only
SecRuleEngine Off
</VirtualHost>
Or to set a lower anomaly threshold for a staging site:
<VirtualHost *:443>
ServerName staging.example.com
SecAction "id:1010,phase:1,pass,nolog,setvar:tx.inbound_anomaly_score_threshold=10"
</VirtualHost>
This kind of per-site control is especially useful for agencies managing client sites where one application may be well-hardened and another is still in development. For more on managing multiple virtual hosts cleanly, the Apache virtual hosts guide is worth reading alongside this one.
Confirming Everything Is Working
Run through this checklist before considering the setup complete:
- Apache starts cleanly —
sudo systemctl status apache2shows active with no errors - SQL injection test returns 403 — verified with the curl test from Step 4
- Audit log is writing —
/var/log/apache2/modsec_audit.logcontains entries - Logrotate config is valid — run
sudo logrotate --debug /etc/logrotate.d/modsecurity - Your own site functions normally — log in to WP admin, submit a contact form, complete a checkout
- No false positives in the audit log for your own normal usage
If legitimate requests are being blocked, trace the rule ID in the audit log and add a targeted exclusion in /etc/modsecurity/custom-rules.conf as shown in Step 5. Don't disable the engine — work through false positives one rule at a time.
Running ModSecurity on a shared host isn't possible — you need server-level access to install and configure it. If your current hosting plan doesn't give you that control, a Hostperl VPS gives you full root access, SSD storage, and the flexibility to run the exact security stack your applications need. For high-traffic or compliance-sensitive workloads, our dedicated server plans remove the shared-resource constraints entirely. Our support team can help you migrate existing sites without downtime.
Frequently Asked Questions
Does ModSecurity slow down my site?
At paranoia level 1 or 2, the overhead is typically 1–3ms per request on a modern VPS — negligible for most sites. At PL4 with a large rule set, you may notice a few extra milliseconds on complex requests. For most hosting use cases, the tradeoff is well worth it.
Can I use ModSecurity with Nginx instead of Apache?
Yes, but the setup is different. Nginx doesn't support dynamic modules the same way, so you typically need to compile ModSecurity as a connector module or use the Nginx-ModSecurity connector. The OWASP rules themselves are identical. This tutorial specifically covers Apache.
Will ModSecurity protect against WordPress plugin vulnerabilities?
Partially. It won't patch a vulnerable plugin, but it can block exploit attempts that use common payload patterns — SQL injection, file inclusion, XSS. Virtual patching (writing a custom ModSecurity rule to block a known CVE) is a legitimate short-term fix while you wait for an upstream patch, but it's not a substitute for keeping plugins updated. For more on keeping your VPS environment secure beyond WAF rules, the VPS security checklist for 2026 covers the broader picture.
What's the difference between paranoia level 1 and 2?
PL1 enables core rules that catch clear-cut attacks with few false positives. PL2 adds stricter checks — more SQL injection patterns, additional header validation, and tighter path handling. PL2 is sensible for a well-known PHP application stack. PL3 and above are generally for high-security environments where staff are available to tune rules regularly.
How do I know if ModSecurity is blocking real attacks vs. false positives?
Check the audit log at /var/log/apache2/modsec_audit.log. Each entry shows the matched data — the actual string that triggered the rule. If the matched data looks like normal form content or URL parameters your app sends, it's a false positive. If it looks like UNION SELECT or <script>alert, it's a real attempt. Use that context to decide whether to add an exclusion or leave the rule as-is.
