opencode auf srv-r2d2 meldete nur: “Cannot connect to API” (api.anthropic.com). Erster Reflex: Problem auf dem Host selbst. War es nicht. DNS war für das gesamte LAN tot — und niemand hatte bis dahin etwas gemerkt, weil interne Zonen weiter funktionierten.

TL;DR:

  • Suricata-Logs (eve.json 7,1G, alerts.log 1,2G) liefen seit der Installation komplett ungebremst — die Root-Partition der Firewall (UFS, 14G) lief auf 107% voll, die UFS-Minfree-Reserve war angefressen.
  • Ein gesetztes Log-Größenlimit allein hätte nichts gebracht: enable_log_mgmt fehlte als eigener Config-Key komplett — ohne ihn ist der komplette Rotations-Codepfad tot, egal welche Limits daneben stehen.
  • unbound crashte dadurch minütlich (No space left on device), und pfBlockerNGs NAT-Redirect zwang jede LAN-DNS-Anfrage auf die Firewall-IP selbst — der lokale Fehler wurde so zum LAN-weiten Totalausfall.

Das Problem

Symptom auf dem Laptop/srv-r2d2: externe DNS-Namen lösen nicht mehr auf.

dig @10.99.99.1 api.anthropic.com +time=3
;; connection timed out; no servers could be reached

Interne AD-Zone dagegen geht:

dig @10.99.99.25 darkside.local SOA +short
dc-vader.darkside.local. hostmaster.darkside.local. ...

Das ist der entscheidende Hinweis: externe Namen tot, interne Zone (von dc-vader autoritativ beantwortet) lebt. Kein generischer DNS-Ausfall — irgendwas zwischen Resolver und Internet.

Die Diagnose

Erster Check direkt auf der Firewall (fw-dagobah, pfSense):

ssh root@10.99.99.1 'df -h /; tail -20 /var/log/resolver.log'
Filesystem  Size  Used  Avail  Capacity  Mounted on
/dev/ufs/   14G   14G   -922M    107%    /
fatal error: could not open autotrust file for writing ... No space left on device

Bad: Capacity über 100%, negativer Avail-Wert — die UFS-Minfree-Reserve ist angeknabbert. unbound kann seine Autotrust-Datei nicht mehr schreiben und stirbt daran.

Zweiter Check: Verursacher und Rotation-Config:

ssh root@10.99.99.1 '
du -sh /var/log/suricata/suricata_vtnet0/eve.json \
       /var/log/suricata/suricata_vtnet0/alerts.log
grep -n "enable_log_mgmt\|eve_log_limit_size" /conf/config.xml
ls /var/unbound/root.key.* | wc -l
'
7.1G    eve.json
1.2G    alerts.log
(kein Treffer für enable_log_mgmt oder eve_log_limit_size)
674

Bad → Good-Vergleich: grep auf enable_log_mgmt liefert hier keine Zeile — der Key fehlt komplett in der config.xml, nicht nur auf off gesetzt. Dazu 674 verwaiste root.key.<pid>-*-Leichen aus gescheiterten Autotrust-Schreibversuchen.

Erfolgskriterium Diagnose: externe Query failt, interne Zone geht, / voll, Resolver-Log zeigt ENOSPC.

Die Ursache

Vier Stufen, jede einzeln per SSH verifiziert:

  1. eve.json (7,1G) und alerts.log (1,2G) wuchsen seit der Suricata-Installation komplett ungebremst. Die pfSense-Suricata-“Logs Mgmt”-Seite war nie gespeichert worden.
  2. Die Root-Partition (/, UFS, 14G) erreichte 107% — die UFS-Reserve war weg.
  3. unbound crashte dadurch minütlich neu (No space left on device). Der pfSense-eigene Service-Watchdog (Cron alle 60 Sekunden) versuchte pflichtgemäß, den Dienst neu zu starten — erfolglos, weil die Platte voll blieb. Kein Watchdog-Defekt: Er hat die ganze Zeit korrekt gearbeitet, der Blocker war einzig der volle Datenträger.
  4. pfBlockerNGs NAT-Redirect-Regel (rdr ... port=domain -> 10.99.99.1) zwingt jede LAN-DNS-Anfrage auf die Firewall-IP selbst. Ohne laufenden unbound liefen alle Queries ins Leere — dc-vader als Haupt-DNS (Forwarder = Firewall) bekam SERVFAIL/Timeout für jede externe Domain. Interne Zonen blieben erreichbar, weil dc-vader sie selbst autoritativ beantwortet.

Das Perfide: Ein reines Log-Größenlimit (eve_log_limit_size) hätte hier gar nichts gebracht, selbst wenn es gesetzt gewesen wäre. Der Rotations-Codepfad in suricata_check_cron_misc.inc liegt komplett innerhalb einer if (enable_log_mgmt)-Bedingung. Ohne diesen separaten Flag-Key läuft das Cron-Skript alle 5 Minuten brav durch — und tut nichts. Keine Fehlermeldung, kein Hinweis, einfach stille Inaktivität seit der Installation. Zwei fehlende Config-Keys, die sich wie einer anfühlen, aber unabhängig voneinander gesetzt sein müssen.

Und ein zweiter Fallstrick, der fast in die Lösung eingebaut worden wäre: Der GUI-Default für eve_log_limit_size liegt bei 5 MB. Ein täglicher Security-Report-Cron liest tail -50000 eve.json ohne jedes Rotations-Bewusstsein — bei 5-MB-Rotation wäre das Live-File nur Stunden alt, der Report hätte morgens “keine Alerts” gemeldet, obwohl er schlicht blind war. Ein stiller Ausfall, der monatelang unbemerkt geblieben wäre, hätte man den Default blind übernommen.

Die Lösung

Akutfix zuerst — Platz schaffen, DNS zurückholen:

ssh root@10.99.99.1 '
# 1. Backup vor jedem Schreibzugriff
cp /conf/config.xml /root/config.xml.bak-diskfull-$(date +%Y%m%d-%H%M%S)

# 2. Platz schaffen — NICHT rm (Suricata hält das File-Handle offen,
#    rm gibt den Speicher erst beim Prozessende frei)
truncate -c -s 0 /var/log/suricata/suricata_vtnet0/eve.json
truncate -c -s 0 /var/log/suricata/suricata_vtnet0/alerts.log

# 3. Autotrust-Leichen entfernen (nicht die echte root.key)
find /var/unbound -name "root.key.*" -delete
unbound-checkconf   # erwartet: no errors
df -h /
'

unbound danach pfSense-nativ neu starten — nicht generisch:

ssh root@10.99.99.1 '/usr/local/sbin/pfSsh.php playback svc restart unbound'

# Echte Config aktiv?
ssh root@10.99.99.1 'ps auxww | grep "[u]nbound"'
# erwartet: -c /var/unbound/unbound.conf
dig @10.99.99.1 doubleclick.net +short
# erwartet bei aktivem pfBlockerNG-DNSBL: Sinkhole-IP, NICHT die echte IP

dig @10.99.99.1 api.anthropic.com +short
# erwartet: echte öffentliche A-Records

Falsch wäre gewesen: service unbound restart. Das startet unbound ohne pfSense-generierte Config — kein DNSBL, keine Forwarder. ps sieht danach gesund aus. Falscher Grün-Test bei kaputter Firewall.

Danach die nachhaltige Rotation — Wachstum gemessen (~24 MB/Tag bei aktiven Event-Types alert/drop/dns/tls, kein flow/http/netflow), nicht den GUI-Default übernommen:

ssh root@10.99.99.1 'php -r "
require_once(\"config.inc\");
config_set_path(\"installedpackages/suricata/config/0/enable_log_mgmt\", \"on\");
config_set_path(\"installedpackages/suricata/config/0/eve_log_limit_size\", \"300000\");
config_set_path(\"installedpackages/suricata/config/0/eve_log_retention\", \"720\");
config_set_path(\"installedpackages/suricata/config/0/alert_log_limit_size\", \"500\");
config_set_path(\"installedpackages/suricata/config/0/alert_log_retention\", \"336\");
config_set_path(\"installedpackages/suricata/config/0/block_log_limit_size\", \"500\");
config_set_path(\"installedpackages/suricata/config/0/block_log_retention\", \"336\");
config_set_path(\"installedpackages/suricata/config/0/http_log_limit_size\", \"1000\");
config_set_path(\"installedpackages/suricata/config/0/http_log_retention\", \"168\");
config_set_path(\"installedpackages/suricata/config/0/stats_log_limit_size\", \"500\");
config_set_path(\"installedpackages/suricata/config/0/stats_log_retention\", \"168\");
config_set_path(\"installedpackages/suricata/config/0/tls_log_limit_size\", \"500\");
config_set_path(\"installedpackages/suricata/config/0/tls_log_retention\", \"336\");
config_set_path(\"installedpackages/suricata/config/0/sid_changes_log_limit_size\", \"250\");
config_set_path(\"installedpackages/suricata/config/0/sid_changes_log_retention\", \"336\");
config_set_path(\"installedpackages/suricata/config/0/pkt_capture_file_retention\", \"168\");
config_set_path(\"installedpackages/suricata/config/0/suricataloglimit\", \"on\");
\$freeKB = intval(exec(\"df -k /var | grep -v Filesystem | awk \x27{print \\\$4}\x27\"));
\$limitMB = strval(round(\$freeKB * 0.20 / 1024));
config_set_path(\"installedpackages/suricata/config/0/suricataloglimitsize\", \$limitMB);
config_set_path(\"installedpackages/suricata/config/0/file_store_limit_size\", strval(intval(\$limitMB * 0.60)));
write_config(\"Suricata Logs Mgmt aktiviert\");
echo \"suricataloglimitsize=\$limitMB MB\nOK\n\";
"'

300 MB statt der 5-MB-GUI-Default für eve_log_limit_size — bei ~24 MB/Tag Wachstum ergibt das rund 12 Tage Puffer, damit der tägliche Report-Cron nie wieder auf ein frisch rotiertes, fast leeres File trifft. Zusätzlich ein globales Directory-Limit (suricataloglimit=on + Größe nach pfSense-eigener 20%-frei-Formel) als Backstop, falls einzelne Log-Typen aus dem Ruder laufen.

Pflicht: enable_log_mgmt=on muss gesondert gesetzt werden. Ohne diesen Key bleibt die gesamte Konfiguration oben wirkungslos.

End-to-End verifiziert: Der Rotation-Code wurde tatsächlich ausgelöst, nicht nur die Config geschrieben.

ssh root@10.99.99.1 '
php-cgi -f /usr/local/pkg/suricata/suricata_check_cron_misc.inc
grep -i suricata /var/log/system.log | tail -5
ps aux | grep "[s]uricata"
ls -la /var/log/suricata/suricata_vtnet0/eve.json
'

Ergebnis: drei längst überfällige Altbestände (block.log, http.log, tls.log) wurden sofort rotiert, der Suricata-Prozess (PID unverändert) überlebte das ausgelöste SIGHUP, neue Log-Dateien empfingen sofort frische Daten. eve.json selbst rotierte korrekt nicht (286 KB, weit unter dem 300-MB-Limit) — das Limit greift erst, wenn es gebraucht wird.

Lessons Learned

  • Ein Log-Größenlimit ist wirkungslos ohne den Enable-Flag daneben. enable_log_mgmt und eve_log_limit_size sind zwei unabhängige Config-Keys. Nach jeder Suricata-Installation/jedem Reinstall beide einzeln prüfen — die “Logs Mgmt”-Seite im GUI muss aktiv gespeichert worden sein, nicht nur sichtbar.
  • GUI-Defaults nicht blind übernehmen. 5 MB für eve.json klingt harmlos, hätte aber den täglichen Security-Report lautlos blind gemacht, weil der Report-Cron ohne Rotationsbewusstsein liest. Erst Wachstumsrate messen, dann Limit setzen.
  • rm ist bei laufendem Suricata-Prozess der falsche Befehl. Der Prozess hält das File-Handle offen — rm gibt den Speicher erst beim Prozessende frei. truncate -c -s 0 auf denselben Pfad wirkt sofort.
  • Ein generischer Service-Restart ist auf pfSense ein falscher Grün-Test. service unbound restart startet ohne die pfSense-generierte Config (kein DNSBL, keine Forwarder) — sieht in ps gesund aus, ist es aber nicht. Nur pfSsh.php playback svc restart unbound lädt die echte Config.
  • Ein lokaler Fehler auf der Firewall kann durch NAT-Redirects zum LAN-weiten Ausfall werden. pfBlockerNGs Redirect zwingt jede DNS-Anfrage auf die Firewall-IP — fällt dort der Resolver aus, sind alle Clients betroffen, nicht nur die Firewall selbst.
  • Offen geblieben: Das Zabbix-Item für freien Plattenplatz lieferte über rund 1,4 Stunden hinweg den validen Wert 0,0% frei — der Trigger feuerte trotz eindeutig unterschrittener Schwelle nie. Monitoring, das in der Theorie korrekt verdrahtet ist, ist kein Ersatz für einen echten End-to-End-Test der Alarmkette.

Verwandt: pfSense Suricata: Wenn das IPS die eigene WAN-IP blockiert und Suricata blockiert eigenen Smarthost — 7 Tage Mailausfall — dieselbe Firewall, dieselbe Lehre in Variationen: Suricata braucht aktive Pflege, nicht nur eine einmalige Installation.

Checkliste

  • Nach jeder Suricata-Installation/jedem Update: grep enable_log_mgmt /conf/config.xml prüfen — Key vorhanden und on?
  • Log-Größenlimits nicht aus dem GUI-Default übernehmen — erst Wachstumsrate pro Log-Typ messen (du -sh, 20–30 Minuten Abstand).
  • df -h / auf allen Firewalls/Routern mit Suricata-Logging regelmäßig prüfen, nicht nur bei Alarm.
  • Bei vollem Datenträger: truncate -c -s 0, nie rm, auf Dateien mit offenem File-Handle.
  • unbound/pfSense-Dienste nach Eingriffen immer pfSense-nativ neu starten (pfSsh.php playback svc restart <dienst>), nie generisches service restart.
  • Nach Config-Änderung End-to-End testen: Rotation-Skript manuell auslösen, nicht nur grep auf die Config.
  • NAT-Redirect-Regeln (z. B. pfBlockerNG DNS-Redirect) als Verstärker-Risiko im Kopf behalten — ein lokaler Fehler wird so zum flächendeckenden Ausfall.
  • Monitoring-Trigger für genau diesen Fall mindestens einmal live testen (Schwelle kurzzeitig auslösen, Alarmzustellung bestätigen) — korrekt konfiguriert heißt nicht, dass er auch feuert.