Snort 2.9.x ist tot. Nicht “wird eingestellt” — tot. Der pfSense-Package-Manager zeigt noch munter snort-2.9.20_8 an, aber die Regeln werden nicht mehr gepflegt und VRT Subscriber-Regeln für 2.9 sind längst eingefroren.

Suricata ist der logische Nachfolger. Aber die Migration ist nicht so trivial wie “Suricata installieren, Snort deinstallieren, fertig” — zumindest nicht wenn du die Konfiguration nicht manuell durch 47 GUI-Screens klicken willst.

Hier ist wie ich es auf einer pfSense 2.8.1 (FreeBSD) vollautomatisch via PHP-API erledigt habe.


Ausgangslage

  • pfSense 2.8.1-RELEASE (FreeBSD), cloud VPS
  • Snort 2.9.20_8 im IDS-Mode (passiv, kein Inline)
  • ET Open + Snort VRT Subscriber Regeln (Oinkcode vorhanden)
  • Bestehende Suppress-Einträge die migriert werden müssen
  • WAN Interface: virtio NIC (vtnet0)

Ziel: Suricata im Legacy IPS Mode auf WAN, Block Offenders aktiv, Suppress-Liste migriert, 0 Klicks in der GUI.


Warum Legacy IPS und kein Inline Mode?

Kurze Antwort: VPS + virtio NIC = kein Netmap.

Inline IPS auf pfSense nutzt DPDK/Netmap für kernel-bypass — das braucht hardware-native NIC-Treiber. Virtio-NICs auf Hetzner, DigitalOcean & Co. unterstützen das nicht. Wer es trotzdem versucht, riskiert kompletten Netzwerkausfall.

Legacy IPS (pcap-basiert) ist die richtige Wahl für VPS-Umgebungen. Suricata liest Pakete via libpcap, schreibt verdächtige Quell-/Ziel-IPs in die pf-Tabelle snort2c, und pfSense blockiert über normale Firewall-Regeln. Kein Netmap, kein Kernel-Bypass, kein Drama.


Schritt 1: Suricata installieren + Regelwerk runterladen

GUI: System → Package Manager → Available Packages → suricata → Install

Dann Global Settings konfigurieren:

EinstellungWert
Install ETOpen Emerging Threats rules
Install Snort VRT rules
Snort Oinkmaster Code<DEIN-OINKCODE>
Update Interval12 Hours
Update Start Time00:30
Live Rule Swap on Update

Update klicken und Regelwerk einmalig runterladen (dauert 1-3 Min).

Das war der einzige GUI-Klick in dieser Anleitung. Ab jetzt: PHP.


Schritt 2: Suppress-Liste migrieren

Das gute zuerst: Das Suppress-Format ist zwischen Snort und Suricata identisch. Kein Umschreiben, kein Konvertieren.

# Von Snort exportieren:
ssh root@<DEIN-GATEWAY>
cat /usr/local/etc/snort/snort_*/threshold.conf

Suricata erwartet die Suppress-Liste allerdings in der config.xml, base64-encoded. Das per PHP erledigen:

<?php
require_once("config.inc");

$config = parse_config(true);

$suppress_content = <<<'SUPPRESS'
# HTTP-Inspect Noise
suppress gen_id 120, sig_id 3

# Bekannter Scanner
suppress gen_id 1, sig_id 2003068, track by_src, ip 192.168.66.42

# Komplett supprimieren
suppress gen_id 1, sig_id 2100498
SUPPRESS;

$uuid = uniqid("suppress_");
$item = [
    "uuid"             => $uuid,
    "name"             => $uuid,
    "descr"            => "Migriert von Snort",
    "suppresspassthru" => base64_encode($suppress_content),
];

if (!isset($config['installedpackages']['suricata']['suppress']['item'])) {
    $config['installedpackages']['suricata']['suppress'] = ['item' => []];
}
$config['installedpackages']['suricata']['suppress']['item'][] = $item;

// WAN Interface auf neue Liste setzen
foreach ($config['installedpackages']['suricata']['rule'] as &$rule) {
    if ($rule['interface'] === 'wan') {
        $rule['suppresslistname'] = $uuid;
    }
}
unset($rule);

write_config("Suricata: Suppress-Liste migriert");
echo "UUID: $uuid\n";
?>
# Ausführen:
php /tmp/suricata_suppress.php

Schritt 3: Interface + Rulesets konfigurieren

Das ist wo die meiste Arbeit steckt — und die meisten Fallen lauern.

<?php
require_once("config.inc");
require_once("suricata/suricata_defs.inc");   // ← PFLICHT
require_once("suricata/suricata.inc");         // ← PFLICHT (nicht services.inc!)

$config = parse_config(true);

// Backup
copy("/conf/config.xml", "/conf/config.xml.bak." . date("Ymd_His"));

$et_rules = [
    "emerging-attack_response.rules",
    "emerging-botcc.rules",
    "emerging-compromised.rules",
    "emerging-current_events.rules",
    "emerging-dos.rules",
    "emerging-dshield.rules",
    "emerging-drop.rules",
    "emerging-exploit.rules",
    "emerging-malware.rules",
    "emerging-phishing.rules",
    "emerging-scan.rules",
    "emerging-shellcode.rules",
    "emerging-sql.rules",
    "emerging-tor.rules",
    "emerging-web_client.rules",
    "emerging-web_server.rules",
    "emerging-worm.rules",
];

$snort_rules = [
    "snort_botnet-cnc.rules",
    "snort_dos.rules",
    "snort_exploit.rules",
    "snort_malware-cnc.rules",
    "snort_malware-other.rules",
    "snort_scan.rules",
    "snort_server-webapp.rules",
    "snort_sql.rules",
    "snort_virus.rules",
    "snort_web-attacks.rules",
];

$existing = explode("||", $config['installedpackages']['suricata']['rule'][0]['rulesets'] ?? "");
$all_rules = array_unique(array_merge($existing, $et_rules, $snort_rules));
$all_rules = array_filter($all_rules);
sort($all_rules);

foreach ($config['installedpackages']['suricata']['rule'] as &$rule) {
    if ($rule['interface'] === 'wan') {
        $rule['rulesets']   = implode("||", $all_rules);
        $rule['killstates'] = 'on';
        $rule['blockip']    = 'both';
        $rule['enable']     = 'on';
        echo "SET: " . count($all_rules) . " Rulesets, killstates=on, blockip=both\n";
    }
}
unset($rule);

write_config("Suricata WAN: Rulesets gesetzt");

// ← DAS ist die kritische Zeile (siehe Fallen unten)
$rebuild_rules = true;
sync_suricata_package_config();

echo "DONE\n";
?>
php /tmp/suricata_finalize.php

Die drei Fallen

Falle 1: Falsche include-Dateien

// FALSCH (nicht laden was nicht existiert):
require_once("suricata/suricata_generate_yaml.inc");

// RICHTIG:
require_once("suricata/suricata_defs.inc");
require_once("suricata/suricata.inc");

suricata_generate_yaml() ist direkt in suricata.inc enthalten. Es gibt keine eigene Datei dafür. Diesen Fehler zu debuggen kostet Zeit — der PHP-Error sagt nur “No such file” und Suricata läuft trotzdem.

Falle 2: Regeln werden nicht neu generiert

Nach write_config() ist die config.xml aktualisiert — aber Suricata nutzt sein eigenes Rule-File /var/db/suricata/suricata_vtnet0.rules. Das wird nicht automatisch aktualisiert.

// FALSCH (Regeln bleiben alt):
write_config("...");

// RICHTIG (Regeln neu generieren):
write_config("...");
$rebuild_rules = true;
sync_suricata_package_config();

Ohne $rebuild_rules = true und sync_suricata_package_config() hatte ich 389 Zeilen in der Rules-Datei (nur die internen Event-Rules) statt 39.000+. Suricata startet problemlos, erkennt aber fast nichts.

Falle 3: HOME_NET Dropdown ist leer

Suricata auf pfSense hat einen “IP List” Mechanismus für Reputation-Listen. Das ist nicht das gleiche wie HOME_NET. Wenn du eine eigene IP-Liste für HOME_NET erstellst, taucht sie im Dropdown nicht auf.

Lösung: Das default-Setting lassen. pfSense befüllt HOME_NET automatisch aus den konfigurierten Interface-Netzwerken. Wenn du zusätzliche Netze brauchst, fügst du sie als Custom IP List im Suricata Global Settings Reputation-Bereich hinzu — die fließen dann automatisch in default ein.


Schritt 4: Umschalten

# Snort stoppen
pkill -x snort
# oder: /usr/local/etc/rc.d/snort.sh stop

# Suricata starten (via PHP)
php -r "
require_once('config.inc');
require_once('suricata/suricata_defs.inc');
require_once('suricata/suricata.inc');
\$config = parse_config(true);
suricata_start(\$config['installedpackages']['suricata']['rule'][0]);
echo 'Started\n';
"

# Läuft?
ps aux | grep suricata | grep -v grep

Detection-Gap: ~10-30 Sekunden. Kein Traffic-Ausfall, nur die IDS-Überwachung pausiert kurz.


Verifikation

# Regeln geladen?
grep "rules loaded" /var/log/suricata/suricata_vtnet0*/suricata.log

# Pakete werden dekodiert?
tail -5 /var/log/suricata/suricata_vtnet0*/stats.log | grep decoder.pkts

# Test-Alert generieren:
curl -s http://testmynids.org/uid/index.html > /dev/null

# Alert geloggt? (SID 2100498 = NIDS Test Rule)
grep "2100498" /var/log/suricata/suricata_vtnet0*/eve.json | tail -3

Nach wenigen Minuten tauchen echte Alerts auf — TOR Exit Nodes, Dshield Blocklist, Spamhaus. Das EVE JSON Format ist deutlich besser als Snorts Log-Format und lässt sich prima mit jq auswerten:

# Top 10 Angreifer-IPs:
cat /var/log/suricata/suricata_vtnet0*/eve.json | \
  jq -r 'select(.event_type=="alert") | .src_ip' | \
  sort | uniq -c | sort -rn | head -10

Rollback (falls nötig)

Snort bleibt installiert — das ist Absicht. Erst nach 7 Tagen Burn-In deinstallieren.

# Sofort-Rollback:
pkill suricata
/usr/local/etc/rc.d/snort.sh start
ps aux | grep -E "snort|suricata" | grep -v grep

Lessons Learned

  1. sync_suricata_package_config() mit $rebuild_rules = true ist nicht optional. Ohne diese Zeile läuft Suricata mit minimalem Ruleset und du merkst es erst wenn du die Regelanzahl prüfst.

  2. Legacy IPS ist die richtige Wahl für VPS. Inline/Netmap klingt besser, ist es aber nicht wenn die Hardware (virtio NIC) es nicht unterstützt.

  3. Das Suppress-Format ist 1:1 kompatibel. Snort threshold.conf → Suricata suppress → kein Umschreiben nötig.

  4. PHP statt GUI spart Stunden. Die Suricata-GUI auf pfSense hat Dutzende Pflichtfelder mit Placeholdern die man einzeln befüllen muss. Via PHP sind es 50 Zeilen Code die alles setzen.

  5. Burn-In einhalten. 7 Tage mit beiden Systemen installiert (Suricata aktiv, Snort gestoppt) bevor Snort deinstalliert wird. Falsch-Positiv-Rate muss erst kalibriert werden.