RouteHardenHire us
← Back to blog
Network Hardening··6 min read

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 = 10m
  • findtime = 10m
  • maxretry = 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:

  • managed mode, where the bouncer creates and manages rules
  • set-only mode, 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 typeRecommended path
Minimal SSH/WireGuard gatewayFail2Ban only
Multi-service edge hostCrowdSec plus firewall bouncer
Complex firewall hostCrowdSec 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.