Head to head
CrowdSec vs Fail2ban
One tool tails a log and writes an iptables rule. The other splits detection from enforcement and trades a described set of fields for a community blocklist. Which you want depends on what is actually reachable from the internet.
CrowdSec or Fail2ban?
Use CrowdSec on a public web server and Fail2ban on a single box where SSH is the only exposed service. CrowdSec gives you maintained detection scenarios from a hub and a community blocklist, at the cost of two processes (an engine that decides and a bouncer that enforces) and a specific set of fields sent to its Central API. Fail2ban is one Python daemon, around 40 MB of RAM, nothing leaving the machine, and patterns you maintain yourself. If your server has no ports forwarded and you reach it over WireGuard, neither one is doing meaningful work.
The choice is not which tool detects brute force better. Both read log lines, both match patterns, both end up producing a temporary ban. The choice is whether you want a single daemon whose rules you write and maintain on your own box, or a system where detection and enforcement are deliberately separate processes and the detection half is fed by patterns other people maintain and by a blocklist built from other people's incidents.
Fail2ban is the first shape. One Python daemon, one config directory, one SQLite file, nothing on the network. Since 2004. CrowdSec is the second: a Go engine that parses logs, matches scenarios pulled from a hub, and writes decisions into a local database behind an API, plus at least one bouncer that reads that API and actually blocks. Since 2020.
Everything that follows, including the argument about privacy that dominates every forum thread on this, comes out of that structural difference.
What the spec table is telling you#
Watch two rows. Container count is 2 against 0: CrowdSec is an engine plus a bouncer, Fail2ban is a host package with no service to publish. Idle RAM is 120 MB against 40 MB, which on any machine worth defending is not the deciding factor, though it matters on a 512 MB VPS. Both score 2 on ops load, for opposite reasons: Fail2ban because your regexes rot silently, CrowdSec because its two halves can drift apart and nothing tells you.
| Specification | CrowdSec | Fail2ban |
|---|---|---|
| Licence | MIT (Permissive) | GPL-2.0-or-later (Copyleft) |
| Written in | Go | Python |
| First release | 2020 | 2004 |
| Maturity | Stable | Mature |
| Datastore | SQLite by default (MySQL and PostgreSQL supported) | SQLite at `/var/lib/fail2ban/fail2ban.sqlite3` |
| Services to run | 2 | not container shaped |
| Idle memory | 120 MB | 40 MB |
| Memory in use | 250 MB | 80 MB |
| Operational load | 2 / 5, Light | 2 / 5, Light |
| Identity | Not applicable | Not applicable |
| arm64 builds | Yes | Yes |
| Default ports | 8080, 6060 | none |
| Backup shape | SQLite backup | SQLite backup |
Exactly what CrowdSec sends off the box#
This is the real objection, so here is the specific answer instead of reassurance.
A signal pushed to the Central API contains: the offending IP address with geolocation, the name of the scenario that fired plus its version or hash, the timestamp of the decision, your machine ID, and the list of scenarios you have enabled. That last one is a mild fingerprint of what you run. It does not contain your log lines, your URLs, your usernames or your request bodies, because share_context is off.
The shipped console.yaml defaults are:
share_manual_decisions: false
share_custom: true
share_tainted: true
share_context: falseManual bans you type yourself are not shared. Custom and tainted scenario results are shared by default, though only unmodified hub scenarios are pushed as signals, so a scenario you have edited stops feeding the community. If any of that is unacceptable in your environment, change it before you enroll, not after.
You can also opt out completely: delete the credentials in /etc/crowdsec/online_api_credentials.yaml and the engine never contacts the Central API. Local parsing, hub scenarios already on disk, decisions and bouncers all keep working. What you lose is the community blocklist, which is the main thing CrowdSec offers over a well-tuned Fail2ban. Offline CrowdSec is a legitimate configuration, but be honest that you have chosen a heavier Fail2ban with better-maintained rules.
The mistake almost everyone makes with CrowdSec#
The engine blocks nothing. It parses, it decides, it stores. Enforcement lives in a bouncer, which is a different package with its own API key generated by cscli bouncers add <name> and printed exactly once. People install the engine, see alerts accumulate, and believe they are protected. cscli decisions list can be full of bans that no packet has ever encountered.
Test end to end, on install and again after every upgrade:
cscli decisions add --ip 1.2.3.4 --duration 1m
sudo nft list ruleset | grep 1.2.3.4 # firewall bouncer: expect a match
curl -sI -H 'X-Forwarded-For: 1.2.3.4' https://your.site/ # L7 bouncer: expect 403If neither shows anything, you have a detection system and no defense. A rotated or lost API key fails exactly this quietly, which is why the check belongs in your documented upgrade routine rather than in your memory.
Fail2ban's defaults are close to useless#
Out of the box: bantime = 10m, findtime = 10m, maxretry = 5. Five failures inside ten minutes buys a ten minute ban. A botnet cycling thousands of source addresses does not notice, and you get the feeling of protection without much of it.
Minimum viable /etc/fail2ban/jail.local:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.1.0/24
bantime = 1w
findtime = 1h
maxretry = 3
backend = systemd
banaction = nftables-multiport
[sshd]
enabled = true
[recidive]
enabled = trueThree things in that file are load bearing. Never edit jail.conf: the package replaces it on upgrade and your changes vanish months later without a word. backend = systemd is mandatory on distributions that no longer write /var/log/auth.log, and getting it wrong is the single most common reason a fresh install detects nothing while reporting itself healthy. And banaction defaults to iptables-multiport, which is wrong on nftables-only systems.
Confirm detection with a log file rather than by locking yourself out:
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
fail2ban-client status sshdContainers break Fail2ban twice#
If your services run in containers and log to stdout, there is no file to tail without extra plumbing. And even when you solve that, published container ports are diverted in the nat table before reaching the INPUT chain where iptables-multiport writes bans. Fail2ban reports the ban, the traffic arrives anyway. The fix is an action targeting the chain Docker leaves free for you, for example action = iptables-allports[chain=DOCKER-USER], or moving enforcement in front of Docker entirely.
CrowdSec handles this better by design. It reads container logs directly:
source: docker
container_name:
- my-nginx
labels:
type: nginxAnd its layer 7 bouncers for Traefik and Caddy enforce at the proxy, above the whole iptables problem. If you already run one of those, that is the strongest single argument for CrowdSec. Caddy vs Traefik covers picking between them; Dozzle is worth having alongside for reading what the containers actually logged.
Both tools will ban your reverse proxy#
If your web server logs the proxy's address instead of the client's, every scenario and every jail fires against that one address and blackholes your entire front end. It looks like a proxy failure and it is a total outage. Fix real-IP logging first (see Reverse proxy and TLS), then whitelist the proxy anyway.
The uncomfortable truth: on a closed server, neither one does anything#
Say your box has no ports forwarded. You reach it over WireGuard or a Tailscale-style overlay. Services sit behind Nginx Proxy Manager or Traefik on a LAN-only listener.
In that setup, CrowdSec and Fail2ban are both ceremony. There is no unauthenticated internet traffic reaching a log they can read. The alerts you do see will be your own devices, your own monitoring, and the occasional LAN scan. Installing either buys you a dashboard, some CPU cost, and a new way to lock yourself out.
What is actually doing the work:
- Not exposing the port. An unreachable service cannot be brute forced. Remote access without port forwarding is the whole answer, and Headscale or wg-easy are the two usual implementations. Headscale vs wg-easy picks between them.
- Key-only SSH.
PasswordAuthentication noplusPubkeyAuthentication yesmakes password guessing arithmetically pointless. A banning tool then only trims log noise. - A reverse proxy that publishes only what you meant to publish. Most accidental exposure is a container binding
0.0.0.0rather than an attacker being clever. - Patching. Nothing in either tool stops an authenticated exploit against an out of date application. An update strategy that does not lose data matters more than either of these.
- Auth in front of anything public. Authelia or an equivalent forward-auth portal removes the login form from the internet entirely. See Single sign-on for self-hosters.
That whole list is A security baseline for a home server, and it outranks this comparison. Run one of these tools if you publish something; do not run one as a substitute for the list.
Is Fail2ban still maintained?#
1.1.0 shipped April 2024. 1.1.1 shipped August 2026. Two years and four months between point releases on a project that tails logs and shells out to a firewall command reads as finished rather than abandoned. It still ships in every distribution, still works, and still needs nothing from the network.
The maintenance risk is not the daemon, it is your filters. When an upstream service changes its log format, a jail stops matching and reports zero failures forever without complaint. Nobody alerts you. That is the failure mode CrowdSec's hub exists to remove, and it is a fair pitch. Pair either tool with something that notices absence, such as an Uptime Kuma push check that fires if your ban counts flatline.
Which one for your situation#
| Your situation | Use | Why |
|---|---|---|
| Single VPS, SSH exposed, little else | Fail2ban | One package, no network dependency, bantime = 1w and done |
| Public web server with real traffic | CrowdSec | The community blocklist drops known scanners before your app sees them |
| Everything behind WireGuard, no ports forwarded | Neither | Nothing reachable to attack; spend the time on patching and key-only SSH |
| Containerized services logging to stdout | CrowdSec | Native Docker log acquisition; Fail2ban has no file and the wrong chain |
| You run Traefik or Caddy | CrowdSec | Layer 7 bouncer plugin, no iptables or nftables involvement at all |
Working jail.local files already in place | Fail2ban | Migration cost is real, the gain is incremental, leave it alone |
| Compliance forbids sending observed addresses off-box | Fail2ban | Or CrowdSec with online credentials removed, minus the blocklist |
| You want a curated ruleset you do not maintain | CrowdSec | cscli collections install crowdsecurity/nginx replaces an afternoon of regex work |
What to do next#
If you publish anything to the internet, pick by enforcement point. Proxy at layer 7 with Traefik or Caddy, use CrowdSec and install the matching bouncer the same hour you install the engine, then run the cscli decisions add test above before you believe it works. Firewall enforcement on a plain VPS with SSH exposed, use Fail2ban, write jail.local with bantime = 1w and maxretry = 3, set backend = systemd, and verify with fail2ban-regex rather than by failing logins.
If you are not sure your server is exposed at all, find that out first. Run A security baseline for a home server end to end, close what does not need to be open, and read Remote access without port forwarding. The rest of the Security category covers what to do once the ports are closed and a banning tool has stopped being the interesting problem.
Questions#
What data does CrowdSec send to its Central API?
A signal carries the offending IP address with geolocation, the name and version or hash of the scenario that fired, the timestamp of the decision, your machine ID, and the list of scenarios you have enabled. Only unmodified hub scenarios are pushed. console.yaml ships with share_manual_decisions: false, share_custom: true, share_tainted: true and share_context: false, so request bodies and matched log content stay local unless you change that. Set it before enrollment, not after.
Does CrowdSec block anything on its own?
No. The Security Engine parses logs and writes decisions into a SQLite database behind a Local API on 127.0.0.1:8080. Blocking is done by a bouncer, which is a separately installed and separately credentialed package: firewall, nginx, OpenResty, Traefik, HAProxy, Cloudflare or Apache. Installing the engine and watching cscli alerts list scroll is not protection. Verify with cscli decisions add --ip 1.2.3.4 --duration 1m and then check that a firewall rule appeared or a 403 came back.
Can I run CrowdSec fully offline?
Yes. Remove the online API credentials from /etc/crowdsec/online_api_credentials.yaml and the engine stops talking to the Central API entirely. You keep local parsing, hub scenarios you have already downloaded, decisions and bouncers. You lose the community blocklist, which is most of the reason to choose CrowdSec over Fail2ban in the first place. If that trade is the point, compare it honestly against Fail2ban rather than treating offline CrowdSec as a free upgrade.
Is Fail2ban abandoned?
No, it is slow. Version 1.1.0 shipped in April 2024 and 1.1.1 did not arrive until August 2026. For a project whose job is tailing a log file and calling iptables, a two year gap between point releases reads as finished rather than dead. The risk is not security bugs in the daemon, it is filter regexes drifting behind upstream log format changes. Run fail2ban-regex against a real log after any major service upgrade.
Why is Fail2ban banning addresses but attacks keep getting through?
Almost always Docker. Traffic to a published container port is diverted in the nat table before it reaches the INPUT chain, which is where iptables-multiport writes its bans. The jail counts failures, the ban appears in fail2ban-client status, and the packets arrive anyway. Fix it with an action that targets the DOCKER-USER chain, which Docker leaves for exactly this, or enforce in front of Docker at the reverse proxy.
Do I need either one if I use key-only SSH?
Not for SSH itself. With PasswordAuthentication no and PubkeyAuthentication yes, brute force against your account is arithmetically impossible, and banning the attempts only reduces log noise and CPU. The case for a banning tool is everything else you publish: web application login forms, mail, and unauthenticated endpoints that get scanned. If you publish none of those, spend the effort on not exposing ports and on patching.
Sources#
- CrowdSec default config.yaml: LAPI on 127.0.0.1:8080, Prometheus 6060, SQLite path
- CrowdSec console.yaml sharing defaults
- CrowdSec Central API documentation, what is sent and received
- CrowdSec remediation components (bouncers) overview
- CrowdSec v1.8.0 release notes
- CrowdSec pricing and commercial tiers
- Fail2ban releases, 1.1.0 and 1.1.1 dates
- Fail2ban shipped jail.conf: bantime, findtime, maxretry, backend defaults
- Fail2ban shipped fail2ban.conf: socket path, dbfile, dbpurgeage
- Docker packet filtering and firewalls: container traffic and the iptables chains
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.