Täglich. Um eine ähnliche Uhrzeit. Die komplette Infrastruktur hinter dem Gateway: im Koma.
Kein Ping, kein SSH, keine Webdienste. pfSense selbst lief noch — nur der Traffic kam nicht mehr durch. Nach einem Neustart des Suricata-Dienstes war alles wieder da. Bis zum nächsten Tag.
Das Muster war zu regelmäßig für einen echten Angriff. Und zu selektiv für einen Hardware-Fehler.
Das Problem
Suricata auf pfSense 2.8.1, Legacy IPS Mode (pcap), WAN-Interface. Konfiguriert mit:
blockoffenders7: on
blockoffenderskill: on
blockoffendersip: both
blockoffendersip: both bedeutet: Suricata blockiert sowohl Quell- als auch Ziel-IP eines verdächtigen Flows. Das klingt nach guter Idee — ist es auch, solange deine eigene WAN-IP nicht in einem Alert als Ziel auftaucht.
Und genau das passierte. Suricata erkannte eingehenden Traffic von einer verdächtigen Quelle, der an die WAN-IP des Gateways adressiert war. Quell-IP: blockiert. Ziel-IP (= eigene WAN-IP 203.0.113.28): ebenfalls blockiert.
Ergebnis: Das Gateway hat sich selbst aus dem Internet ausgesperrt.
Die Diagnose
Erster Check: die snort2c-Tabelle. Das ist die pf-Tabelle in die Suricata (und Snort) verdächtige IPs schreibt:
pfctl -t snort2c -T show
Ausgabe enthielt unter anderem:
203.0.113.28 # ← eigene WAN-IP
Da war sie. Die eigene WAN-IP, geblockt durch Suricata, mitten in der Block-Tabelle.
Zweiter Check: Passlist. Suricata hat einen Passlist-Mechanismus — IPs auf der Passlist werden nie geblockt, egal was die Rules sagen. Die Konfiguration in config.xml zeigte:
<passlistname>passlist_21324</passlistname>
Das Problem: passlist_21324 existierte nicht. Das war ein alter Snort-Rest aus der Migration — der ursprüngliche Snort-Identifier, der nie in die Suricata-Konfiguration übernommen wurde. Der korrekte Name wäre passlist_suricata gewesen.
Suricata hat die kaputte Referenz stillschweigend ignoriert und ohne Passlist gearbeitet.
Dritter Check: laufende Prozesse.
ps aux | grep -E "snort|suricata" | grep -v grep
Beide liefen. Snort und Suricata — gleichzeitig. Beide schreiben in dieselbe snort2c-Tabelle. Ein Migrationsrest, den ich nicht sauber aufgeräumt hatte.
Die drei Ursachen
1. blockoffendersip: both auf einem exposed VPS
Auf einem Gateway der direkt im Internet hängt ist blockoffendersip: both eine Falle. Jeder eingehende Alert dessen Ziel die WAN-IP ist führt dazu, dass die WAN-IP selbst geblockt wird. Die eigene IP landet in der Block-Tabelle und damit ist Game Over.
Auf einem LAN-Gateway mit RFC-1918-Adressen intern wäre das weniger ein Problem — die interne IP käme nicht in den Alerts als Angriffsziel vor. Auf einem Hetzner VPS mit öffentlicher WAN-IP auf allen Interfaces: Zeitbombe.
2. Kaputte Passlist-Referenz
Die Passlist hätte das abgefangen. Wenn die WAN-IP auf der Passlist steht, blockiert Suricata sie nie — egal was die Rules sagen. Aber passlist_21324 war ein Ghost-Reference aus der Snort-Zeit. Suricata fand keine Liste, verwendete keine Liste, und blockierte fröhlich alles was in einem Alert auftauchte.
Dieser Fehler ist schwer zu sehen. Suricata loggt keinen Fehler wenn eine referenzierte Passlist nicht existiert. Es startet einfach ohne.
3. Snort lief parallel
Snort und Suricata teilen sich die snort2c pf-Tabelle. Beide können IPs eintragen, beide können IPs löschen wenn ihre interne Blocklist abläuft. Das führt zu unvorhersehbarem Verhalten — Suricata räumt eine IP auf, Snort trägt sie wieder ein (oder umgekehrt). Zwei konkurrierende Prozesse, eine Tabelle, kein Koordinationsmechanismus.
Die Lösung
Vier Schritte, in dieser Reihenfolge. Das Ziel ist nicht Alert-Only — sondern Blocking mit blockoffendersip: src und einer korrekt konfigurierten Passlist.
Schritt 1: Passlist vervollständigen
Die eigentliche Root Cause: Die Passlist war leer. wanaddr, lanaddr, vpnaddr, wangw — alles nicht gesetzt. Suricata hat ohne Whitelist gearbeitet und konnte jede IP blocken, inklusive der eigenen.
Via PHP in der pfSense-Shell:
<?php
require_once("config.inc");
require_once("suricata/suricata_defs.inc");
require_once("suricata/suricata.inc");
// WICHTIG: $config muss global sein — write_config() liest die globale Variable
parse_config(true);
global $config;
// Passlist: WAN, LAN, VPN, Gateway automatisch whitelisten
foreach ($config['installedpackages']['suricata']['passlist']['item'] as &$pl) {
if ($pl['name'] === 'passlist_suricata') {
$pl['wanaddr'] = 'yes'; // WAN-IP automatisch whitelisten
$pl['wangw'] = 'yes'; // WAN-Gateway whitelisten
$pl['lanaddr'] = 'yes'; // LAN-Subnet whitelisten
$pl['vpnaddr'] = 'yes'; // VPN-IPs whitelisten
echo "Passlist vervollständigt: wanaddr, wangw, lanaddr, vpnaddr = yes\n";
}
}
unset($pl);
write_config("Suricata: Passlist vervollständigt (WAN+LAN+VPN+GW)");
$rebuild_rules = true;
sync_suricata_package_config();
echo "DONE\n";
?>
php /tmp/suricata_passlist_fix.php
Wichtige PHP-Falle: write_config() liest die globale $config-Variable — nicht eine lokale Kopie. Wenn du parse_config(true) aufrufst ohne danach global $config zu deklarieren, speichert write_config() den alten Zustand.
Zweite Falle: Nach write_config() muss sync_suricata_package_config() aufgerufen werden. Sonst ist config.xml aktualisiert, aber Suricata läuft noch mit der alten Konfiguration. $rebuild_rules = true ist dabei Pflicht — sonst werden nur Teile der Konfiguration synchronisiert.
Schritt 2: Passlist-Referenz fixen
Wenn du von Snort migriert hast, kann die Passlist-Referenz auf einen alten Snort-Identifier zeigen (passlist_21324 statt passlist_suricata). Suricata ignoriert die kaputte Referenz lautlos — kein Error, kein Warning, einfach keine Passlist aktiv.
<?php
require_once("config.inc");
require_once("suricata/suricata_defs.inc");
require_once("suricata/suricata.inc");
parse_config(true);
global $config;
// Referenz in WAN-Interface korrigieren
foreach ($config['installedpackages']['suricata']['rule'] as &$rule) {
if ($rule['interface'] === 'wan') {
$old = $rule['passlistname'] ?? '(keine)';
$rule['passlistname'] = 'passlist_suricata';
echo "Passlist: $old → passlist_suricata\n";
}
}
unset($rule);
write_config("Suricata WAN: Passlist-Referenz korrigiert");
$rebuild_rules = true;
sync_suricata_package_config();
echo "DONE\n";
?>
Schritt 3: blockoffendersip auf src umstellen
Selbst mit vollständiger Passlist ist both auf einem exposed VPS riskant. Ein Suricata-Alert bei dem deine WAN-IP als Ziel auftaucht — und sie steht in der Block-Tabelle. Mit src wird nur die Quell-IP (= der Angreifer) blockiert, nie die Ziel-IP.
<?php
require_once("config.inc");
require_once("suricata/suricata_defs.inc");
require_once("suricata/suricata.inc");
parse_config(true);
global $config;
foreach ($config['installedpackages']['suricata']['rule'] as &$rule) {
if ($rule['interface'] === 'wan') {
$rule['blockoffendersip'] = 'src';
echo "blockoffendersip: both → src\n";
}
}
unset($rule);
write_config("Suricata: blockoffendersip src (nur Quell-IP blocken)");
$rebuild_rules = true;
sync_suricata_package_config();
echo "DONE\n";
?>
Zusätzlich: Eigene Server-IPs die von extern auf das Gateway zugreifen (z.B. OpenVPN-Clients) in einen pfSense-Alias packen und diesen in der Passlist referenzieren:
Firewall → Aliases → snort_allow → eigene Server-IPs eintragen
Suricata → Passlist → snort_allow als Adress-Quelle hinzufügen
Schritt 4: Snort deinstallieren
Solange Snort installiert ist, kann es in die snort2c-Tabelle schreiben — auch wenn es nicht aktiv IDS macht. Die sauberste Lösung ist vollständige Deinstallation.
GUI: System → Package Manager → Installed Packages → snort → Remove
Oder via Shell:
pkg delete pfSense-pkg-snort
Nach der Deinstallation: snort2c-Tabelle leeren und alle blockierten IPs freigeben:
pfctl -t snort2c -T flush
echo "snort2c geleert: $(pfctl -t snort2c -T show | wc -l) Einträge verbleibend"
Achtung: snortglobal und suricata sind separate Sektionen in config.xml. Die Deinstallation von Snort entfernt die snortglobal-Sektion — die suricata-Sektion bleibt unangetastet. Keine Suricata-Konfiguration geht verloren.
Das Ergebnis: IPS mit korrekter Passlist
Nach dem Fix läuft Suricata im IPS-Modus mit drei Schutzschichten gegen Selbstblockade:
Schicht 1: blockoffendersip: src — nur Quell-IPs werden blockiert, nie Ziel-IPs. Das allein verhindert schon, dass die eigene WAN-IP in der Block-Tabelle landet.
Schicht 2: Passlist mit automatischen Adressen:
| Passlist-Eintrag | Effekt |
|---|---|
wanaddr: yes | WAN-IP wird nie geblockt |
wangw: yes | WAN-Gateway wird nie geblockt |
lanaddr: yes | LAN-Subnet wird nie geblockt |
vpnaddr: yes | VPN-IPs (WireGuard) werden nie geblockt |
Schicht 3: snort_allow Alias — eigene Server-IPs die von extern auf das Gateway zugreifen (OpenVPN-Clients, andere Hetzner-Server). Manuell gepflegt, in der Passlist referenziert.
Zusammen mit pfBlockerNG ergibt das ein vollständiges Defense-in-Depth Setup:
| Schicht | Mechanismus | Status |
|---|---|---|
| DNS-Ebene | pfBlockerNG DNSBL (645k Domains) | aktiv, blockend |
| IP-Ebene | pfBlockerNG IP Blocklist | aktiv, blockend |
| IDS/IPS | Suricata (src-only + Passlist + snort_allow) | aktiv, blockend |
Die snort2c-Tabelle füllt sich mit externen Angreifer-Quell-IPs — eigene Infrastruktur bleibt unangetastet.
Verifikation
# snort2c-Tabelle prüfen — eigene WAN-IP darf nicht drinstehen:
pfctl -t snort2c -T show | grep "203.0.113.28"
# → keine Ausgabe = gut
# blockoffendersip auf src?
grep blockoffendersip /conf/config.xml
# → nur "src" in der Suricata-Sektion
# Alerts werden geloggt?
tail -f /var/log/suricata/suricata_vtnet0*/eve.json | jq 'select(.event_type=="alert") | {src: .src_ip, dest: .dest_ip, sig: .alert.signature}'
# Snort wirklich weg?
ps aux | grep snort | grep -v grep
# → keine Ausgabe
pkg info | grep snort
# → keine Ausgabe
Lessons Learned
blockoffendersip: src, nichtboth. Auf einem exposed VPS mit öffentlicher WAN-IP istbotheine Zeitbombe — jeder eingehende Alert blockiert auch die Ziel-IP, und das ist deine eigene WAN-IP. Mitsrcblockst du nur den Angreifer. Die Passlist (wanaddr, lanaddr, vpnaddr, wangw) ist die zweite Absicherung, und einsnort_allow-Alias für eigene Server-IPs die dritte.Kaputte Passlist-Referenzen werden lautlos ignoriert. Suricata loggt keinen Fehler wenn
passlistnameauf eine nicht existierende Liste zeigt. Immer nach Migration prüfen: Existiert die referenzierte Liste wirklich?grep passlistname /conf/config.xmlund dann gegen die tatsächlich vorhandenen Listen abgleichen.Die Passlist kann korrekt referenziert sein und trotzdem leer.
passlistname: passlist_suricatazeigt auf die richtige Liste — aber wennwanaddr,lanaddretc. nicht aufyesstehen, ist die Liste effektiv leer. Zwei Fehler, die sich gegenseitig verstärken.snortglobalundsuricatasind separate Config-Sektionen. Wer von Snort auf Suricata migriert, muss aktiv sicherstellen dass Snort vollständig deinstalliert ist — nicht nur gestoppt. Gestoppt != weg. Beide schreiben in dieselbesnort2c-Tabelle.write_config()braucht die globale$config. Wer in PHP-Skripten auf pfSense die Konfiguration ändert:parse_config(true); global $config;— in dieser Reihenfolge. Danach zwingend$rebuild_rules = true; sync_suricata_package_config()— sonst ist config.xml aktualisiert aber der laufende Dienst weiß nichts davon.