fail2ban and CrowdSec for VPN servers
How to choose between Fail2Ban and CrowdSec on public VPN gateways, when one tool is enough, and how to avoid two intrusion tools fighting over your firewall.
Public VPN servers attract a very specific class of garbage traffic. On a minimal WireGuard box, the real control plane is usually just SSH. Maybe HTTPS too, if you insisted on running a panel. That means the best abuse-prevention stack is often much simpler than the average "modern intrusion prevention" blog post wants to admit.
My opinion first:
- On a minimal SSH/WireGuard gateway, Fail2Ban is often enough.
- On a multi-service edge box, CrowdSec becomes more interesting.
- Running both is fine only if they do not fight over the same firewall logic.
If you start with that framing, the rest becomes straightforward.
What a public VPN server actually gets attacked on
For a small gateway, WireGuard itself is not the main brute-force target. SSH is.
That changes the problem. You do not need a giant detection pipeline for a host that exposes one admin service and one UDP tunnel. You need:
- sensible SSH policy
- a firewall you understand
- a log-based response that is easy to verify
That is why /blog/ssh-hardening-vpn-bastion comes before this article. If password auth is still on, or if every account can forward anything anywhere, no banning tool is going to save you from bad boundaries.
This framing also helps you avoid solving imaginary problems. Plenty of operators pile on "intrusion prevention" before they have even reduced the public admin surface to something reasonable.
Fail2Ban is enough more often than people admit
Upstream Fail2Ban ships conservative defaults in jail.conf:
bantime = 10mfindtime = 10mmaxretry = 5
It also ships with jails disabled by default, which is good. Operators should enable the jails that match the services that actually exist.
For a public gateway, Fail2Ban's strengths are exactly the boring ones:
- local
- understandable
- easy to audit
- no extra threat-intelligence story required
If your box is a simple SSH gateway, a journald-backed sshd jail is already a meaningful improvement:
# /etc/fail2ban/jail.d/sshd.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = nftables[type=allports]
[sshd]
enabled = true
backend = systemd
port = ssh
The backend = systemd part matters. Upstream notes that when you use the systemd backend, logpath is not the right mental model because Fail2Ban is reading the journal instead.
For the kind of host in /blog/self-hosted-wireguard-2026, that is often the right answer. Small surface, small tool.
That is not an anti-CrowdSec argument. It is an anti-overengineering argument.
What CrowdSec adds
CrowdSec is doing something broader. Its getting-started docs describe a pipeline where logs are parsed, normalized, evaluated through scenarios, and turned into decisions. That is a real detection engine, not just "regex plus ban."
It also has the collaborative layer that people either love or distrust: local detections can contribute to a shared blocklist, and you can also benefit from shared intelligence coming back.
That does not automatically make it better for every host. It makes it more capable.
CrowdSec becomes more attractive when:
- the box exposes multiple services
- you want shared detection logic across hosts
- you are already comfortable with a parser/scenario model
- you want richer response than "this IP failed SSH five times"
The firewall bouncer docs are also refreshingly practical. They support iptables, nftables, ipset, and pf, and they distinguish between:
managedmode, where the bouncer creates and manages rulesset-onlymode, where it only updates sets inside an existing policy
That second mode is important if the host already has carefully owned firewall policy.
The hub management docs also make clear that SSH-focused collections and scenarios are first-class citizens. That matters because you should not have to invent your own parser tree for a problem the project already understands well.
Where people get into trouble
The biggest operational mistake is not choosing the "wrong" tool. It is choosing two tools and letting both of them scribble over the same firewall layout.
Typical bad pattern:
- Fail2Ban inserts ban rules
- CrowdSec managed bouncer inserts its own rules
- Docker adds NAT and forwarding behavior
- nobody is quite sure which chain wins anymore
That is how you end up with a host that is technically protected and practically un-debuggable.
If two different tools are both "helpfully" inserting rules, then rule ordering and chain ownership become your real security problem.
Docker makes this worse. CrowdSec's own firewall-bouncer docs explicitly call out the DOCKER-USER chain for Docker-published services when you are enforcing in iptables mode. That is not a trivia note. That is the difference between "the ban applies" and "the traffic never passes through the place you think it does."
So keep the responsibilities explicit:
| Host type | Recommended path |
|---|---|
| Minimal SSH/WireGuard gateway | Fail2Ban only |
| Multi-service edge host | CrowdSec plus firewall bouncer |
| Complex firewall host | CrowdSec set-only, or Fail2Ban only, but not overlapping managed chains |
| "I want both" | Only with a clear chain-ownership plan |
A sane deployment pattern
If I were building from scratch, I would use one of these three patterns.
1. Minimal public gateway
Use Fail2Ban for sshd, keep the firewall small, and stop there.
Why? Because the exposed surface is small and the debugging path is short.
2. Multi-service edge box
Use CrowdSec, install the relevant collection, and add the firewall bouncer.
For example:
sudo apt install crowdsec crowdsec-firewall-bouncer-nftables
sudo cscli collections install crowdsecurity/sshd
sudo systemctl enable --now crowdsec crowdsec-firewall-bouncer
sudo cscli metrics show bouncers
That is already enough to make SSH protection a first-class path rather than a homebrew parser project.
3. Existing firewall layout you care about
Use CrowdSec in set-only mode so your hand-written policy remains the source of truth:
mode: nftables
nftables:
ipv4:
enabled: true
set-only: true
table: crowdsec
chain: crowdsec-chain
ipv6:
enabled: false
This is the adult option when the host already has a real firewall design.
And if the host does not have a real firewall design, adding more intrusion tooling is not going to conjure one into existence.
Verify that it is working
Do not install intrusion tooling and then trust vibes.
For Fail2Ban:
sudo fail2ban-client status sshd
For CrowdSec:
sudo cscli decisions list
sudo cscli metrics show bouncers
And for the actual enforcement plane:
sudo nft list ruleset
If the host is supposed to block something and you cannot point to the exact decision plus the exact firewall object enforcing it, you do not yet understand your deployment well enough.
That is the common theme here. Abuse prevention on public VPN servers should be boring and legible. Fancy tooling is fine when the host complexity justifies it. But on a minimal gateway, the fastest path to reliable protection is still usually the smaller one.