Your Server Is Getting Hammered and You Know It
Go ahead. Run this right now:
sudo journalctl -u ssh --since "1 hour ago" | grep "Failed password" | wc -l
If that number is anything above zero — and it almost certainly isn’t zero — congratulations. You’re a target. Not because you’re special. Because every publicly-accessible server on the internet is a target within minutes of getting an IP address. Bots don’t discriminate. They just knock on every door, forever, until someone lets them in or they find somewhere easier.
The good news: you have options. Two very good ones, actually.
Fail2ban is the old reliable — the bouncer who reads the guest list (your logs) and kicks people out who’ve had too many failed attempts. It’s been doing this job since 2004 and it’s very good at it.
CrowdSec is the new kid who brought an entire neighborhood watch. It still watches the logs, but it also radios every other CrowdSec installation in the world and says “hey, this IP is being sketchy — ban it preemptively.”
Let’s dig into both.
Fail2ban: The Classic Log-Parsing Guard Dog
Fail2ban works on a beautifully simple premise: read log files, find patterns that indicate abuse, ban the IP using your firewall. That’s it. No accounts, no cloud, no telemetry. Just regex, logs, and iptables doing its thing.
How It Actually Works
Fail2ban runs as a daemon and watches log files using “jails.” Each jail defines:
- Which log file to watch
- Which filter (regex) to apply
- How many failures trigger a ban
- How long the ban lasts
When a jail catches enough failures from one IP, fail2ban issues a ban through whatever “action” you’ve configured — usually iptables, nftables, or firewalld.
Setting Up a Basic SSH Jail
Install it first:
# Debian/Ubuntu
sudo apt install fail2ban
# RHEL/Rocky/Alma
sudo dnf install fail2ban
# Enable and start
sudo systemctl enable --now fail2ban
Now configure your jails. Don’t edit /etc/fail2ban/jail.conf directly — it gets overwritten on updates. Use the local override instead:
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# Ban duration (10 minutes)
bantime = 10m
# Window to count failures in
findtime = 10m
# Number of failures before ban
maxretry = 5
# Use systemd backend for better performance
backend = systemd
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 3
bantime = 1h
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 24h
Restart and check status:
sudo systemctl restart fail2ban
# Check overall status
sudo fail2ban-client status
# Check a specific jail
sudo fail2ban-client status sshd
You’ll see something like:
Status for the jail: sshd
|- Filter
| |- Currently failed: 3
| |- Total failed: 847
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 2
|- Total banned: 156
`- Banned IP list: 192.168.1.100 10.0.0.42
856 failed attempts and you’ve only been running it a week. Told you.
Fail2ban’s Limitations
Fail2ban is great but it has a few rough edges worth knowing about:
It’s purely reactive. An IP has to fail at your server before it gets banned. That first attempt always gets through.
It’s single-server. Your ban lists don’t share across your homelab. If you’ve got five VMs, that bot is hitting all five.
Regex maintenance is a pain. New log formats break filters. Application updates break filters. Your own sanity breaks a little every time.
No threat intelligence. Fail2ban doesn’t know that an IP has been hitting a thousand other servers today. It only knows what’s happened on yours.
For a single server or a small homelab, none of this is disqualifying. But it’s why CrowdSec exists.
CrowdSec: Fail2ban With a Neighborhood Watch
CrowdSec looks similar on the surface — it reads logs, detects attacks, bans IPs. But underneath, it’s a completely different architecture.
The big idea: community threat intelligence. Every CrowdSec installation that catches an attack shares that signal (anonymized) to a central database. Every other installation can then query that database and preemptively block known bad actors before they even knock on your door.
The Architecture (It’s Worth Understanding)
CrowdSec has two main components:
The Security Engine (Agent): Watches your logs, applies detection scenarios, and reports decisions to the Local API (LAPI). Think of this as the intelligence layer.
Bouncers: These are the enforcement layer. They talk to the LAPI and actually block traffic — via firewall rules, nginx, HAProxy, Cloudflare, etc. The separation is clever: your detection logic is separate from your enforcement mechanism, so you can swap one without touching the other.
This also means you can run one CrowdSec agent watching multiple services, with multiple bouncers enforcing blocks at different layers.
Installing CrowdSec
# Add the official repo (Debian/Ubuntu)
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
sudo apt install crowdsec
# Install the firewall bouncer
sudo apt install crowdsec-firewall-bouncer-iptables
# or for nftables:
sudo apt install crowdsec-firewall-bouncer-nftables
Check that everything’s running:
sudo systemctl status crowdsec
sudo cscli version
CrowdSec comes with collections
Rather than writing regex yourself, CrowdSec uses “collections” — curated detection scenarios for common services. Install what you need:
# List installed collections
sudo cscli collections list
# Install collections for your services
sudo cscli collections install crowdsecurity/nginx
sudo cscli collections install crowdsecurity/ssh-bf
sudo cscli collections install crowdsecurity/linux
# Reload after installing
sudo systemctl reload crowdsec
Checking Decisions
This is where it gets fun:
# See active bans
sudo cscli decisions list
# See recent alerts
sudo cscli alerts list
# See what the community blocklist has flagged
sudo cscli bouncers list
Sample output from cscli decisions list:
+--------+----------------+-------------------+------+--------+---------+
| Source | IP | Reason | Action | Country | Expiration |
+--------+----------------+-------------------+------+--------+---------+
| crowdsec | 185.220.101.32 | crowdsecurity/ssh-bf | ban | RU | 3h59m |
| CAPI | 45.33.32.156 | community-blocklist | ban | US | 23h12m |
+--------+----------------+-------------------+------+--------+---------+
That second line — CAPI source — is a ban that came from the community blocklist, not from anything that happened on your server. CrowdSec already knew that IP was bad because thousands of other servers reported it.
Running CrowdSec in Docker
If you’re self-hosting with Docker (and you are, because you’re here), here’s a solid compose setup:
version: "3.8"
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
- GID=1000
- COLLECTIONS=crowdsecurity/nginx crowdsecurity/ssh-bf crowdsecurity/linux
volumes:
- ./crowdsec/config:/etc/crowdsec
- ./crowdsec/data:/var/lib/crowdsec/data
# Mount your log directories
- /var/log/nginx:/var/log/nginx:ro
- /var/log/auth.log:/var/log/auth.log:ro
networks:
- proxy
bouncer-traefik:
image: fbonalair/traefik-crowdsec-bouncer:latest
container_name: bouncer-traefik
restart: unless-stopped
environment:
- CROWDSEC_BOUNCER_API_KEY=${CROWDSEC_BOUNCER_API_KEY}
- CROWDSEC_AGENT_HOST=crowdsec:8080
networks:
- proxy
networks:
proxy:
external: true
Register the bouncer to get an API key:
docker exec crowdsec cscli bouncers add traefik-bouncer
# Copy the API key it outputs into your .env file
Fail2ban vs CrowdSec: The Side-by-Side
| Feature | Fail2ban | CrowdSec |
|---|---|---|
| Log parsing | Yes | Yes |
| Reactive banning | Yes | Yes |
| Proactive/community bans | No | Yes |
| Multi-server sync | No | Yes (via LAPI) |
| Docker-native | Passable | First-class |
| Cloud-based component | None | Optional (CAPI) |
| Privacy | Fully local | Anonymized sharing |
| Complexity | Low | Medium |
| Bouncer model | Built-in actions | Separate bouncers |
| Community collections | Regex filters only | Full scenario library |
| Resource usage | Very light | Light-moderate |
| Setup time | 15 minutes | 30-45 minutes |
When Should You Use Which?
Use Fail2ban if:
- You have a single server and want something that just works
- You’re already familiar with it and it’s doing its job
- You want zero external dependencies, full local operation
- Your threat model is “script kiddies, not nation-states”
- You’ve already got iptables rules you don’t want to rethink
Use CrowdSec if:
- You’re running multiple servers or containers
- You want proactive protection, not just reactive
- You’re in a Docker/Traefik/Nginx Proxy Manager environment
- You want community threat intelligence without buying a product
- You’re building out a more serious homelab or small production setup
Use both if:
- You’re paranoid (healthy)
- You want defense in depth
- You’re already running Fail2ban and want to layer on CrowdSec’s intelligence
Can You Run Both at the Same Time?
Yes. Completely fine. They operate independently and don’t conflict.
A common setup: Fail2ban handles local log-based banning via iptables, while CrowdSec runs its firewall bouncer that adds to a separate ipset. They don’t step on each other’s chains.
You might even prefer this during migration — run both, watch both sets of logs for a while, get comfortable with CrowdSec, then decide if you want to drop Fail2ban. Or keep both. Defense in depth is a legitimate strategy, not just paranoia.
Just make sure you’re not double-counting resource usage on a very constrained VPS. But honestly, both are lightweight enough that it’s not usually a concern.
A Few Practical Tips Before You Go
Whitelist your own IPs first. Both tools will happily ban you if you typo your password too many times. In Fail2ban, add to jail.local:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 YOUR.HOME.IP.HERE
In CrowdSec:
sudo cscli decisions add --ip YOUR.HOME.IP.HERE --type allow --duration 8760h
Set sensible ban times. The default 10-minute ban in Fail2ban is a slap on the wrist for a bot. For SSH brute force, 24 hours is more satisfying. A week is appropriate levels of petty.
Watch your logs after setup. Both tools are only as good as the patterns they’re catching. Check fail2ban-client status sshd and cscli alerts list regularly when you first set things up. You’ll be amazed — and slightly horrified — by what you see.
CrowdSec’s dashboard is genuinely nice. You can connect to app.crowdsec.net for a free overview of your security posture. It’s optional, but worth a look.
The Bottom Line
Your server is going to get probed. That’s just the internet being the internet. The question is whether you greet those probes with indifference, a single bouncer, or an entire coordinated security detail.
Fail2ban is battle-tested, simple, and still totally valid in 2025. If you’ve got it running and it’s working, there’s no burning need to rip it out.
But if you’re setting something up fresh, or you’re tired of every server in your homelab fighting its own battles in isolation, CrowdSec is worth the extra setup time. The community blocklist alone — all those IPs pre-emptively flagged before they even reach your logs — is a genuine upgrade over flying blind.
Pick one, set it up properly, whitelist yourself, and then go do something else. Your servers will handle the rest.
Your 2 AM self will appreciate it.