On this page
Step-by-Step Guide: Securing a Linux VPS with CrowdSec, Fail2Ban, and Apache ModSecurity (OWASP CRS)
If you’re running a low-cost VPS — say, a $5/month Ubuntu instance — and hosting even a small web app or API, you will get scanned. Constantly.
I’ve seen it: after spinning up a new droplet, within minutes of opening port 22 or 80, you’ll see SSH brute-force attempts, WordPress login floods, SQL injection probes, and port scanners poking around. It’s not personal — it’s the internet.
The good news? You don’t need $200/month WAFs or enterprise firewalls. With CrowdSec, Fail2Ban, and Apache ModSecurity + OWASP CRS, you can build a robust, layered defense stack — all free, open-source, and lightweight enough to run on a 512MB RAM VPS.
Let me walk you through how I harden every VPS I deploy — no theory, just what actually works in production.
Why Layer Security? (CrowdSec + Fail2Ban + ModSecurity)
Before we dive in: why use three tools instead of one?
- Fail2Ban is reliable, lightweight, and great for SSH, SMTP, and basic HTTP brute force. But it’s reactive and per-service — it doesn’t understand web payloads.
- CrowdSec is newer but smarter: it’s a behavioral detection engine. It correlates events across services, uses community threat intelligence (the Bouncer system), and can auto-block IPs at the firewall level (iptables/nftables).
- ModSecurity + OWASP CRS is your last line of defense at the application layer — it inspects HTTP traffic, blocks SQLi, XSS, LFI, etc., in real time.
Together, they cover:
- Network-level abuse (SSH, FTP, etc.) → Fail2Ban + CrowdSec
- Web-layer attacks (SQLi, XSS, RCE) → ModSecurity + CRS
- Global threat context (e.g., known botnets, Tor exit nodes) → CrowdSec
They complement each other — and yes, they can run side-by-side without conflict (we’ll configure that).
Prerequisites
- A fresh Ubuntu 22.04 or 24.04 VPS (Debian-based works similarly)
- Root or sudo access
- Apache installed (
apt install apache2) - Basic familiarity with
bash,systemd, and config files
💡 Note: If you’re using Nginx, skip ModSecurity for now — I’ll cover it in a future post. This guide assumes Apache.
Step 1: Install & Configure Fail2Ban (The Classic Jail)
Fail2Ban is still the most straightforward way to block repeated SSH attacks.
Install
apt update && apt install fail2ban -y
Configure
Copy the default config to a local override:
cp /etc/fail2ban/jail.local /etc/fail2ban/jail.local.orig # backup
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit jail.local:
nano /etc/fail2ban/jail.local
Critical settings to tweak:
[DEFAULT]
# Ban IPs for 10 minutes (600s) — short enough to reset, long enough to deter
bantime = 600
findtime = 600 # look back 10 minutes
maxretry = 5 # 5 failures → ban
# Enable SSH jail (usually enabled by default)
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3 # stricter for SSH
# Optional: protect Apache (basic)
[apache-auth]
enabled = true
port = http,https
logpath = /var/log/apache2/error.log
maxretry = 6
⚠️ Don’t overdo
maxretry— too high = too slow to react. Too low = risk of locking yourself out during config.
Restart and enable:
systemctl restart fail2ban
systemctl enable fail2ban
Check status:
fail2ban-client status sshd
You’ll see active bans if any. If not — good! You’re not getting hammered yet.
Step 2: Install & Configure CrowdSec (The Behavioral Sentinel)
CrowdSec is where things get interesting. It’s not just IP blocking — it’s behavioral analysis.
Install
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
apt install crowdsec
Initialize & Enable
Run the interactive setup:
crowdsec
Answer prompts:
- Accept default database (
/var/lib/crowdsec/data/crowdsec.db) - Enable local API (yes)
- Enable CLI (yes)
Now, install the bouncers — the components that enforce blocks at the firewall or proxy level.
Install the iptables bouncer (for network-level blocking):
apt install crowdsec-firewall-bouncer-iptables
This writes rules into
iptables— no extra process needed.
Enable the nginx/apache bouncer (optional — we’ll use ModSecurity for deep web inspection instead)
Skip this for now — ModSecurity is more precise for HTTP attacks.
Test CrowdSec
Start the service:
systemctl start crowdsec
systemctl enable crowdsec
Generate a fake SSH brute-force (only in a test environment!):
for i in {1..6}; do ssh root@localhost 2>&1 | grep -q "Permission denied" || true; done
Check logs:
tail -f /var/log/crowdsec.log
You should see a decision being made.
Check active decisions:
crowdsec-cli decisions list
You’ll see something like:
+----------------------------------+----------+----------+------------+-----------+
| Value | Type | Action | Duration | Source |
+----------------------------------+----------+----------+------------+-----------+
| 192.168.1.100 | ip | ban | 1h0m0s | crowdsec |
+----------------------------------+----------+----------+------------+-----------+
✅ CrowdSec is blocking — and it’s using behavioral context, not just raw counts.
Step 3: Install & Configure ModSecurity + OWASP CRS (Web Firewall)
Now for the heavy hitter: ModSecurity. It’s Apache’s WAF — and with the OWASP Core Rule Set (CRS), you get battle-tested rules for SQLi, XSS, RCE, etc.
Install ModSecurity
apt install libapache2-mod-security2 -y
Enable the module:
a2enmod security2
Download & Install OWASP CRS
cd /etc/apache2
git clone https://github.com/coreruleset/coreruleset.git crs
Configure ModSecurity
Edit the main config:
nano /etc/apache2/mods-available/security2.conf
Replace the default rules with this minimal but solid setup:
<IfModule mod_security2.c>
# Base config
SecRuleEngine On
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyLimitAction Reject
SecResponseBodyAccess Off
# Logging
SecDebugLog /var/log/apache2/modsec_debug.log
SecDebugLogLevel 3
# Include CRS
IncludeOptional crs/crs-setup.conf
IncludeOptional crs/rules/*.conf
</IfModule>
🔍 Important:
SecRequestBodyLimitis set to ~12.5MB. Adjust if you run file uploads — but keep it low unless needed.
Configure OWASP CRS
Edit crs-setup.conf:
nano /etc/apache2/crs/crs-setup.conf
Tweak these (comment out defaults if needed):
# Set paranoia level: 1 = low false positives, 4 = high (use 1–2 for production)
SecAction "id:900110,phase:1,nolog,pass,ctl:paranoia_level=1"
# Set attack detection mode: 1 = anomaly mode (default, recommended)
SecAction "id:900120,phase:1,nolog,pass,ctl:rule_engine=On"
# Whitelist your own IP to avoid false positives during testing
SecRule REMOTE_ADDR "@ipMatch 192.168.1.100" "id:900000,phase:1,nolog,pass,ctl:ruleEngine=Off"
✅ Pro tip: Start with
paranoia_level=1. Level 2+ catches more, but will trigger false positives on common CMS (e.g., WordPress permalinks). You can upgrade later.
Restart Apache
apachectl configtest
systemctl reload apache2
Check logs:
tail -f /var/log/apache2/modsec_debug.log
If you see Access denied with code 403 (phase 2). Matched phrase "Attack detected" — it’s working.
To test safely, try a harmless SQLi probe (on a test page):
https://your-vps/test.php?id=1' OR '1'='1
You should get a 403 — and the rule ID in the log.
Step 4: Coordinate Fail2Ban & CrowdSec (No Overlap)
You can run both — but avoid double-banning.
Option 1: Let CrowdSec handle all IP blocking (recommended)
- Disable Fail2Ban’s firewall actions (it still logs/jails, but won’t write iptables rules).
- CrowdSec’s
iptablesbouncer handles all bans.
In /etc/fail2ban/jail.local:
[DEFAULT]
# Disable firewall actions
banaction = iptables-multiport
# → Change to:
banaction = none
Then restart Fail2Ban:
systemctl restart fail2ban
✅ This keeps Fail2Ban’s rich logging (great for auditing) while offloading enforcement to CrowdSec.
Option 2: Let Fail2Ban do SSH, CrowdSec do web/other
- Keep Fail2Ban for
[sshd] - Disable CrowdSec’s
iptablesbouncer (or use only CrowdSec for web)
But this adds complexity — and CrowdSec is better at contextual detection.
I recommend Option 1 unless you really need Fail2Ban’s SSH-specific tuning.
Comparison: CrowdSec vs Fail2Ban vs ModSecurity
| Feature | CrowdSec | Fail2Ban | ModSecurity + CRS |
|---|---|---|---|
| Layer | Network + Behavioral | Network (service logs) | Application (HTTP) |
| Real-time? | ✅ Yes (live analysis) | ❌ Delayed (log polling) | ✅ Yes (request-by-request) |
| Global context | ✅ Crowd intelligence | ❌ Local only | ❌ Local rules only |
| False positives | Low (configurable) | Medium (fixed thresholds) | Medium-High (tuning needed) |
| Resource usage | ~50–100MB RAM | ~20MB RAM | ~80–150MB RAM (CRS) |
| Best for | Proactive threat blocking | SSH/FTP brute force | SQLi/XSS/LFI protection |
| Requires tuning? | Minimal | Low | High (paranoia levels, exclusions) |
💡 My stack: CrowdSec for global blocking + Fail2Ban for SSH + ModSecurity for web.
Common Pitfalls & Fixes
❌ “I’m getting blocked but I’m not an attacker!”
- Fix: Whitelist your IP in CrowdSec and CRS.
- In
crs-setup.conf:SecRule REMOTE_ADDR "@ipMatch YOUR_IP" "id:900000,phase:1,nolog,pass,ctl:ruleEngine=Off" - In CrowdSec:
crowdsec-cli whitelist add --ip YOUR_IP --reason "home office"
- In
❌ “ModSecurity blocks everything!”
- Fix: Start with
paranoia_level=1, and check logs before blaming rules. - Use
SecRuleRemoveByIdin a custom file (e.g.,/etc/apache2/mods-enabled/security2-local.conf) to selectively disable rules.
❌ “CrowdSec doesn’t block SSH attacks!”
- Fix: Ensure
crowdsec-firewall-bouncer-iptablesis installed and running (systemctl status crowdsec-firewall-bouncer-iptables). - Check
/var/log/crowdsec.logfordecision addedon SSH events.
Maintenance & Monitoring
-
Check CrowdSec daily:
Bash crowdsec-cli decisions list --active -
Rotate logs — ModSecurity + CrowdSec logs grow fast. Use
logrotate:Bash cat > /etc/logrotate.d/modsec <<'EOF' /var/log/apache2/modsec*.log { daily rotate 14 compress delaycompress missingok notifempty create 0640 root adm } EOF -
Update rules:
- CrowdSec: automatic (via
crowdsecservice) - ModSecurity CRS:
cd /etc/apache2/crs && git pull(weekly) - Fail2Ban: restart after config changes
- CrowdSec: automatic (via
FAQ
1. Can I use CrowdSec without Fail2Ban?
Yes — CrowdSec can parse Apache/SSH logs directly. But Fail2Ban’s mature filters (e.g., apache-botscout) are still useful. Run both if you want layered logging.
2. Does ModSecurity slow down my site?
With paranoia_level=1 and basic CRS rules, latency is usually <1ms. On a 1GB RAM VPS, you won’t notice. Avoid paranoia_level=4.
3. What if I get false positives on my API?
Use ModSecurity’s ctl:ruleEngine=Off on specific endpoints:
<LocationMatch "^/api/.*">
SecRuleEngine Off
</LocationMatch>
Or whitelist Content-Type: application/json for your API routes.
4. Is this enough for GDPR/PCI-DSS?
No. This is baseline hardening — not a compliance solution. Use it as a foundation, then add logging aggregation (e.g., ELK), WAF reporting, and audit trails.
5. How do I unban an IP?
- CrowdSec:
crowdsec-cli decisions delete --ip IP - Fail2Ban:
fail2ban-client unban IP - ModSecurity: no manual unban — rules auto-expire or you restart Apache.
Final Thoughts
Hardening a VPS isn’t about adding more firewalls. It’s about reducing blast radius — and these tools do exactly that.
CrowdSec blocks known bad actors before they hit your server. Fail2Ban catches the low-hanging SSH bots. ModSecurity + CRS stops the clever ones who get past the first two layers.
It’s not perfect — but it’s realistic. You won’t get 100% protection (nothing does), but you’ll survive the daily noise.
I’ve used this stack on 50+ VPSes — from a $5 Ubuntu box to a $40/month Kubernetes node. It scales down.
If you’re just starting out: install CrowdSec + ModSecurity + CRS first. Add Fail2Ban later if you need SSH-specific tuning.
Security isn’t a one-time setup — it’s a habit. One rule at a time.
This guide is based on real deployments on Ubuntu 22.04/24.04. If you hit a snag, check the logs — they’ll tell you the story. And if you found this useful, I write more on practical sysadmin work at mahbuburriad.com.