Skip to content

Knoten einrichten

ZPO installiert nichts selbst auf den Knoten — es erwartet eine bestimmte Grundkonfiguration und prüft sie beim Verbindungstest. Diese Seite beschreibt das Installationsskript, die Vorgaben im Detail und die Anbindung der Resolver.

GegenstandWert
HAProxy2.8 oder neuer
HAProxy Data Plane API3.x (Pfad-Präfix /v3), über HTTPS mit Basic Auth
MikroTik RouterOS7.x mit REST-API
AdGuard Homemit Web-API (/control/...)

ZPO spricht die Data Plane API ausschließlich als v3. Ein Knoten mit 2.x antwortet auf ZPOs Anfragen mit 404 — der Verbindungstest meldet das.

Der schnellste Weg zum HAProxy-Knoten ist das Installationsskript setup-haproxy.sh. Es richtet einen frischen Ubuntu-Server in einem Schritt so ein, wie ZPO ihn erwartet. Auf dem künftigen Knoten genügt:

Terminal window
curl -fsSL https://repo.techeve.de/zpo/setup-haproxy.sh | sudo bash

Mit festem Hostnamen:

Terminal window
curl -fsSL https://repo.techeve.de/zpo/setup-haproxy.sh | sudo bash -s -- --name haproxy-standort

Das Skript liegt auch im Repository unter packaging/setup-haproxy.sh und im Debian-Paket unter /usr/share/doc/zpo/setup-haproxy.sh; von dort lässt es sich ebenso direkt starten (sudo ./setup-haproxy.sh --name haproxy-standort).

Optionen: --api-bind <ip> (Vorgabe: die erste eigene IP — nicht 0.0.0.0), --api-port <port> (Vorgabe 5555), --api-user <name> (Vorgabe zpo), --allow-from <netz>[,<netz>…] (Verwaltungs-Schnittstelle, LAPI und Metriken nur aus diesen Netzen — die Adresse von ZPO und, auf dem Proxy mit der LAPI, die der angebundenen Proxys; wirkt über ufw, ersetzt die Freigaben für alle aus früheren Läufen und gilt für spätere Läufe weiter), --crowdsec (siehe unten).

Nach einem Update von ZPO das Skript erneut ausführen, wenn ZPO am Knoten „Grundkonfiguration veraltet” meldet: Seit 0.6.0 braucht die Grundkonfiguration die Maps zpo_login.map und zpo_trusted.map (Login-Schutz, vertrauenswürdige Netze), die Fehlerraten- und Verbindungsgrenze und — mit CrowdSec — WAF, Prüfseite und Köderpfade.

Das Skript:

  1. setzt den Hostnamen,
  2. installiert HAProxy und die Data Plane API (3.4.3),
  3. legt /etc/haproxy/maps und /etc/haproxy/certs an, mit einer ersten Zeile in jeder Map-Datei,
  4. erzeugt ein selbstsigniertes Zertifikat für die Verwaltung (in /etc/zpo-tls) und gibt dessen Fingerabdruck aus,
  5. schreibt haproxy.cfg und dataplaneapi.yml (eine vorhandene Konfiguration wird vorher gesichert),
  6. richtet den systemd-Dienst samt Härtung ein,
  7. gibt bei aktiver ufw-Firewall den API-Port frei,
  8. legt die Zugangsdaten in /root/haproxy-credentials ab (nur für root lesbar) und prüft die Schnittstelle am Ende selbst.

Das Skript ist wiederholbar: ein zweiter Lauf ändert nichts, was schon stimmt, und erzeugt insbesondere kein neues Passwort.

Was das Skript meldet, was es bedeutet und wie man es behebt, steht unter Knoten: Fehlersuche.

Die Verwaltungs-Schnittstellen sprechen TLS: die Data Plane API (Port 5555), die LAPI von CrowdSec (Port 8080) und die Steuerung der Prüfseite (Port 5556). Alle drei tragen dasselbe selbstsignierte Zertifikat aus /etc/zpo-tls/cert.pem, der Schlüssel liegt daneben. Eine CA gibt es nicht; ZPO prüft das Zertifikat deshalb am Fingerabdruck (SHA-256), den das Skript am Ende ausgibt und den man in ZPO beim Knoten einträgt. Stimmt der Fingerabdruck nicht mehr, verweigert ZPO die Verbindung.

Die Metriken der CrowdSec-Security-Engine (Port 6060) bleiben HTTP: CrowdSec kann dort kein TLS. Sie geben nur Zahlen preis, ohne Anmeldung, und sind mit --allow-from auf das Netz von ZPO beschränkt.

Das Zertifikat wird nur neu erzeugt, wenn sich die Verwaltungsadresse ändert (--api-bind). Dann ändert sich der Fingerabdruck, und er muss in ZPO erneut eingetragen werden.

Bestehende Knoten umstellen. Ein Knoten, der noch über HTTP eingerichtet ist, spricht nach einem Lauf des neuen Skripts nur noch TLS. Deshalb in dieser Reihenfolge:

  1. ZPO auf die Version mit TLS-Unterstützung bringen.
  2. Auf dem Knoten setup-haproxy.sh mit denselben Optionen erneut ausführen. Der Lauf erzeugt das Zertifikat und stellt die Dienste um; die Ausgabe nennt den Fingerabdruck.
  3. In ZPO den Knoten auf https://… umstellen und den Fingerabdruck eintragen. Hat der Standort CrowdSec, die LAPI ebenso.

Zwischen Schritt 2 und 3 kann ZPO den Knoten nicht erreichen und meldet ihn als gestört. Beides deshalb kurz hintereinander erledigen. Angebundene Proxys (--crowdsec-join) brauchen dazu einen neuen Befehl von --crowdsec-proxy, weil der Befehl jetzt den Fingerabdruck der LAPI enthält.

Eine Host-Route ist in HAProxy normalerweise eine ACL plus use_backend in der Konfiguration — jede neue Domain wäre eine Änderung mit Reload. ZPO geht den anderen Weg: ein generisches Frontend zpo_in schlägt den Hostnamen in einer Map-Datei nach und wählt daraus das Backend. Map-Einträge lassen sich über die Runtime-API zur Laufzeit setzen; eine neue Domain braucht keinen Reload.

use_backend %[var(txn.zpo_host),map(/etc/haproxy/maps/zpo_hosts.map)]
MapInhaltWirkung
zpo_hosts.mapHostname → Backend-NameRouting der Dienste
zpo_ipgroups.mapCIDR → Name der ZugangslisteNetze der Zugangslisten
zpo_auth.map<liste>:<base64(benutzer:passwort)> → 1Basic-Auth-Zugänge der Zugangslisten
zpo_ratelimits.mapHostname → <limit>/<liste>/<schalter>/<modus>/<fehlerprofil>/<fehlergrenze>/<verbindungsgrenze>Rate-Limit, Fehlerraten- und Verbindungsgrenze (429), Zugangsliste und Schalter des Dienstes
zpo_login.map<host><pfad> → HöchstzahlLogin-Schutz: Anmeldeversuche je Client in zehn Minuten
zpo_trusted.mapCIDR → Name der IP-Gruppevertrauenswürdige Netze — CrowdSec prüft sie nie
zpo_acme.mapToken → AntwortHTTP-Challenge: nur solange eine Bestellung läuft
zpo_errors.map<fehlerprofil>:<fall> → Fehler-Backendeigene Fehlerseiten für abgewiesene Anfragen

Der Modus der Zugangsliste entscheidet, was ein Client vorweisen muss: ip_only das passende Netz (sonst 403), basic_only eine gültige Anmeldung (sonst 401), and beides, or eines von beidem. Das Rate-Limit zählt je Client-IP und Dienst — wer einen Dienst stark nutzt, stößt dadurch nicht an das Limit eines anderen.

Alle liegen im maps_dir der Data Plane API und lassen sich reload-frei über die Runtime-API ändern. Die Dateien dürfen nicht leer sein: HAProxy lädt eine leere Map zwar, registriert sie aber nicht in der Runtime — und was dort nicht steht, lässt sich nicht beschreiben. Deshalb die Platzhalter (zpo-placeholder.invalid zeigt auf das Auffang-Backend zpo_unknown; 203.0.113.0/24 ist ein nie vergebenes Testnetz; Limit 0 heißt aus). Fehlt eine der vier, meldet ZPO den Knoten mit dem Hinweis, das Einrichtungsskript erneut auszuführen.

Das Frontend bindet mit ssl crt /etc/haproxy/certs/ ein ganzes Verzeichnis. Neue Zertifikate greifen dadurch ohne Reload. Das Verzeichnis braucht mindestens eine Datei, sonst scheitert der Start — bis ZPO echte Zertifikate ausrollt, genügt das selbstsignierte Platzhalter-Zertifikat.

Die API (v3) verwaltet die haproxy.cfg und lädt HAProxy neu. Kernausschnitt aus dataplaneapi.yml:

dataplaneapi:
host: <interne-ip>
port: 5555
user:
- name: zpo
insecure: false
password: "$5$…" # Hash, kein Klartext
resources:
maps_dir: /etc/haproxy/maps
ssl_certs_dir: /etc/haproxy/certs
haproxy:
config_file: /etc/haproxy/haproxy.cfg
haproxy_bin: /usr/sbin/haproxy
reload:
reload_cmd: systemctl reload haproxy
reload_delay: 5

Der Runtime-Socket (stats socket /run/haproxy/admin.sock … level admin expose-fd listeners) ist der Weg, auf dem ZPO Map-Einträge und den Zustand einzelner Server ohne Reload setzt.

Die Verwaltungs-Schnittstelle gehört nicht ins offene Netz: sie bindet an eine interne Adresse oder steht hinter dem VPN, über das ZPO den Standort erreicht.

Nach der Einrichtung gehört die haproxy.cfg ZPO allein — von Hand geänderte Zeilen überschreibt die Data Plane API beim nächsten Schreibzugriff. ZPO legt Backends mit dem Präfix zpo_ an und fasst nur solche an; alles ohne dieses Präfix bleibt unberührt.

Der Proxy-Knoten bekommt in ZPO die Verwaltungs-Adresse (https://<ip>:5555), den Fingerabdruck des Zertifikats, Benutzer und Passwort aus /root/haproxy-credentials sowie die internen Adressen (IPv4/IPv6), unter denen Clients im lokalen Netz den Proxy erreichen — die landen in den lokalen DNS-Einträgen. Optional kommen die öffentlichen Adressen dazu, unter denen der Proxy aus dem Internet erreichbar ist.

Der Verbindungstest meldet drei Stände:

  • erreichbar — API antwortet, Zugangsdaten stimmen, zpo_hosts.map ist da.
  • beeinträchtigt — die API antwortet, aber die Map-Datei fehlt. Der Hinweis nennt sie; bis dahin lässt sich auf dem Knoten nichts routen.
  • nicht erreichbar — keine Antwort oder abgelehnte Anmeldung; der Grund steht am Knoten.

CrowdSec erkennt Angriffe in den HAProxy-Logs und sperrt direkt in HAProxy. Je Standort gibt es eine LAPI (Local API, die Stelle, an der CrowdSec Erkennungen sammelt und Sperren verwaltet). Sie läuft auf einem Proxy des Standorts; weitere Proxys desselben Standorts melden dorthin und holen ihre Sperren von dort. Zwischen den Standorten gleicht ZPO die Sperren ab.

Auf dem ersten Proxy eines Standorts:

Terminal window
curl -fsSL https://repo.techeve.de/zpo/setup-haproxy.sh | sudo bash -s -- --crowdsec

Was dabei entsteht:

  • Security Engine mit der LAPI des Standorts (crowdsec), die die HAProxy-Logs liest — aus /var/log/haproxy.log, wenn CrowdSec die Datei bei der Installation selbst erkennt (Ubuntu mit rsyslog), sonst aus dem Journal; dazu die Collections crowdsecurity/haproxy (Scanner, Exploits, empfindliche Dateien, Brute-Force) und crowdsecurity/wordpress. Die LAPI lauscht auf der Verwaltungsadresse, Port 8080, die Metriken der Security Engine auf Port 6060. Die LAPI spricht TLS mit dem Zertifikat des Knotens wie die Data Plane API; die Metriken bleiben HTTP (siehe Verwaltung über TLS und Fingerabdruck). Ist ufw aktiv, gibt das Skript die Ports frei; wer kann, beschränkt sie auf die Adresse von ZPO (ufw allow from <zpo> to any port 8080,6060 proto tcp).
  • Hostname und Sperre im Log: HAProxy schreibt den Namen des Dienstes in jede Logzeile, bei einer Sperre dazu ban ({name|ban} hinter den Zählern). Zwei eigene Parser werten das aus: zpo/haproxy-host macht den Namen zu target_fqdn — Alarme in cscli alerts inspect nennen dann den angegriffenen Dienst —, und zpo/blocked nimmt Anfragen aus, die schon die Sperrseite bekamen. Sonst zählte CrowdSec deren 403 als neue Sonde, und jede Sperre verlängerte sich selbst.
  • HAProxy-Bouncer (crowdsec-haproxy-spoa-bouncer), lokal auf Port 9000. HAProxy fragt ihn für jede Verbindung; eine gesperrte Adresse bekommt die Sperrseite (403), bevor Umleitung, Zugangsliste oder Rate-Limit greifen. Die Seite kommt aus dem Standardprofil der Fehlerseiten (Fall „Gesperrt (CrowdSec)”), bis ZPO sie ablegt steht dort ein schlichter Text. Die HTTP-01-Antworten für Let’s Encrypt gehen immer durch, ebenso Dienste, bei denen „Von CrowdSec prüfen lassen” aus ist.
  • Fail open: Steht der Bouncer, nimmt der Health-Check von HAProxy ihn aus dem Spiel, und die Dienste antworten ohne Prüfung weiter — ohne Wartezeit je Anfrage. Dass der Schutz dann fehlt, meldet ZPO.
  • Zwei Zugänge für ZPO: der Lesezugang zpo-monitor (Überwachung) und der Maschinen-Zugang zpo-sync (Abgleich der Sperren, Alarme für die Statistik). Adresse, Schlüssel und Maschinen-Passwort stehen am Ende der Ausgabe und in /root/haproxy-credentials.
  • Der Dienst in jedem Alarm: eine Kontext-Datei (/etc/crowdsec/contexts/zpo.yaml) gibt jedem Alarm den angegriffenen Dienst mit — die Statistik zeigt damit, wen es trifft.

Einmal eingerichtet, bleibt CrowdSec bei jedem weiteren Lauf des Skripts erhalten, auch ohne den Schalter.

Die Pakete kommen aus der Paketquelle von CrowdSec (packagecloud, nur HTTPS). Steht apt hinter einem Cache-Proxy wie apt-cacher-ng, der HTTPS nicht tunnelt, legt das Skript /etc/apt/apt.conf.d/99zpo-https-direct an: HTTPS-Quellen gehen dann direkt, HTTP-Quellen weiter über den Cache.

In ZPO eintragen: unter Einrichtung → CrowdSec beim Standort „LAPI eintragen”: LAPI-Adresse (https), Fingerabdruck, Schlüssel und Maschinen-Passwort. Die Seite zeigt je Standort die LAPI und alle seine Proxys mit ihrem CrowdSec-Zustand, dazu den Abgleich zwischen den Standorten.

Ein zweiter (dritter …) Proxy am selben Standort bekommt keine eigene LAPI, sondern meldet an die des Standorts. Zwei Schritte:

  1. Auf dem Proxy mit der LAPI den Zugang anlegen — das ändert sonst nichts und gibt den Befehl für den neuen Proxy aus. Der Name ist der des Proxys in ZPO; daran erkennt ZPO, ob sein Bouncer abholt:

    Terminal window
    sudo setup-haproxy.sh --crowdsec-proxy <name-des-neuen-proxys>
  2. Den ausgegebenen Befehl auf dem neuen Proxy ausführen (--crowdsec-join <lapi> --crowdsec-fingerprint … --crowdsec-token … --crowdsec-bouncer-key …). Der Fingerabdruck lässt das Skript das Zertifikat der LAPI prüfen, bevor es ihr vertraut; stimmt er nicht, bricht der Lauf ab und ändert nichts. Er bekommt Agent, WAF und Bouncer: Der Agent liest die eigenen Logs und meldet an die LAPI des Standorts (Auto-Registrierung mit dem Token, nur aus privaten Netzen), der Bouncer holt seine Sperren von dort. In ZPO ist dafür nichts einzutragen — der Proxy erscheint unter Einrichtung → CrowdSec bei seinem Standort. Spätere Läufe des Skripts lassen ihn angebunden.

Der ausgegebene Befehl enthält Token und Bouncer-Schlüssel; er gehört nur auf den neuen Proxy. Das Token steht auf dem Proxy mit der LAPI in /etc/crowdsec/config.yaml.local (nur für root lesbar) und in /root/haproxy-credentials. Mit ihm können sich Agents aus privaten Netzen anmelden.

Update von 0.6: Hatten an einem Standort mehrere Proxys je eine eigene LAPI in ZPO, übernimmt 0.7 die des ersten; die übrigen Proxys nennt eine Warnung im Log. Sie werden mit --crowdsec-proxy/--crowdsec-join an die übernommene LAPI angebunden.

ZPO prüft im Takt des Abgleichs die LAPIs und die Proxys, dazu alle fünf Minuten an den Metriken von CrowdSec, ob der Schutz tatsächlich arbeitet.

Zustände der LAPI (beim Standort):

ZustandBedeutung
aktivLAPI antwortet, der Zugang gilt, alle Bouncer holen ab — dazu die Zahl der Sperren aus eigener Erkennung und von Hand
LAPI antwortet nichtneue Sperren kommen nicht mehr an, der Abgleich ruht; die Bouncer setzen die bekannten weiter durch
Zugang abgewiesender Schlüssel stimmt nicht (mehr)
Bouncer holt nichts abein Bouncer, der schon abgeholt hat, holt seit fünf Minuten nichts mehr — der Hinweis nennt ihn; neue Sperren wirken an seinem Proxy nicht

Zustände eines Proxys (beim Proxy-Knoten und unter Einrichtung → CrowdSec):

ZustandBedeutung
aktivBouncer läuft, der Agent liest und versteht die Logs
Bouncer stehtHAProxy erreicht den Bouncer nicht — Anfragen gehen ungeprüft durch
nicht verdrahtetdie Grundkonfiguration kennt den Bouncer nicht; das Skript mit --crowdsec bzw. --crowdsec-join ausführen
liest keine LogsHAProxy hat Anfragen beantwortet, der Agent aber keine Logzeile gelesen — er erkennt nichts. Meist läuft rsyslog nicht oder die Log-Quelle fehlt (cscli metrics show acquisition)
versteht die Logs nichtder Agent hat nicht einmal halb so viele Zeilen verstanden, wie HAProxy Anfragen weitergereicht hat — das Log-Format stimmt nicht; das Skript erneut ausführen

Sind die Metriken nicht erreichbar (ältere Einrichtung, Firewall), bleibt der Zustand „aktiv” mit dem Zusatz „Logs nicht überwacht”: Der Schutz wirkt, ZPO sieht nur nicht, ob CrowdSec etwas erkennt. Abhilfe: das Skript mit --crowdsec erneut ausführen. Weil der Vergleich ein Zeitfenster braucht, fällt eine solche Störung nach fünf bis zehn Minuten auf.

Jeder Wechsel zwischen „aktiv” und einer Störung erzeugt ein Ereignis (crowdsec_down, crowdsec_recovered) und damit einen Alarm.

Gesamtzustand der LAPI. Die LAPI ist das Herzstück eines Standorts: Fällt sie aus, wirken keine neuen Sperren. Unter Einrichtung → CrowdSec steht beim Standort deshalb ein Gesamtzustand — gesund, eingeschränkt oder gestört — mit den Prüfungen dahinter:

Prüfungeingeschränkt bzw. gestört, wenn …
LAPI erreichbar, Lesezugang gültigdie LAPI nicht antwortet oder den Schlüssel abweist (gestört)
Metriken der LAPI lesbarZPO Port 6060 der LAPI seit fünf Minuten nicht erreicht
Jeder Proxy holt Sperren abfür einen aktiven Proxy des Standorts kein Bouncer abholt
Agents aller Proxys melden sichweniger Agents anfragen, als der Standort aktive Proxys hat
CrowdSec auf allen Proxys in Ordnungein Proxy eine der Störungen oben meldet
Firewall-Bouncer holen Sperren ab(nur wenn einer an der LAPI hängt) ein Firewall-Bouncer — etwa cs-routeros-bouncer, der Sperren in den MikroTik trägt — seit zehn Minuten nichts mehr abholt; nach einem Tag Stille gilt er als entfernt und fällt aus der Prüfung
Abgleich mit den anderen Standorten(bei mehreren Standorten) der Maschinen-Zugang fehlt, der letzte Abruf älter als fünf Minuten ist oder Sperren anderer Standorte fehlen

Über Bouncer und Agents urteilt ZPO erst, wenn es der LAPI ein paar Minuten zugesehen hat.

Benachrichtigung: Besteht eines dieser Probleme fünf Minuten lang, legt ZPO ein Ereignis an (crowdsec_down, „CrowdSec am Standort … eingeschränkt” mit dem Grund) und schickt es über die Alarmkanäle; ist es behoben, folgt eine Entwarnung (crowdsec_recovered), sobald wieder alle Prüfungen laufen und in Ordnung sind. Ausfälle der LAPI selbst und der Proxys melden die Zustände oben ohnehin.

Firewall-Bouncer erkennt ZPO am Namen (routeros-bouncer…, cs-routeros-bouncer…, cs-firewall-bouncer…) und führt sie als zweite Sperrebene; meldet sich einer von einer neuen Adresse, zählen seine Einträge (name@adresse) zusammen.

Angebundene Systeme. Darunter stehen alle Bouncer und Maschinen, die bei der LAPI anfragen — mit Zuordnung (Bouncer bzw. Agent eines Proxys, ZPO selbst) und letzter Anfrage. Systeme, die ZPO nicht angelegt hat, etwa ein Firewall-Bouncer auf einem App-Server, stehen als fremd da. Die LAPI verrät über ihre Schnittstelle nicht, wer angemeldet ist; ZPO liest die Systeme einmal je Minute aus ihren Metriken (Anfragen je Bouncer und Maschine). Es kennt deshalb nur Systeme, die seit dem letzten Start der LAPI angefragt haben, und ohne Adresse oder Version. Wer sieben Tage still ist, fällt aus der Liste. Stellt man die Adresse der LAPI nur um (etwa von http:// auf https://), bleibt die Liste erhalten — so fällt auch ein Firewall-Bouncer auf, der danach nicht mehr abholt. Ein anderer Rechner oder Port gilt als andere LAPI, dann beginnt die Liste von vorn. Zugänge entfernen: auf dem LAPI-Proxy cscli bouncers delete <name> bzw. cscli machines delete <name>.

Echte Client-Adresse: CrowdSec sperrt die Adresse, die HAProxy als Quelle sieht. Steht vor dem Proxy ein weiterer Proxy oder ein NAT, der die Adresse ersetzt, sähe CrowdSec nur dessen Adresse. Private Adressen nimmt CrowdSec von sich aus aus (crowdsecurity/whitelists) — ein vorgelagertes Gerät im eigenen Netz wird also nicht gesperrt, die Angreifer dahinter allerdings auch nicht.

Sperren zwischen den Standorten abgleichen

Section titled “Sperren zwischen den Standorten abgleichen”

Jeder Standort führt seine eigene LAPI und arbeitet auch ohne Verbindung zu den anderen weiter. ZPO gleicht die Sperren ab: Was eine LAPI selbst erkennt oder von Hand gesperrt wird, trägt ZPO mit der Restlaufzeit in die LAPIs der übrigen Standorte ein — als Herkunft zpo-sync, als Grund zpo-sync/<standort> (cscli decisions list zeigt beides). Unter Einrichtung → CrowdSec steht, wie viele Sperren von welchem Standort auf welchen übertragen sind und welche zuletzt.

  • Voraussetzung: bei der LAPI in ZPO zusätzlich das Maschinen-Passwort. Ohne es liefert der Standort seine Sperren nur an die anderen und bekommt keine.
  • Aufheben am Ursprung hebt die Sperre überall auf. Aufheben an einem anderen Standort gilt nur dort — etwa um einen Kunden an einem Standort zuzulassen.
  • Adressen aus dem eigenen Netz (privat, Loopback, Link-Local) bleiben an ihrem Standort: Ein Fehlalarm gegen ein internes Gerät soll nicht überall zugleich aussperren.
  • Die Community-Blockliste von CrowdSec gleicht ZPO nicht ab — die holt jede LAPI selbst.
  • Antwortet eine LAPI nicht, holt der Abgleich nach, sobald sie wieder da ist.

Die aktiven Entscheidungen aller Standorte zeigt die Ansicht CrowdSec-Sperren in einer Tabelle. Eine Adresse, die an mehreren Standorten gilt, steht dort einmal:

  • Art: Ban ist eine echte Sperre — die Adresse kommt nicht durch, am Router auch nicht. Captcha ist Stufe 1: Die Adresse bekommt nur die Prüfseite und darf nach gelöster Aufgabe durch.
  • Herkunft: der Standort, von dem die Sperre stammt (bei von Hand gesetzten mit dem Zusatz „von Hand”). Darunter kleine Marken: Alle Standorte, wenn sie an jedem erreichbaren Standort gilt, sonst die Standorte, an denen sie gilt.
  • Läuft ab: ein Countdown bis zur Freigabe, darunter der genaue Zeitpunkt. Gilt die Adresse an mehreren Standorten, zählt das späteste Ende — erst dann ist sie überall wieder frei.

Über der Tabelle steht je Standort der Stand des Abgleichs: Zahl der eigenen und der übernommenen Sperren, der letzte Abruf durch ZPO und — falls die LAPI eine Weile nicht antwortete — der Rückstand: Sperren anderer Standorte, die dort noch fehlen und die ZPO nachträgt, sobald sie wieder antwortet.

Suchen und filtern: Das Suchfeld sucht in Adresse, Grund und Standort; dazu lässt sich nach Art (Ban, Captcha) und Standort filtern. Ein Klick auf eine Adresse zeigt nur sie — und darüber ihren Verlauf: die Vorfälle, die ZPO für die Sperrstufen zählt, mit Zeit, Standort und Grund, und dem Vorfall, der zur Sperre führte. Der Verlauf reicht so weit zurück wie das Zeitfenster der Sperrstufen; nach einem manuellen Aufheben beginnt er von vorn.

Aufheben geht mit den beiden Symbolen am Ende der Zeile: überall (offenes Schloss mit Wellen) oder nur an einem Standort (offenes Schloss — bei mehreren Standorten erscheint eine Auswahl). An den übrigen Standorten gilt die Sperre dann weiter. Das geht über den Maschinen-Zugang zpo-sync und braucht Schreibrecht auf die Knoten; es steht im Audit-Log. Von Hand am Knoten: cscli decisions delete --ip <adresse>.

Eine einzelne Adresse oder ein Netz lässt sich auch ohne die Liste entsperren: unter Einrichtung → CrowdSec, Adresse entsperren — an einem Standort oder an allen.

Vertrauenswürdige Netze (Allowlist): Unter Einrichtung → CrowdSec, Vertrauenswürdige Netze, oder an einer IP-Gruppe mit dem Haken „vertrauenswürdig — nie von CrowdSec sperren”: etwa die öffentlichen Adressen der eigenen Standorte, ein Monitoring-Dienst — oder ein Router, der Verkehr aus anderen internen Netzen auf seine eigene Adresse umschreibt. Dann erscheint der ganze interne Verkehr von dort unter einer einzigen Adresse, und eine Sperre gegen sie schnitte alle diese Netze auf einmal ab. HAProxy fragt für vertrauenswürdige Netze den Bouncer gar nicht (zpo_trusted.map), und ZPO hebt Sperren gegen sie im Abgleich auf, statt sie zu verteilen. Die LAPI bietet Allowlists nur zum Lesen an — deshalb dieser Weg statt cscli allowlists. Was dort trotzdem von Hand hinterlegt ist, zeigt ZPO beim Standort unter Allowlists in CrowdSec; diese gelten für alle Bouncer an der LAPI, auch fremde.

Das Skript richtet Ubuntu-Server ein; läuft HAProxy als Container, geht es mit den offiziellen Images ebenso — ohne eigene Variante:

  • CrowdSec (crowdsecurity/crowdsec) als LAPI des Standorts, mit den Collections crowdsecurity/haproxy und den AppSec-Collections (Variable COLLECTIONS) und denselben Dateien, die das Skript schreibt: die ZPO-Parser, die Kontext-Datei, zpo-waf.yaml und eine AppSec-Quelle auf 0.0.0.0:7422 (nur im Docker-Netz). Port 8080 und die Metriken (6060) auf einer internen Adresse veröffentlichen, die ZPO erreicht; 6060 auch unter der Verwaltungsadresse des Proxys.

  • Logs: HAProxy loggt nach stdout. CrowdSec liest sie über die Docker-API (source: docker, container_name: [<haproxy>], labels: type: haproxy) — am besten über einen Socket-Proxy, der nur lesende Zugriffe auf Container, Info und Ereignisse erlaubt, statt den Docker-Socket in den CrowdSec-Container zu hängen. Gemeinsame Log-Verzeichnisse sind nicht nötig.

  • Bouncer (crowdsecurity/spoa-bouncer) als eigener Container im Netz von HAProxy, CHALLENGE_HTTP_LISTEN=0.0.0.0:9100, AppSec über eine crowdsec-spoa-bouncer.yaml.local mit appsec_url und dem Host * für die Sperrstufen (siehe unten).

  • HAProxy: die CrowdSec-Blöcke des Skripts (Frontend, SPOE-Datei zpo-crowdsec.cfg) und die Backends crowdsec-spoa/crowdsec-challenge mit dem Container-Namen statt 127.0.0.1 — mit resolvers auf Dockers DNS (127.0.0.11) und init-addr last,libc,none, damit HAProxy auch startet, wenn der Bouncer gerade fehlt.

  • TLS: Ein Zertifikat für alle Verwaltungs-Schnittstellen des Proxys erzeugen (wie management_tls im Skript, mit allen Adressen und Container-Namen, über die es angesprochen wird, im SAN) und in die Container einhängen. Die Data Plane API und zpo-captcha als program im HAProxy-Container bekommen die Optionen wie auf einem Knoten (--scheme https --tls-host … --tls-port … --tls-certificate … --tls-key … bzw. -tls-cert … -tls-key …). Das CrowdSec-Image löscht den TLS-Block der LAPI beim Start, solange USE_TLS nicht gesetzt ist; deshalb über seine Variablen: USE_TLS=true, LAPI_CERT_FILE, LAPI_KEY_FILE, CACERT_FILE und LOCAL_API_URL=https://127.0.0.1:8080. Der SPOA-Bouncer bekommt CROWDSEC_URL=https://… und ca_cert_path in seiner .yaml.local.

  • zpo-captcha im Container von Hand aktualisieren: Die Binary stammt aus dem Paket zpo-captcha und muss zur Version von ZPO passen. Eine ältere Binary kennt neuere Optionen nicht, bricht ab, und HAProxy beendet mit exit-on-failure alle Programme mit — der Proxy fällt aus. Deshalb vor dem Neustart zpo-captcha -version und zpo-captcha -h prüfen.

Den Agent im CrowdSec-Image (Maschine localhost) erkennt ZPO als Agent des Proxys.

Mit --crowdsec richtet das Skript auch CrowdSec AppSec ein, lokal auf 127.0.0.1:7422: die Collections für virtuelle Patches, allgemeine Regeln, das OWASP Core Rule Set und die Bot-Erkennung mit Prüfseite. Was davon für einen Dienst gilt, stellt man am Dienst ein (WAF-Stufe, Browser-Prüfung, Köderpfade, siehe Dienste); HAProxy reicht die Einstellung über die Schalter in zpo_ratelimits.map an AppSec weiter (AppSec-Config zpo/waf). Dienste ohne WAF fragt der Bouncer nur nach der Adresse — AppSec sieht sie nie.

  • Anfragen mit Körper bis 12 kB gehen samt Körper an AppSec, größere ohne. WebSockets (etwa die Proxmox-Konsole) sind davon nicht betroffen.
  • Die Prüfseite liefert der Bouncer selbst aus (127.0.0.1:9100, Pfad /crowdsec-internal/challenge/). Ferngesteuerte Browser (Selenium, Playwright, Browser-Erweiterungen mit Debugger) fallen durch — das ist der Sinn.
  • Wie Erkennungen zu Sperren werden, steht unter Zwei Sperrstufen. Eine Entscheidung „challenge” der LAPI macht der Bouncer ohnehin zur Sperre — prüfen kann nur AppSec.
  • Köderpfade führen über das Szenario zpo/trap sofort zur Sperre.

Ohne CrowdSec am Knoten wirken WAF, Prüfseite und Köderpfade dort nicht — die frühere WAF aus eigenen Regex-Regeln gibt es seit 0.6.0 nicht mehr.

Seit 0.9.0 sperrt ZPO in zwei Stufen. Wer zum ersten Mal auffällt, ist oft kein Angreifer — ein Mensch hinter einem NAT, ein zu eifriger Client. Erst wer weitermacht, wird an der Firewall gesperrt.

WirkungWo
Stufe 1 (erste Erkennung)Prüfseite: eine kurze Rechenaufgabe im Browser (ALTCHA), danach durchnur HAProxy
Stufe 2 (Wiederholung, Prüfseite ignoriert, Ausnutzungsversuch)SperreHAProxy und Router

So greift es ineinander:

  • LAPI (profiles.yaml, das Original liegt als profiles.yaml.vor-zpo daneben): Jede Erkennung gegen eine Adresse wird eine Entscheidung vom Typ captcha für 4 h. Sofort ban gibt es nur für Köder (zpo/trap) und wer die Prüfseite ignoriert (zpo/captcha-ignored: mehr als 60 Seitenaufrufe auf die Prüfseite in kurzer Folge). Als Sicherheitsnetz, falls ZPO nicht läuft, sperrt die LAPI ab dem fünften Alarm in sieben Tagen selbst — deshalb lässt ZPO höchstens „Stufe 2 ab dem 4. Vorfall” zu. Wer von Hand entsperrt wird, fängt wieder bei Stufe 1 an.
  • ZPO zählt die Vorfälle über alle Standorte und trägt ab dem eingestellten (Vorgabe: zweiten) an der LAPI ein ban ein; der Abgleich verteilt es. Szenarien aus der Liste „direkt Stufe 2” (Vorgabe: virtuelle Patches der WAF, also Versuche, bekannte Lücken auszunutzen) überspringen Stufe 1. Die Sperre dauert erst 24 h und verdoppelt sich bei jedem weiteren Vorfall bis zu einer Woche. Einstellbar unter Einrichtung → CrowdSec → Sperrstufen; dort lässt sich Stufe 1 auch ganz abschalten.
  • Router-Bouncer (cs-routeros-bouncer, cs-firewall-bouncer) nehmen nur ban (supported_decisions_types: [ban]) — Stufe 1 erreicht den Router nie. Die Community-Liste ist ban und landet weiter am Router.
  • HAProxy prüft das Cookie von zpo-captcha selbst (hmac, Schlüssel in zpo_captcha.map) und schickt Browser ohne gültiges Cookie zur Prüfseite. Dienste mit „Captcha bei Verdacht erlauben” aus (Schalter n) lassen Stufe 1 durch. Die Prüfseite steht im Log als captcha.
  • Bouncer: Für Dienste mit WAF machte der Bouncer aus captcha ein ban, weil er keinen passenden Host kennt. Das Skript legt deshalb crowdsec-spoa-bouncer.yaml.local mit einem Host * an (fallback_remediation: captcha, AppSec mit always_send) — die Meldung „empty captcha provider” beim Start des Bouncers ist erwartet.

zpo-captcha ist ein kleines Programm von ZPO auf jedem Proxy (Paket zpo-captcha aus der Paketquelle von ZPO, --repo, Vorgabe https://repo.techeve.de). Die Prüfseite lauscht lokal auf 127.0.0.1:9200, die Steuerung für ZPO auf der Verwaltungsadresse, Port 5556, mit den Zugangsdaten der Data Plane API (/etc/zpo-captcha/env). ZPO überträgt Schlüssel, Gültigkeit des Cookies, Schwierigkeit und die Prüfseiten der Fehlerprofile (Fall „Prüfseite (Stufe 1)”, Platzhalter {{captcha}}) und meldet einen Proxy, dessen zpo-captcha nicht antwortet, als „Prüfseite fehlt” — samt Alarm. Ist ufw aktiv, gibt das Skript 5556 frei. Die Steuerung spricht TLS mit demselben Zertifikat wie die Data Plane API, und ZPO prüft es am Fingerabdruck des Knotens. Der Schlüssel, mit dem sich Cookies fälschen ließen, geht damit nicht mehr im Klartext über das Verwaltungsnetz.

Läuft der Proxy in Docker: das Image registry.techeve.de/techeve/zpo/captcha (Prüfseite auf 9200 im Container-Netz, Steuerung auf 5556, ZPO_CAPTCHA_API_USER/ZPO_CAPTCHA_API_PASSWORD, Stand unter /data), im Backend zpo-captcha der Container-Name statt 127.0.0.1, die profiles.yaml, das Szenario und die .yaml.local des Bouncers wie oben. Die Steuerung startet das Image ohne TLS. Mit Zertifikat das Zertifikat und den Schlüssel unter /data/tls einhängen und hinten anhängen: -tls-cert /data/tls/cert.pem -tls-key /data/tls/key.pem.

Firewall-Bouncer: Sperren auf dem MikroTik

Section titled “Firewall-Bouncer: Sperren auf dem MikroTik”

Stufe 2 sperrt zusätzlich am Router: Ein Firewall-Bouncer holt die Sperren (ban) von der LAPI des Standorts und trägt sie als Address-Lists samt Filterregeln in den MikroTik ein. So prallt ein gesperrter Angreifer schon an der Firewall ab, nicht erst an HAProxy. Das Setup-Skript richtet ihn nicht ein; ZPO erkennt und überwacht ihn aber (siehe Überwachung). Bewährt hat sich cs-routeros-bouncer (Projekt jmrplens/cs-routeros-bouncer), als Binary mit systemd-Unit auf dem Proxy mit der LAPI oder als Container.

1. Am Router einen eigenen Zugang anlegen — im RouterOS-Terminal als admin, mit der Adresse des Rechners, auf dem der Bouncer läuft:

/user group add name=crowdsec-bouncer policy=read,write,api,sensitive,!ftp,!local,!ssh,!reboot,!policy,!test,!password,!sniff,!romon,!rest-api,!winbox,!web comment="cs-routeros-bouncer"
/user add name=crowdsec-bouncer group=crowdsec-bouncer password=<passwort> address=<bouncer-ip>/32 comment="cs-routeros-bouncer"
/ip service set api disabled=no address=<bouncer-ip>/32

Die Gruppe hat genau die Rechte, die der Bouncer braucht (Address-Lists und Regeln schreiben, über die API). Der Benutzer darf sich nur von der Adresse des Bouncers anmelden, und der API-Dienst (Port 8728) ist nur für diese Adresse offen. Verlangt eine bestehende Firewall-Regel mehr, den API-Port für diese Adresse dort ebenfalls freigeben.

2. An der LAPI einen Bouncer-Zugang anlegen — auf dem Proxy mit der LAPI:

Terminal window
sudo cscli bouncers add routeros-bouncer

Den ausgegebenen Schlüssel aufheben; er erscheint nur einmal. Der Name sollte mit routeros-bouncer oder cs-routeros-bouncer beginnen, daran erkennt ZPO einen Firewall-Bouncer.

3. Den Bouncer installieren — die Binary des Release nach /usr/local/bin/cs-routeros-bouncer, die Konfiguration nach /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml (Besitzer root, Modus 600, sie enthält beide Zugänge). Die wichtigen Teile:

crowdsec:
api_url: "https://<verwaltungsadresse>:8080/"
api_key: "<schlüssel aus schritt 2>"
update_frequency: "10s"
scopes: ["ip", "range"]
supported_decisions_types: ["ban"] # nur Stufe 2, nie die Prüfseite
retry_initial_connect: true
mikrotik:
address: "<router-ip>:8728"
username: "crowdsec-bouncer"
password: "<passwort aus schritt 1>"
firewall:
ipv4: { enabled: true, address_list: "crowdsec-banned" }
ipv6: { enabled: true, address_list: "crowdsec6-banned" }
filter: { enabled: true, chains: ["input"] }
raw: { enabled: true, chains: ["prerouting"] }
deny_action: "drop"
rule_placement: "top"

supported_decisions_types: ["ban"] ist wichtig: Ohne das sperrte der Router schon in Stufe 1, und die Prüfseite käme nie an.

Die LAPI spricht TLS mit dem selbstsignierten Zertifikat des Knotens. Der Bouncer vertraut ihm über SSL_CERT_FILE; die Option ca_cert_path im Abschnitt crowdsec greift bei diesem Bouncer nicht. Die Unit:

/etc/systemd/system/cs-routeros-bouncer.service
[Unit]
Description=CrowdSec RouterOS Bouncer
After=network-online.target crowdsec.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/cs-routeros-bouncer -c /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml
Environment=SSL_CERT_FILE=/etc/zpo-tls/cert.pem
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

Läuft der Bouncer nicht auf dem Proxy mit der LAPI, das Zertifikat der LAPI (/etc/zpo-tls/cert.pem dort) auf seinen Rechner kopieren und den Pfad in SSL_CERT_FILE anpassen.

Als Container: CROWDSEC_URL=https://<lapi>:8080/, SSL_CERT_FILE auf das eingehängte Zertifikat, die Zugänge über eine env_file mit Modus 600.

4. Prüfen:

Terminal window
sudo systemctl enable --now cs-routeros-bouncer
sudo journalctl -u cs-routeros-bouncer -n 20

Nach dem Start gleicht der Bouncer die Listen ab und meldet reconciliation complete; danach trägt er neue Sperren in wenigen Sekunden ein. In ZPO erscheint er unter Einrichtung → CrowdSec bei der LAPI als Firewall-Bouncer.

ZPO schreibt statische Einträge über die REST-API (/rest/ip/dns/static).

Hat ein Dienst am Standort nur eine IPv4-Adresse, legt ZPO auf RouterOS zusätzlich einen AAAA-Eintrag mit der Sperradresse ::ffff an. Ohne ihn beantwortet der Router AAAA nicht leer, sondern fragt den Upstream und reicht eine öffentliche IPv6 des Namens durch — Clients bevorzugen sie und gingen am lokalen Proxy vorbei ins Internet. Mit der Sperradresse fallen sie sofort auf IPv4 zurück; so empfiehlt es MikroTik selbst. Trägt der Proxy eine interne IPv6, steht stattdessen sie im AAAA-Eintrag. AdGuard Home braucht das nicht: Es beantwortet fehlende Typen eines umgeschriebenen Namens ohnehin leer.

  • URL: https://<router>/rest
  • Zugang: ein Benutzer, dessen Gruppe die Richtlinien read, write und rest-api umfasst — alle übrigen dürfen gesperrt sein. api gilt nur für die binäre API auf Port 8728/8729 und ist für ZPO nicht nötig. Ein eigenes Benutzerkonto nur für ZPO ist angeraten.
  • Dienst: Die REST-API läuft über den Webdienst des Routers — www-ssl (bzw. www für http://) muss unter IP → Services aktiv und von ZPO aus erreichbar sein.
  • Verbindungstest: Der Knopf Testen liest /rest/system/resource und /rest/ip/dns/static. Schlägt er fehl, steht der Grund am Knoten und im Protokoll von ZPO (connection test failed).
  • TLS: RouterOS spricht REST über HTTPS; bei selbstsignierten Geräte- Zertifikaten bleibt die Zertifikatsprüfung im Knoten abschaltbar.

ZPO überschreibt keinen Eintrag, den jemand anderes angelegt hat.

  • RouterOS: Jeder Eintrag, den ZPO schreibt, bekommt den Kommentar #ZPO. Ein Eintrag, dessen Kommentar mit #ZPO beginnt, gehört ZPO — auch wenn ZPO ihn nicht kennt (etwa nach einer verlorenen Datenbank oder wenn du ihn von Hand so markierst). Solche Einträge übernimmt ZPO ohne Rückfrage. Der alte Vermerk zpo:<nummer> zählt ebenfalls und wird beim nächsten Schreiben ersetzt.
  • AdGuard Home hat kein Kommentarfeld. Hat ZPO einen Namen einmal geschrieben, gehört er ZPO. Beim ersten Mal ist fremd, was dort schon steht und nicht genau den gewünschten Adressen entspricht.

Steht unter einem Namen ein fremder Eintrag, schreibt ZPO nicht, sondern meldet einen Konflikt: Unter Abgleich erscheint der Eintrag mit dem Zustand „Konflikt“, den fremden Adressen und dem Knopf Übernehmen. Erst nach dieser Freigabe ersetzt ZPO die fremden Einträge; von da an gehören sie ZPO. Ohne Freigabe bleibt alles, wie es ist — auch kein stilles Nebeneinander, bei dem der Resolver beide Adressen reihum ausliefern würde.

ZPO pflegt DNS-Rewrites über die Web-API (/control/rewrite/...).

  • URL: die Basis-Adresse der Weboberfläche, z. B. http://<host>:3000
  • Zugang: Benutzername und Passwort eines Admin-Kontos der Oberfläche.

AdGuard Home kennt keine TTL je Rewrite; Antworten tragen die serverweite blocked_response_ttl (Vorgabe: 10 Sekunden). ZPO liest den Wert beim Verbindungstest aus und zeigt ihn an, ändert ihn aber nicht ungefragt — er gilt für den ganzen Server, nicht nur für ZPOs Einträge.