Ich habe lange genug mit fail2ban gearbeitet, um zu wissen, wo es solide ist und wo es mich im Alltag ausbremst. Für einzelne Hosts mit klaren Logquellen funktioniert es. Aber je mehr Systeme ich betreut habe, desto deutlicher wurde das Muster: SSH-Bruteforce auf mehreren Maschinen, Bot-Traffic auf nginx, Login-Versuche auf Admin-Pfade und das Ganze immer wieder pro Server isoliert. Jeder Host reagiert lokal, jeder Host lernt nur aus seinen eigenen Logs, und ich darf dieselben Regeln an mehreren Stellen pflegen. Genau an dem Punkt bin ich bei CrowdSec gelandet.
Der Umstieg war für mich kein Selbstzweck. Ich wollte drei Dinge: Erkennung und Reaktion sauber trennen, Entscheidungen serverübergreifend nutzen und weniger Zeit damit verbringen, lokale Jail-Logik auf jedem System separat zu debuggen. CrowdSec hat das in meiner Praxis besser abgebildet als fail2ban. Nicht, weil es magisch mehr sieht, sondern weil das Modell sauberer ist: Der Agent liest Logs, Parser und Szenarien erzeugen Signale, daraus entstehen Alerts und Decisions, und Bouncer setzen diese Decisions technisch durch. Das ist ein anderer Zuschnitt als bei fail2ban, wo Erkennung und Reaktion enger gekoppelt sind.
Der Auslöser
Der eigentliche Schmerz war nicht ein einzelner Angriff, sondern die Summe aus Kleinkram. SSH auf mehreren Hosts, Webserver mit ständigem Bot-Lärm, Scanner auf Standardpfaden, Login-Versuche auf Anwendungen hinter nginx und dazu Container-Setups, bei denen die echte Client-IP nicht immer sauber im Log landet. fail2ban hat das lokal abgefangen, aber eben nur lokal und nur dann, wenn genau dieser Host genug Signale gesehen hat.
Was mich daran zunehmend gestört hat: Ein Angreifer konnte auf Host A auffallen und auf Host B erst einmal weitermachen, bis dort ebenfalls genug Events zusammenkamen. Dazu kam der Pflegeaufwand. Unterschiedliche Jails, unterschiedliche Logpfade, Ausnahmen für interne Netze, Sonderfälle für Reverse Proxies. Nichts davon ist unlösbar, aber mit wachsender Infrastruktur wird es unnötig zäh.
CrowdSec war für mich deshalb interessant, weil es den lokalen Blick aufbricht. Die Erkennung passiert weiter auf Basis meiner Logs, aber Decisions lassen sich zentral verteilen. Und über die Community- beziehungsweise Console-Anbindung kommt noch ein externer Signalstrom dazu. Das ersetzt keine saubere lokale Konfiguration, aber es ergänzt sie sinnvoll.
Was CrowdSec anders macht
Kurzfassung für Admins: CrowdSec besteht aus mehreren Bausteinen. Der Agent liest Logquellen aus der acquis.yaml. Parser normalisieren die Events, Szenarien bewerten Muster wie Bruteforce oder Scans. Daraus entstehen Alerts. Eine Local API, meist kurz LAPI genannt, verwaltet diese Ergebnisse und stellt Decisions bereit. Bouncer holen sich diese Decisions und setzen sie durch, etwa in nftables, iptables oder vor nginx.
Der Unterschied zu fail2ban ist für mich vor allem architektonisch. fail2ban beobachtet Logs und bannt lokal. CrowdSec trennt Erkennung und Durchsetzung. Dadurch kann ich dieselbe Erkennung auf mehreren Maschinen nutzen und die Reaktion passend zum Dienst wählen. Für SSH und generischen Netzwerkverkehr ist ein Firewall-Bouncer naheliegend. Für Web-Traffic kann ein nginx-Bouncer sinnvoll sein, wenn ich näher an HTTP reagieren will.
Wichtig ist aber auch die Kehrseite: CrowdSec ist mächtiger, aber nicht automatisch simpler. Man muss die Begriffe sauber einordnen, Logquellen korrekt anbinden und gerade bei Reverse-Proxy-Setups darauf achten, dass die echte Client-IP in den Logs landet. Sonst blockt man am Ende den Proxy statt des Clients oder produziert False Positives.
Meine Ausgangslage
Mein Setup ist kein exotisches Spezialkonstrukt, aber auch kein einzelner Root-Server. Ich betreibe mehrere Linux-Server auf Ubuntu, darauf laufen nginx, SSH und je nach Host Container-Workloads. Ein Teil der Webdienste hängt hinter Reverse Proxies, einige Anwendungen laufen in Docker-Setups. Genau diese Mischung ist relevant, weil CrowdSec nur so gut arbeitet wie die Logdaten, die ich ihm gebe.
Schützen wollte ich vor allem drei Flächen: SSH, öffentlich erreichbare Webdienste hinter nginx und typische Bot- beziehungsweise Scan-Muster auf HTTP-Ebene. Mailserver oder andere Spezialdienste würde ich nur dann anbinden, wenn die Logqualität passt und ich die Szenarien bewusst auswähle. Ich bin kein Fan davon, blind jede verfügbare Collection zu installieren und dann zu hoffen, dass schon alles gutgeht.
Installation und Grundkonfiguration
Ich habe mit einem einzelnen Host angefangen, bevor ich die zentrale Verteilung aufgebaut habe. Das war wichtig, um Parser, Szenarien und Bouncer erst lokal zu verstehen. Auf Ubuntu war die Installation unspektakulär, aber versionsabhängig. Deshalb prüfe ich grundsätzlich zuerst, welche Pakete und Bouncer für die eingesetzte Distribution und CrowdSec-Version offiziell verfügbar sind.
Nach der Basisinstallation habe ich zuerst den Agenten und die LAPI auf dem ersten Host betrieben. Danach kamen die Collections aus dem Hub dazu. Für meinen Fall waren SSH und nginx die ersten Kandidaten.
sudo cscli collections install crowdsecurity/sshdsudo cscli collections install crowdsecurity/nginxDanach prüfe ich immer, was tatsächlich installiert ist und ob die Szenarien geladen wurden.
sudo cscli collections listsudo cscli scenarios listDer entscheidende Teil ist dann die /etc/crowdsec/acquis.yaml. Dort definiere ich explizit, welche Logs eingelesen werden. Für SSH und nginx sah das bei mir zunächst bewusst schlicht aus:
/etc/crowdsec/acquis.yaml
filenames:
- /var/log/auth.log
labels:
type: syslog
---
filenames:
- /var/log/nginx/access.log
- /var/log/nginx/error.log
labels:
type: nginxWichtig: Die konkreten Pfade hängen von Distribution, Logrotate-Setup und Containerisierung ab. Wer nginx-Logs in ein anderes Verzeichnis schreibt oder über journald arbeitet, muss das sauber anpassen. Ich habe an der Stelle bewusst nicht geraten, sondern die tatsächlich vorhandenen Pfade auf jedem Host geprüft.
Nach Änderungen an der Log-Akquise starte ich den Dienst neu und prüfe die Konfiguration.
sudo systemctl restart crowdsecsudo cscli metricscscli metrics ist für mich eines der nützlichsten Werkzeuge in der Einführungsphase. Dort sehe ich schnell, ob Dateien gelesen werden, Parser feuern und Szenarien überhaupt Input bekommen. Wenn dort nichts ankommt, ist fast nie CrowdSec das eigentliche Problem, sondern die Logquelle, das Format oder die falsche Typ-Zuordnung in der acquis.yaml.
Reaktion: Bouncer einrichten
Erkennung allein bringt nichts, wenn Decisions nicht umgesetzt werden. Ich habe deshalb früh einen Firewall-Bouncer eingerichtet. Für mich war das die robusteste Standardreaktion, weil sie unabhängig vom einzelnen Dienst greift. Ob man nftables oder iptables nimmt, hängt von der eigenen Plattform ab. Auf aktuellen Distributionen ist nftables oft die sauberere Wahl. Wer noch klar auf iptables setzt oder bestehende Automatisierung darum gebaut hat, kann dabei bleiben. Entscheidend ist, dass der Bouncer zur realen Paketfilter-Implementierung des Hosts passt.
Konkret habe ich auf Ubuntu 24.04 die nftables-Variante des Firewall-Bouncers installiert:
sudo apt install crowdsec-firewall-bouncer-nftablesDas Paket registriert sich beim Setup automatisch an der lokalen LAPI und legt seinen API-Key unter /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml ab. Ich prüfe danach, ob der Bouncer angedockt hat und Decisions zieht:
sudo cscli bouncers listDen wichtigsten Test mache ich immer einmal von Hand: eine IP künstlich sperren und nachsehen, ob sie wirklich im Kernel landet.
sudo cscli decisions add --ip 203.0.113.55 --duration 5m --reason "test"
sudo nft list ruleset | grep -i crowdsec
sudo cscli decisions delete --ip 203.0.113.55Erst als diese Test-IP im crowdsec-blacklists-Set von nftables auftauchte, war für mich klar: Aus „CrowdSec läuft" ist „CrowdSec schützt" geworden. Ohne Bouncer erkennt der Agent zwar fleißig und schreibt Decisions, aber technisch gesperrt wird nichts. Das ist der Punkt, an dem ich anfangs zuerst gestolpert bin.
Zusätzlich kann ein nginx-Bouncer sinnvoll sein, wenn man Entscheidungen direkt auf HTTP-Ebene durchsetzen will. Ich sehe ihn eher als Ergänzung, nicht als Pflicht. Für SSH und generischen Scan-Traffic war bei mir der Firewall-Bouncer der erste Hebel.
Die eigentliche Ban-Logik wird über Profiles gesteuert. Dort lässt sich definieren, welche Alerts zu welchen Decisions führen. Ich habe die Defaults nicht blind übernommen, sondern vor allem auf Ausnahmen und Dauer geschaut. Ein vereinfachter Ausschnitt aus /etc/crowdsec/profiles.yaml sieht so aus:
/etc/crowdsec/profiles.yaml
name: default_ip_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
on_success: breakDie konkrete Syntax und vorhandene Default-Profile können je nach Version variieren. Deshalb fasse ich diese Datei nur an, wenn ich genau weiß, warum. Mein wichtigster Eingriff war am Anfang nicht die Ban-Dauer, sondern Whitelisting und das Vermeiden eigener Aussperrungen.
Mehrere Server zentral
Der eigentliche Mehrwert kam bei mir erst mit einer zentralen LAPI. Ich wollte nicht auf jedem Host isolierte Decisions haben, sondern Signale teilen. Dafür habe ich einen zentralen CrowdSec-Knoten betrieben und weitere Maschinen als Clients beziehungsweise Maschinen an diese LAPI angebunden.
Auf dem zentralen Knoten registriert man zusätzliche Maschinen und verteilt die notwendigen Zugangsdaten. Die genauen Schritte hängen etwas vom Setup und der Version ab, aber der Kern ist immer gleich: Die Maschinen authentifizieren sich gegenüber der LAPI, senden ihre Signale dorthin und konsumieren Decisions von dort.
Konkret mache ich am LAPI-Host zwei Dinge: die Local API im Netz erreichbar machen, denn standardmäßig lauscht sie nur auf localhost, und für jeden Agent eine Maschine registrieren.
# /etc/crowdsec/config.yaml am LAPI-Host:
# api.server.listen_uri: 0.0.0.0:8080 (statt 127.0.0.1:8080)
sudo systemctl restart crowdsec
sudo cscli machines add agent-host6 --auto # gibt Login + Passwort ausDie ausgegebenen Zugangsdaten trage ich zusammen mit der URL der zentralen LAPI am jeweiligen Agent ein:
/etc/crowdsec/local_api_credentials.yaml am Agent
url: https://lapi.intern.example:8080
login: agent-host6
password: <generiertes-passwort>Nach einem Neustart des Agents muss er auf dem zentralen Knoten in der Maschinenliste auftauchen:
sudo systemctl restart crowdsec
sudo cscli machines list # am LAPI-Host müssen jetzt alle Agents auftauchenDie Firewall-Bouncer der einzelnen Hosts zeigen über ihre api_url ebenfalls auf die zentrale LAPI. So arbeitet jeder Host aus demselben gemeinsamen Decision-Pool: Eine IP, die auf einem Web-Host auffällt, ist Sekunden später auch auf den anderen angebundenen Maschinen gesperrt.
In meinem Setup war das der Punkt, an dem CrowdSec seinen Aufwand rechtfertigt. Ein auffälliger Client, der auf einem Web-Host negativ auffällt, landet dadurch nicht nur lokal in einer Decision, sondern kann auch auf anderen angebundenen Maschinen geblockt werden. Genau das hat mir bei fail2ban gefehlt.
Worauf ich geachtet habe: Die zentrale LAPI ist ein kritischer Baustein. Wenn man sie zentralisiert, sollte man Monitoring, Backups der Konfiguration und saubere Erreichbarkeit mitdenken. Den LAPI-Port gebe ich per Firewall nur für die eigenen Server frei und setze TLS davor. Fällt die LAPI aus, sperren die Bouncer mit ihrem letzten Stand weiter, neue Entscheidungen kommen aber erst nach der Rückkehr. Für kleine Umgebungen kann auch ein lokales Setup pro Host ausreichend sein. Zentralisierung lohnt sich vor allem dann, wenn man wirklich mehrere exponierte Systeme mit ähnlichen Angriffsflächen betreibt.
Console und Community-Blocklists
Die Anbindung an die CrowdSec Console habe ich nicht als Erstes gemacht, sondern erst nachdem die lokale Erkennung sauber lief. Das war Absicht. Ich wollte erst verstehen, was meine eigenen Logs liefern, bevor ich externe Signale dazunehme. Für das Enrollment nutzt man den Befehl mit dem Enrollment-Key, den die Console ausgibt:
sudo cscli console enroll <DEIN-ENROLLMENT-KEY>Danach kann man Signal-Sharing und kuratierte beziehungsweise Community-Blocklists nutzen, je nach Setup und Freigaben. Ich sehe das als Ergänzung, nicht als Ersatz für lokale Szenarien. Externe Blocklisten können früh filtern, aber sie sind nie so präzise für die eigene Umgebung wie sauber ausgewertete lokale Logs.
Außerdem gilt hier derselbe Grundsatz wie überall in der Security: Mehr Signale sind nicht automatisch besser, wenn man ihre Wirkung nicht beobachtet. Ich habe deshalb nach der Aktivierung bewusst geprüft, welche Decisions hereinkommen und ob es Seiteneffekte gibt.
Was es gebracht hat
Ich erfinde an der Stelle keine Zahlen. Was ich tatsächlich ausgewertet habe, lief über die Standardwerkzeuge von CrowdSec. Für den Alltag sind vor allem diese drei Kommandos relevant:
sudo cscli metricssudo cscli decisions listsudo cscli alerts listÜber diese Kommandos zeigt sich bei mir über die Zeit ein wiederkehrendes Bild: Der Großteil der Decisions entfällt auf SSH-Bruteforce und auf HTTP-Scanner, die Standardpfade und bekannte Lücken abklopfen. Konkrete Zahlen sind dabei weniger aussagekräftig, als man denkt, weil sie stark von Exposition, Zeitraum und Szenario-Auswahl abhängen. Interessanter ist die Struktur dahinter: Über decisions list sehe ich, dass dieselben Quellen auf mehreren Hosts auftauchen, und genau dieses geteilte Lagebild hatte ich mit rein lokalem fail2ban vorher nicht.
Subjektiv war der größte Unterschied nicht, dass plötzlich alles ruhig wurde, sondern dass die Reaktion konsistenter wurde. Wiederkehrende Quellen wurden schneller und hostübergreifend abgefangen. Ich musste weniger pro Maschine nachziehen. Und ich hatte mit alerts und decisions eine klarere Trennung zwischen Erkennung und Durchsetzung als vorher.
Stolpersteine und Lektionen
Eigene Netze whitelisten
Das ist der Klassiker und ich erwähne ihn bewusst deutlich: Wer seine eigene feste IP, das Heimnetz, VPN-Egress oder Admin-Jump-Hosts nicht sauber berücksichtigt, sperrt sich früher oder später selbst aus. Das gilt besonders bei aggressiveren Szenarien auf SSH oder bei ungewöhnlichen Login-Mustern während Wartungsfenstern.
Konkret löse ich das mit einer eigenen Whitelist in der Parser-Stufe s02-enrich, also bevor überhaupt eine Entscheidung entsteht:
/etc/crowdsec/parsers/s02-enrich/mywhitelists.yaml
name: crowdsecurity/whitelists
description: "Eigene IPs/Netze nie bannen"
whitelist:
reason: "Admin-Zugänge, Heimnetz, VPN"
ip:
- "203.0.113.10"
cidr:
- "192.168.178.0/24"
- "10.0.0.0/8"sudo systemctl reload crowdsecWichtig ist, hier die echten öffentlichen IPs einzutragen, nicht die Adressen hinter einem Proxy. Aktuelle CrowdSec-Versionen bieten dafür zusätzlich native Allowlists über cscli allowlists, die zentral über die LAPI auf alle angebundenen Server wirken. Die Parser-Variante oben funktioniert versionsübergreifend.
False Positives sauber prüfen
Nicht jeder Alert ist automatisch ein sinnvoller Ban. Gerade bei Webanwendungen mit speziellen Pfaden, Healthchecks oder internen Scannern lohnt es sich, die Alerts durchzugehen und notfalls Szenarien, Ausnahmen oder Logquellen anzupassen. Ich habe mir angewöhnt, nicht sofort an der Ban-Dauer zu drehen, sondern zuerst zu prüfen, warum ein Event überhaupt als verdächtig klassifiziert wurde.
Echte Client-IP hinter Reverse Proxy
Das war in meiner Umgebung einer der wichtigsten Punkte. Wenn nginx oder ein vorgeschalteter Proxy nicht die echte Client-IP ins Access-Log schreibt, arbeitet CrowdSec mit der falschen Quelle. Dann blockt der Bouncer im schlimmsten Fall den Proxy oder ein internes Netzsegment. Wer Docker, Load Balancer oder Reverse Proxies nutzt, sollte die Logformate und Header-Verarbeitung zuerst verifizieren, bevor er aus den Daten Decisions ableitet.
Container und Logpfade
In Docker- oder Plattform-Setups wie Coolify ist die Frage nicht nur, welche Dienste laufen, sondern wo deren Logs tatsächlich landen. Ich habe bewusst nicht versucht, jeden Container direkt anzubinden, sondern mich zuerst auf stabile Host-Logs konzentriert: SSH und nginx. Das bringt schnell Nutzen und reduziert Fehlersuche.
Collections nicht blind anhäufen
Der Hub ist praktisch, aber mehr installierte Collections bedeuten nicht automatisch mehr Sicherheit. Jede zusätzliche Collection erweitert die Angriffsoberfläche der eigenen Fehlkonfiguration. Ich installiere nur das, was ich wirklich logseitig sauber bedienen kann, und prüfe nach Updates, ob sich Parser oder Szenarien relevant geändert haben.
Mein Fazit
Für einen einzelnen kleinen Host ohne große Anforderungen ist fail2ban weiterhin nicht falsch. Es ist direkt, bekannt und schnell erklärt. Aber sobald mehrere öffentlich erreichbare Systeme im Spiel sind, ich Entscheidungen teilen will und Logs aus verschiedenen Quellen zusammenführen muss, ist CrowdSec für mich die bessere Wahl.
Mein ehrliches Urteil ist deshalb nicht, dass CrowdSec fail2ban in jeder Lage ersetzt. Sondern dass CrowdSec ab einer gewissen Komplexität das sauberere Betriebsmodell bietet. Die Trennung aus Agent, LAPI und Bouncer ist im Alltag sinnvoll. Die serverübergreifende Nutzung von Decisions spart Zeit. Die Console- und Community-Signale können nützlich sein, wenn die lokale Basis stimmt.
Was ich heute anders machen würde: Ich würde noch früher mit einem kleinen, klar begrenzten Scope starten. Erst SSH und nginx, dann zentrale LAPI, dann Console. Nicht alles gleichzeitig. Und ich würde das Thema echte Client-IP hinter Proxies noch vor der ersten produktiven Aktivierung prüfen, nicht erst nach den ersten merkwürdigen Alerts.
Wenn ich CrowdSec in einem Satz zusammenfassen müsste, dann so: Es ist kein Klick-und-fertig-Werkzeug, aber für Admins mit mehreren Linux-Servern ist es deutlich näher an der Realität moderner Angriffsflächen als ein rein lokaler Ban-Mechanismus.
