The moment a fresh node gets a public IP, the internet starts knocking. A researcher who exposed SSH on a standard Hetzner Cloud VPS in November 2025 logged more than 24,000 attacks in 15 days — and the first automated attack landed in fewer than 10 minutes. A September 2026 eServers report puts it bluntly: within minutes of provisioning, botnets begin aggressively scanning port 22, working through credential lists against root and admin accounts all day, every day. On a hosted PaaS, that background radiation is somebody else's problem. On machines you own, it is yours from the first boot.
TL;DR — the clean split: keep fail2ban on SSH, where per-box simplicity wins, and put CrowdSec on the HTTP layer, where crowd-sourced IP reputation and fleet-wide remediation win. On a single box, fail2ban alone is fine. On a fleet serving tenant apps, you want both, split by layer. The rest of this post is the evidence for that sentence.
fail2ban: the per-box bouncer that never phones home
fail2ban is the classic answer, and it earns that status. It tails your logs, matches failures against regular-expression filters, and after N failures in a time window it issues a ban — usually a firewall rule dropping the offender's IP for a while, then unbanning automatically. All of it happens on one box, with no network dependency, no account, and no telemetry. It is written in Python, it has been around since 2004, and its configuration model (jails per service, filters per log pattern) is understood by essentially every Linux operator alive.
That simplicity is also its ceiling. fail2ban sees only the logs on its own machine: an attacker scanning ten of your nodes looks like ten unrelated strangers, and banning one on node A does nothing on node B. There is no shared reputation, no central decision log, and each box carries its own jail configuration — which starts to smell on immutable Cluster API nodes, where per-machine snowflake config is exactly what declarative provisioning exists to eliminate.
One practical gotcha for modern fleets: fail2ban's 1.1.0 release still defaults its ban action to iptables. On an nftables-native node image, set banaction = nftables explicitly or your bans land in a backend the rest of your firewalling may not even be using.
CrowdSec: detect once, remediate everywhere
CrowdSec keeps fail2ban's core idea — parse logs, spot bad behavior, ban the IP — and rebuilds everything around it for fleets. The architecture splits into three parts. The Security Engine (written in Go) reads logs, runs them through parsers, and evaluates YAML scenarios: multi-line, multi-event patterns like "twenty HTTP 404s in five minutes plus a scanner user-agent," far richer than fail2ban's per-line regexes. Detections become decisions served by a Local API (LAPI). And enforcement is delegated to bouncers — separate components that consume decisions and act on them at the firewall (nftables/ipset), the reverse proxy, a CDN edge, or a Kubernetes ingress.
That split is the whole point. Detection and remediation no longer have to live on the same machine: one node can spot an attacker and every bouncer in the fleet can drop them. CrowdSec's own docs frame it exactly this way — detect on one server, remediate on one or more others — which is the capability a multi-machine platform needs and a per-box tool cannot offer.
The second half of the value is the Central API (CAPI): installations share anonymized attack signals upward, and CrowdSec curates them into a community blocklist every installation pulls down. Your node starts dropping known-bad IPs it has never seen before, because thousands of other nodes already have. Enrollment is free; the hosted Console dashboard at app.crowdsec.net is the optional window into it.
The vendor claims the Go engine processes logs 60x faster than fail2ban — treat that as a vendor number, but the direction is unsurprising given fail2ban's single-threaded Python log tailing. The HTTP-inspection side is under active development, with bot detection landing in the 1.8.0 release in late August 2026.
Head to head
| fail2ban | CrowdSec | |
|---|---|---|
| Detection | Per-line regex on local logs | Parsers + multi-event YAML scenarios |
| Threat sharing | None — each box learns alone | Community blocklist via CAPI; local signals shared upward |
| Remediation points | Local firewall (iptables/nftables) | Bouncers: firewall, reverse proxy, K8s ingress, CDN/WAF, and more |
| Multi-machine story | None built in | Detect on one node, enforce on all bouncers via LAPI |
| Kubernetes story | Sidecar-per-node hacks; no native concept | Official Helm chart, ingress bouncers, firewall-bouncer DaemonSet |
| Language / footprint | Python, tiny, single box | Go engine + LAPI + bouncers; more moving parts |
| Offline behavior | Fully offline, no accounts | Core works offline; blocklist needs CAPI connectivity |
| Cost | Free, forever | Free core + community blocklist; curated premium lists paid |
Read the table as a layer map, not a winner's podium. fail2ban wins every row where "simple and local" is the requirement. CrowdSec wins every row where "shared and fleet-wide" is. That is why the community converged on running both: fail2ban stays on SSH while CrowdSec takes the web layer, as one widely-shared self-hosting guide puts it — one bouncer protects all apps at once, the scenarios stay maintained, and the community blocklist comes free.
Where each one sits on a Cluster API fleet
Concretely, here is the placement for a small CAPI-managed fleet on owned hardware:
SSH: fail2ban baked into the node image, or CrowdSec at the firewall. sshd brute-forcing is the textbook fail2ban jail, and baking one static jail config into the machine image sidesteps the immutable-node objection — the config is identical on every node because it ships with the image. The alternative is CrowdSec's firewall bouncer as a DaemonSet enforcing LAPI decisions in kernel nftables on every node, which buys you fleet-wide SSH attacker sharing at the cost of running the LAPI somewhere highly available. Either is defensible; what is not defensible is hand-configured per-node SSH jails drifting across the fleet.
HTTP: CrowdSec at the ingress. This is where CrowdSec earns its complexity. A Traefik or NGINX ingress bouncer consults CrowdSec decisions per request, so a scanner fingerprinted against one tenant's app is dropped at the edge before touching any tenant's app — plus every IP the global community already flagged. The official docs support the NGINX ingress path directly; note that the Traefik Kubernetes ingress integration is third-party-maintained, so pin it and watch its release notes rather than assuming first-party support velocity. A typical setup registers bouncer keys with the LAPI and points the middleware at it:
kubectl exec -n crowdsec deploy/crowdsec-lapi -- cscli bouncers add traefik-ingress
kubectl create secret generic crowdsec-keys --from-literal=traefik-key='<key>' -n crowdsecThe detect-here-ban-everywhere flow is what makes this a fleet control rather than a box control: the engine sees the pattern in one ingress log stream, the decision propagates to every bouncer, and the attacker's next probe — against any node, any tenant — dies at the edge.
The honest costs: false positives, shared IPs, and trust
Crowd-sourced blocking has a failure mode per-box tools do not: somebody else's false positive becomes your outage. There is a documented case of Facebook's crawler being blocked through the community blocklist, silently breaking link-preview image fetches until the operator traced it. CrowdSec says it curates lists with reporter trust scores and cross-checking, and default ban durations are short (around four hours), which bounds the blast radius — but when your tenants' users sit behind carrier-grade NAT or corporate VPN egress IPs, one bad reputation entry can lock out a whole office of legitimate users. If you serve tenant traffic, run the community list in a monitored mode first, keep an allowlist path for reported blocks, and think twice before stacking aggressive third-party feeds on top.
Then there is the trust question. fail2ban phones home to nobody; CrowdSec's full value needs CAPI connectivity and, for the dashboard experience, an account on someone else's console. For a platform whose pitch is owning the whole stack, that is a real — if narrow — external dependency to accept deliberately. And the free tier draws a line: the community blocklist is free, but the curated premium feeds are paid, so price the lists you actually want before promising tenants "enterprise threat intel."
Resource-wise, be honest about the moving parts: an engine, a local API with its database, and a bouncer per enforcement point is genuinely more to operate than a single Python daemon. On one box that is overkill. Across a fleet, it is the cost of having one shared immune system instead of N amnesiac ones.
Verdict: the clean split
The decision rule fits in three lines. One box, SSH only: fail2ban, with banaction = nftables, and move on. A fleet with tenant HTTP traffic: add CrowdSec at the ingress for shared reputation and fleet-wide remediation, and keep fail2ban (or the firewall bouncer) on sshd. The "versus" in the title is really a division of labor — per-box simplicity where the threat is local and dumb, crowd-sourced reputation where it is distributed and coordinated.
That division mirrors how self-hosting matures in general: the first server gets hand-tended tools, and the fleet gets shared control planes. The scanner traffic never stops — the only question is whether each of your nodes learns about it separately, or once, together.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



