Securing Your Self-Hosted Apps with Fail2Ban and CrowdSec

Securing Your Self-Hosted Apps with Fail2Ban and CrowdSec

We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.

If you're exposing services like Nextcloud, Vaultwarden, Gitea, or a Jellyfin instance to the internet, bots will find you within hours — usually minutes. I've watched fresh VPS droplets get hammered with SSH brute-force attempts before I'd even finished the initial setup. Fail2Ban has been the classic answer to this problem for years, but CrowdSec has changed the game with crowd-sourced threat intelligence. In this tutorial I'll show you how to run both tools together on a single Ubuntu 24.04 server so you get the best of both worlds.

Why Both Tools, Not Just One?

Fail2Ban is reactive and local: it watches log files and bans IPs that trip a threshold on your machine. It's incredibly flexible and battle-tested. CrowdSec is also reactive, but it additionally shares attack signals across a community of hundreds of thousands of servers, so it can pre-emptively block IPs that have been attacking other people's machines before they even knock on your door. I prefer running CrowdSec as the primary ban engine for web-facing apps and keeping Fail2Ban focused on SSH, because that's where each tool is strongest out of the box.

If you're running these services on a cloud VPS, I'd strongly recommend DigitalOcean Droplets — the networking is clean, the kernel is modern, and both tools play nicely with their firewall API. Create your DigitalOcean account today.

Step 1 — Install and Configure Fail2Ban for SSH

Start with Fail2Ban since it's simpler and gives you immediate SSH protection while you set up CrowdSec.

# Install Fail2Ban
sudo apt update && sudo apt install -y fail2ban

# Copy the default config so upgrades don't overwrite your changes
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

# Open jail.local and configure the SSH jail
sudo nano /etc/fail2ban/jail.local

Find the [sshd] section (or add it at the bottom of the file) and configure it like this. I use a 10-minute ban window with a low threshold — bots don't deserve second chances:

# /etc/fail2ban/jail.local — SSH jail configuration

[DEFAULT]
# Whitelist your own IP or home network here
ignoreip = 127.0.0.1/8 ::1

# Ban duration in seconds (3600 = 1 hour)
bantime  = 3600

# Time window to watch for failures
findtime = 600

# Number of failures before ban
maxretry = 3

# Email alerts (optional — replace with your address or remove)
destemail = [email protected]
sendername = Fail2Ban
mta = sendmail

[sshd]
enabled  = true
port     = ssh
logpath  = %(sshd_log)s
backend  = %(sshd_backend)s
maxretry = 3
bantime  = 3600

After editing, enable and start Fail2Ban:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Verify the SSH jail is active
sudo fail2ban-client status sshd

# Watch bans in real time
sudo tail -f /var/log/fail2ban.log
Tip: If you changed your SSH port from 22 to something custom (which you should — see our SSH hardening tutorial), set port = 2222 (or whatever your port is) in the [sshd] section, otherwise Fail2Ban won't monitor the right port.

Step 2 — Install CrowdSec

CrowdSec has an official repository for Debian/Ubuntu. The install is painless:

# Add the CrowdSec repository
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash

# Install CrowdSec and the firewall bouncer
sudo apt install -y crowdsec crowdsec-firewall-bouncer-iptables

# Check that the service is running
sudo systemctl status crowdsec

The bouncer is the component that actually enforces bans — CrowdSec's agent detects threats and writes decisions, but without a bouncer nothing gets blocked. The crowdsec-firewall-bouncer-iptables package translates those decisions into iptables/nftables rules automatically.

After installation, CrowdSec auto-detects common log sources and installs default collections. You can see what's been detected:

# Check installed collections
sudo cscli collections list

# Check active parsers
sudo cscli parsers list

# Check active scenarios (attack signatures)
sudo cscli scenarios list

# View current decisions (live bans)
sudo cscli decisions list

Step 3 — Add CrowdSec Coverage for Your Apps

Out of the box, CrowdSec handles SSH, Nginx, Apache, and a handful of others. If you're running Nextcloud, Vaultwarden, or other Docker-based apps behind a reverse proxy, you need to tell CrowdSec where those logs live and install the right collections.

For a Caddy or Nginx reverse proxy setup, install the relevant collections:

# Install collections for common self-hosted apps
sudo cscli collections install crowdsecurity/nginx
sudo cscli collections install crowdsecurity/caddy
sudo cscli collections install crowdsecurity/nextcloud
sudo cscli collections install crowdsecurity/http-cve

# If you're using WordPress or similar PHP apps:
sudo cscli collections install crowdsecurity/wordpress

# Reload CrowdSec to apply new collections
sudo systemctl reload crowdsec

Then configure the log acquisition file so CrowdSec knows where your app logs are:

sudo nano /etc/crowdsec/acquis.yaml
# /etc/crowdsec/acquis.yaml — add your log sources

# Nginx access logs (adjust path if using Docker volume mounts)
- filenames:
    - /var/log/nginx/access.log
    - /var/log/nginx/error.log
  labels:
    type: nginx

# Caddy logs (JSON format)
- filenames:
    - /var/log/caddy/access.log
  labels:
    type: caddy

# Nextcloud (if logging to a file)
- filenames:
    - /var/www/html/nextcloud/data/nextcloud.log
  labels:
    type: nextcloud

# SSH (already handled by default, but explicit here for clarity)
- filenames:
    - /var/log/auth.log
  labels:
    type: syslog

After editing, restart CrowdSec:

sudo systemctl restart crowdsec

# Check for any parsing errors
sudo journalctl -u crowdsec -f
Watch out: If your apps run in Docker, their logs may not be written to the host filesystem by default. You'll need to either configure Docker logging drivers to write to a host path, or use the CrowdSec Docker collection with socket access. The simplest fix is to mount a log directory as a volume in your docker-compose.yml and point your app to log there.

Step 4 — Register with the CrowdSec Console (Optional but Recommended)

The CrowdSec console at app.crowdsec.net gives you a dashboard of all decisions, alerts, and the ability to enroll in the community blocklist. Registration is free and the community blocklist alone is worth it — it pre-emptively blocks IPs that have been seen attacking other servers in the CrowdSec network.

# Register your instance (grab your enrollment key from app.crowdsec.net)
sudo cscli console enroll YOUR_ENROLLMENT_KEY

# Restart to apply
sudo systemctl restart crowdsec

# Verify the community blocklist is active
sudo cscli hub list

Step 5 — Testing That Everything Works

Don't assume it's working — verify it. CrowdSec has a built-in way to simulate an attack without actually triggering a real ban:

# Check the metrics dashboard (shows lines parsed, alerts triggered, etc.)
sudo cscli metrics

# Manually add a test ban (use a harmless IP — never your own!)
sudo cscli decisions add --ip 198.51.100.1 --duration 5m --reason "test ban"

# Confirm the ban is in iptables
sudo iptables -L INPUT -n | grep 198.51.100.1

# Remove it when done
sudo cscli decisions delete --ip 198.51.100.1

For Fail2Ban, you can manually test a jail:

# Check the status of all active jails
sudo fail2ban-client status

# Test what Fail2Ban sees for SSH
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

# Unban an IP you accidentally blocked
sudo fail2ban-client set sshd unbanip 203.0.113.5

Keeping Both Tools From Conflicting

Since both Fail2Ban and CrowdSec write iptables rules, there's a small chance of rule ordering conflicts. My approach: let Fail2Ban own SSH via its iptables-multiport action, and let CrowdSec's bouncer handle everything else via its own iptables chain (CROWDSEC_CHAIN). They don't typically fight each other, but if you see duplicate bans or rules disappearing, check which tool is managing which chain with sudo iptables -L -n -v.

Conclusion

Running Fail2Ban and CrowdSec in tandem gives you layered, reactive protection that's genuinely effective at keeping the noise off your self-hosted services. Fail2Ban handles SSH reliably and is easy to reason about; CrowdSec adds the community intelligence layer that means you're not fighting the internet alone. Once this is set up, check sudo cscli decisions list after 24 hours — the number of blocked IPs will probably surprise you.

Your next steps: head over to the Authelia tutorial to add SSO and MFA in front of your exposed apps, and consider pairing this with WireGuard so admin interfaces never have to be public-facing at all.

Build and deploy apps from code to production in just a few clicks

Discussion

```