Zum Hauptinhalt springen

Zwei Jahre CrowdSec, und jetzt mit WAF

·1090 Wörter·6 min
KI-unterstützt
Castel del Monte

Vor rund zwei Jahren habe ich auf unserem dedizierten Server CrowdSec installiert – halb aus Neugier, halb weil mir fail2ban nach Jahren treuer Dienste zu statisch geworden war. Inzwischen läuft das Ding so unauffällig, dass ich fast vergessen hätte darüber zu schreiben. Zeit für eine Zwischenbilanz.

Der Server beherbergt rund 70 Container in etwa 30 Docker-Compose-Projekten hinter einem einzigen Traefik – Nextcloud, GitLab, ein paar Kartendienste, diverse statische Seiten und einiges an Kleinkram. Also genau die Sorte Host, die im Internet permanent abgeklopft wird.

Was CrowdSec anders macht #

Der grundsätzliche Ablauf ist der von fail2ban: Logs lesen, Muster erkennen, Angreifer sperren. Zwei Dinge sind anders. Erstens ist die Erkennung von der Durchsetzung getrennt – der Agent wertet aus und trifft Entscheidungen, die Bouncer setzen sie um; man kann also mehrere Durchsetzungspunkte an eine Analyse hängen. Zweitens werden die Signale geteilt: Wer mitmacht, bekommt die Community-Blocklist zurück und blockiert damit IP-Adressen, die anderswo schon auffällig geworden sind, bevor sie es bei einem selbst versuchen.

Bei mir läuft der Agent bewusst auf dem Host und nicht im Container (crowdsec v1.8.1 plus crowdsec-firewall-bouncer v0.0.36, beide als systemd-Units). Der Agent muss ohnehin an das Docker-Log von Traefik und an /var/log/auth.log heran, und ein Sicherheitswerkzeug, das mit dem Container-Stack mitstirbt, den es schützen soll, fand ich keine gute Idee.

Zwei Ebenen der Durchsetzung #

Das ist der Teil, der mich anfangs am meisten Nachdenken gekostet hat, denn es gibt zwei Bouncer und sie tun sehr Unterschiedliches:

  • Der Firewall-Bouncer wirft Pakete auf nftables-Ebene für den ganzen Host weg. Grobes Werkzeug, aber billig: Aktuell stehen dort 23.504 IP-Adressen, wovon 23.475 aus der Community-Blocklist stammen und nur 29 lokal erzeugt wurden. Gemeinsam haben sie 905.420 Pakete bzw. 58,8 MB verworfen.
  • Das Traefik-Plugin (maxlerebourg/crowdsec-bouncer-traefik-plugin) entscheidet pro HTTP-Request im Proxy, bevor das Backend überhaupt etwas mitbekommt. Eine so geblockte Anfrage erkennt man im Traefik-Access-Log gut daran, dass ServiceName leer bleibt.

Das Verhältnis 23.475 zu 29 finde ich bemerkenswert: Der weitaus größte Teil dessen, was hier abgewehrt wird, hat nie bei mir angeklopft, sondern wurde von jemand anderem gemeldet. Genau dafür teilt man die Signale.

Der neue Teil: AppSec als WAF #

Wirklich interessant wurde es aber erst mit dem AppSec-Modul, das CrowdSec zu einer Web Application Firewall macht. Der Unterschied zur bisherigen Betrachtung ist grundlegend: Bisher wurden Angreifer im Nachhinein anhand ihres Log-Verhaltens erkannt – ein Scanner musste erst ein paar Mal danebengreifen, bevor er aufflog. AppSec prüft den Request selbst, bevor er durchgelassen wird.

Die Engine hört auf 0.0.0.0:7422 und wird in /etc/crowdsec/acquis.d/ deklariert. Der spannendste Regelsatz heißt crowdsecurity/virtual-patching: Regeln für konkrete, bekannte CVEs in verbreiteter Software. Der Name ist gut gewählt – man patcht damit nicht die Applikation, sondern schiebt dem bekannten Exploit-Pfad einen Riegel vor. Die Sammlung wächst dabei laufend mit: Aktuell stehen 195 Regeln dahinter, jede für eine eigene CVE – von altbekannten Klassikern bis zu Einträgen mit einer CVE-Nummer aus 2026.

Eingebunden wird das im Traefik-Plugin, das dieselbe Middleware sowohl für die LAPI-Abfrage als auch für AppSec verwendet:

[http.middlewares.crowdsec.plugin.crowdsec]
  enabled = true
  logLevel = "INFO"
  crowdsecMode = "live"
  crowdsecLapiScheme = "http"
  crowdsecLapiHost = "host.docker.internal:8080"
  crowdsecLapiKey = "<hier steht der LAPI-Key>"
  crowdsecAppsecEnabled = true
  crowdsecAppsecHost = "host.docker.internal:7422"
  crowdsecAppsecFailureBlock = true
  crowdsecAppsecUnreachableBlock = false

Die letzten beiden Zeilen sind die eigentliche Design-Entscheidung. crowdsecAppsecFailureBlock = true heißt: Trifft eine Regel zu, wird der Request sofort geblockt. crowdsecAppsecUnreachableBlock = false heißt: Ist die AppSec-Engine nicht erreichbar, geht der Request trotzdem durch. Man wählt hier zwischen “im Zweifel dicht” und “im Zweifel offen”, und ich habe mich bewusst für Verfügbarkeit entschieden – ein abgestürzter Sicherheitsdienst soll nicht alle 30 Applikationen offline nehmen.

Das hat allerdings einen Haken, über den man Bescheid wissen muss: Wenn AppSec stirbt, merkt das niemand. Die Seiten funktionieren ja weiter, nur eben ungefiltert. Bei mir hängt deshalb ein Prometheus-Job an CrowdSec, der genau dafür da ist.

Eine Ausnahme mache ich bewusst: maps.webman.at, meine Karte auf Basis von PMTiles, hängt nicht hinter dem CrowdSec-Plugin. Ursprünglich hatte ich die WAF für alle rund 30 Compose-Projekte auf einmal an einer Stelle in der Traefik-Konfiguration aktiviert – das sah zunächst elegant aus, brachte aber prompt die Karte zum Stillstand: Tiles wurden sehr stark verzögert und zum Teil gar nicht mehr geladen. Bei der Anfragefrequenz einer Kartenanwendung – schon ein einziger Zoom- oder Pan-Vorgang löst Dutzende bis Hunderte Tile-Requests aus – reicht die zusätzliche Latenz der AppSec-Prüfung pro Request, um die Karte praktisch unbenutzbar zu machen. Seither bekommt der Router von maps.webman.at bewusst kein crowdsec@file-Middleware mehr zugewiesen. Der Firewall-Bouncer greift dort trotzdem weiter, weil der auf nftables-Ebene ohnehin für den ganzen Host gilt und nichts mit einzelnen Requests zu tun hat.

Was in zwei Jahren angefallen ist #

Ein Blick auf cscli metrics in einem typischen Zeitfenster:

Quelle Zeilen gelesen geparst in Buckets
docker:traefik-traefik-1 393.675 393.621 (99,99 %) 29.396
file:/var/log/auth.log 31.140 2.264 125
appsec:appsec 2.419 2.419 616

Die häufigsten lokal ausgelösten Szenarien sind fast schon langweilig vorhersagbar: http-probing (9.200 Mal), http-sensitive-files (3.572), vpatch-env-access (1.535), vpatch-git-config (212), http-bad-user-agent (153). Übersetzt: Es wird pausenlos nach vergessenen .env-Dateien und offen liegenden .git-Verzeichnissen gesucht. Nichts Kreatives – aber es reicht ja, wenn es einmal klappt.

Auf den echten Traffic gerechnet bekommen etwa 0,2 % der Anfragen ein 403. Stichproben zeigen dahinter Crawler (Bytespider, Amazonbot, Amzn-SearchBot) oder klar bösartige Pfade wie /.env, /_profiler/phpinfo oder /?rest_route=/batch/v1. Blockierten Browser-Traffic von echten Besucher*innen habe ich bisher keinen gefunden – was mir bei einem WAF die wichtigste Zahl überhaupt ist.

Stolpersteine #

Ganz ohne Kanten war es nicht:

  • Whitelists nicht vergessen. Die Collection whitelist-good-actors bringt CDN-, SEO-Bot- und Google-Crawler-Whitelists mit. Ohne so etwas sperrt man sich früher oder später den eigenen Uptime-Check oder einen Suchmaschinen-Bot aus.
  • Der Traefik-Bouncer registriert sich unter seiner Container-IP (traefik-bouncer@<ip>). Bei jedem IP-Wechsel entsteht ein neuer Eintrag; bei mir haben sich inzwischen sechs angesammelt, von denen weiterhin genau einer tatsächlich Daten abholt. Harmlos, aber verwirrend – aufräumen mit cscli bouncers delete traefik-bouncer@<ip>.
  • Der LAPI-Key steht im Klartext in der Traefik-Konfiguration. Wer diese Datei, so wie ich, in Git versioniert, sollte sich das vorher überlegen und nicht, so wie ich, hinterher 🙈.

Fazit #

Nach zwei Jahren ist mein Urteil deutlich: Der Firewall-Bouncer mit der Community-Blocklist ist für den Aufwand von einer halben Stunde Installation ein absurd gutes Geschäft. AppSec ist die interessantere Ergänzung, weil es nicht mehr nur den Absender bewertet, sondern die Anfrage selbst – und virtual-patching ist genau das, was man auf einem Host mit dreißig gewachsenen Applikationen haben will, von denen nicht jede jederzeit auf dem letzten Stand ist.

Was ich beim nächsten Mal anders machen würde: gleich zu Beginn das Monitoring aufsetzen. Ein Sicherheitswerkzeug, das leise ausfällt, ist schlimmer als keines, weil man sich in Sicherheit wähnt.