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-EintragEffekt
wanaddr: yesWAN-IP wird nie geblockt
wangw: yesWAN-Gateway wird nie geblockt
lanaddr: yesLAN-Subnet wird nie geblockt
vpnaddr: yesVPN-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:

SchichtMechanismusStatus
DNS-EbenepfBlockerNG DNSBL (645k Domains)aktiv, blockend
IP-EbenepfBlockerNG IP Blocklistaktiv, blockend
IDS/IPSSuricata (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

  1. blockoffendersip: src, nicht both. Auf einem exposed VPS mit öffentlicher WAN-IP ist both eine Zeitbombe — jeder eingehende Alert blockiert auch die Ziel-IP, und das ist deine eigene WAN-IP. Mit src blockst du nur den Angreifer. Die Passlist (wanaddr, lanaddr, vpnaddr, wangw) ist die zweite Absicherung, und ein snort_allow-Alias für eigene Server-IPs die dritte.

  2. Kaputte Passlist-Referenzen werden lautlos ignoriert. Suricata loggt keinen Fehler wenn passlistname auf eine nicht existierende Liste zeigt. Immer nach Migration prüfen: Existiert die referenzierte Liste wirklich? grep passlistname /conf/config.xml und dann gegen die tatsächlich vorhandenen Listen abgleichen.

  3. Die Passlist kann korrekt referenziert sein und trotzdem leer. passlistname: passlist_suricata zeigt auf die richtige Liste — aber wenn wanaddr, lanaddr etc. nicht auf yes stehen, ist die Liste effektiv leer. Zwei Fehler, die sich gegenseitig verstärken.

  4. snortglobal und suricata sind 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 dieselbe snort2c-Tabelle.

  5. 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.