Ausgehende Mails kommen nicht an. Seit einer Woche. Niemand merkt es.

Kein Bounce, kein Alert. Der Absender sieht nichts. Der Empfänger sieht nichts. Irgendwo zwischen Postfix und dem Internet: Stille.


Das Symptom

Die Postfix-Queue auf dem Mail-Gateway wächst still vor sich hin:

postqueue -p
-Queue ID-  --Size-- ----Arrival Time---- -Sender/Recipient-------
EC2F61C747    79181 Mon Mar 10 15:23:39  luke@rebellion.local
   (connect to smtp.darkside.bz[192.168.66.76]:587: Connection timed out)
                                         leia@alderaan.de

Der Smarthost smtp.darkside.bz:587 — Connection timed out. Seit einer Woche.

Intern läuft alles: Zabbix-Mails kommen an, BCC-Kopien werden zugestellt. Nur nach draußen geht nichts. Postfix retried brav, alle paar Stunden, immer dasselbe Ergebnis.

Die Diagnose

Erster Reflex: Ist der Smarthost down? Ping vom Gateway:

ssh root@gw-todesstern
ping -c 2 192.168.66.76
2 packets transmitted, 0 received, 100% packet loss

Vom Gateway aus nicht erreichbar. Aber von einem anderen Server im selben Netzwerk:

ping -c 2 192.168.66.76
64 bytes from 192.168.66.76: icmp_seq=1 ttl=53 time=9.84 ms

Ping geht. Der Smarthost lebt. Nur Traffic durch die Firewall kommt nicht an.

snort2c — die stille Blocktabelle

pfSense nutzt die pf-Tabelle snort2c für Suricata-Blocks. Steht die Smarthost-IP drin?

pfctl -t snort2c -T test 192.168.66.76
1/1 addresses match.

Treffer. Suricata hat den eigenen Smarthost geblockt.

Welche Regel hat getriggert?

grep "192.168.66.76" /var/log/suricata/suricata_vtnet0*/eve.json | tail -3
{
  "alert": {
    "signature_id": 2260002,
    "signature": "SURICATA Applayer Detect protocol only one direction",
    "category": "Generic Protocol Command Decode",
    "severity": 3
  },
  "src_ip": "192.168.66.76",
  "dest_port": 587
}

SID 2260002: “Applayer Detect protocol only one direction.” Suricata erkennt bei der SMTP-STARTTLS-Verbindung das Protokoll nur in einer Richtung — ein klassischer False Positive bei asymmetrischen TLS-Handshakes.

Jeder einzelne Zustellversuch von Postfix triggert den Alert neu. Die IP wird sofort wieder geblockt. Ein Teufelskreis.

Die Überraschung: Passlist schützt nicht vor Blocking

Die Passlist war korrekt konfiguriert. Die Smarthost-IP stand drin:

cat /usr/local/etc/suricata/suricata_*/passlist
10.0.0.0/24
192.168.66.76   # Smarthost
...

Trotzdem wurde sie geblockt. Warum?

Im Legacy IPS pcap-Mode unterdrückt die Passlist nur die Alert-Generierung — sie verhindert NICHT das Eintragen in die snort2c-Blocktabelle.

Das ist der entscheidende Punkt: Im pcap-Mode liest Suricata den Traffic passiv mit. Die Blocking-Entscheidung und die Alert-Generierung sind zwei getrennte Pfade. Die Passlist wirkt nur auf den Alert-Pfad.

Im Inline-IPS-Mode (Netmap/NFQ) wäre die Passlist wirksam gewesen. Aber auf einem Hetzner VPS steht Inline-Mode nicht zur Verfügung — es bleibt nur pcap.

Der Fix — dreistufiger Schutz

Ein einzelner Fix reicht nicht. Im pcap-Mode braucht man mehrere Schutzebenen:

1. Suppress-Regel für die spezifische SID

Die Suppress-Regel verhindert, dass der Alert überhaupt erzeugt wird — kein Alert, kein Block-Kandidat:

suppress gen_id 1, sig_id 2260002, track by_src, ip 192.168.66.76

In pfSense ist die Suppress-Liste base64-encoded in der config.xml. Änderung per PHP:

require_once("config.inc");
require_once("suricata/suricata_defs.inc");
require_once("suricata/suricata.inc");
parse_config(true);
global $config;  // PFLICHT — write_config() nutzt globale Variable

$suppress = $config["installedpackages"]["suricata"]["suppress"]["item"][0]["suppresspassthru"];
$decoded = base64_decode($suppress);
$decoded .= "\nsuppress gen_id 1, sig_id 2260002, track by_src, ip 192.168.66.76\n";
$config["installedpackages"]["suricata"]["suppress"]["item"][0]["suppresspassthru"] = base64_encode($decoded);

write_config("Suppress SID 2260002 for smarthost");
$rebuild_rules = true;
sync_suricata_package_config();

Danach Suricata neu starten — die Suppress-Regel wird erst beim Neustart geladen:

kill $(pgrep suricata)
sleep 3
/usr/local/bin/suricata -c /usr/local/etc/suricata/suricata_*/suricata.yaml --pcap=vtnet0 -D

2. Cron-Guard: Passlist-IPs jede Minute aus snort2c entfernen

Der Fallback-Schutz — falls eine andere SID die IP doch wieder blockt:

#!/bin/sh
# /usr/local/bin/snort2c_passlist_guard.sh
PASSLIST="/usr/local/etc/suricata/suricata_*/passlist"

for f in $PASSLIST; do
    [ -f "$f" ] || continue
    while IFS= read -r line; do
        ip=$(echo "$line" | sed "s/#.*//" | tr -d " \t")
        [ -z "$ip" ] && continue
        result=$(pfctl -t snort2c -T test "$ip" 2>/dev/null)
        case "$result" in
            *"1/1"*)
                pfctl -t snort2c -T delete "$ip" 2>/dev/null
                echo "$(date +%Y-%m-%dT%H:%M:%S) UNBLOCKED: $ip" >> /var/log/snort2c_guard.log
                ;;
        esac
    done < "$f"
done

Als Cron-Job (jede Minute):

crontab -l
* * * * * /usr/local/bin/snort2c_passlist_guard.sh

Wichtig auf pfSense: Nicht in /etc/crontab eintragen — pfSense überschreibt diese Datei bei jeder Config-Änderung. Stattdessen crontab -e als root verwenden (User-Crontab in /var/cron/tabs/root).

3. Postfix: IPv4-Preference

Nebenbefund: Der Mail-Gateway hatte kein routbares IPv6. Postfix versuchte trotzdem zuerst IPv6, wartete 30 Sekunden auf den Timeout, und probierte erst dann IPv4. Das verdoppelte die Retry-Zeit.

postconf -e 'smtp_address_preference=ipv4'
postfix reload

Monitoring: Nie wieder 7 Tage Stille

Zabbix UserParameter: Kritische IPs in snort2c?

Auf der Firewall (zabbix_agentd.conf.d/):

UserParameter=custom.snort2c.critical_blocked,/usr/local/bin/check_snort2c_critical.sh

Das Script prüft eine Liste kritischer IPs gegen die snort2c-Tabelle:

#!/bin/sh
CRITICAL_IPS="192.168.66.76 192.168.66.133"
BLOCKED=0
for ip in $CRITICAL_IPS; do
    result=$(pfctl -t snort2c -T test $ip 2>/dev/null)
    case "$result" in
        *"1/1"*) BLOCKED=$((BLOCKED + 1)) ;;
    esac
done
echo $BLOCKED

Trigger: Wert > 0 → Disaster (Severity 5). Da gibt es nichts zu diskutieren — wenn eine kritische IP geblockt ist, brennt es.

Zabbix UserParameter: Smarthost erreichbar?

Auf dem Mail-Gateway:

UserParameter=custom.smarthost.reachable,timeout 5 bash -c 'echo QUIT | openssl s_client -connect smtp.darkside.bz:587 -starttls smtp 2>/dev/null | grep -c 250' || echo 0

Trigger: 3 aufeinanderfolgende Checks = 0 → High (Severity 4).

Deferred Queue Aging

UserParameter=custom.postfix.deferred.age,find /var/spool/postfix/deferred -type f -mmin +60 2>/dev/null | wc -l

Trigger: Wert > 0 → Average (Severity 3). Mails die länger als eine Stunde in der deferred Queue liegen, deuten auf ein systematisches Problem hin.

TL;DR

ProblemUrsacheFix
Smarthost in snort2c geblocktSID 2260002 False Positive bei STARTTLSSuppress-Regel für SID + IP
Passlist wirkungslospcap-Mode: Passlist schützt nur vor Alerts, nicht vor BlockingCron-Guard entfernt Passlist-IPs aus snort2c
7 Tage unbemerktKein Monitoring auf snort2c-InhaltZabbix: Critical IP Check + Smarthost Reachability
Doppelte Retry-ZeitIPv6 nicht routbar, Postfix probiert trotzdemsmtp_address_preference=ipv4

Lessons Learned

IDS/IPS auf dem Gateway kann stillschweigend kritische Dienste lahmlegen. Es gibt keine Fehlermeldung nach außen. Postfix retried still. Der Smarthost antwortet nicht mit einem SMTP-Fehler. Die Queue wächst. Niemand bekommt eine Bounce-Mail, weil die Mail nie den eigenen Server verlassen hat.

Drei Erkenntnisse für pfSense + Suricata im Legacy pcap-Mode:

  1. Passlist ≠ Block-Schutz. Im pcap-Mode unterdrückt die Passlist nur Alerts, nicht das Blocking. Das steht nirgends prominent in der Dokumentation.
  2. Monitoring auf snort2c-Inhalt ist Pflicht. Ohne Zabbix-Alert auf kritische IPs ist der Silent Failure vorprogrammiert.
  3. Suppress vor Passlist. Für bekannte False Positives ist die Suppress-Regel der direkte Weg — sie verhindert den Alert an der Quelle.

Für produktive Umgebungen: Entweder Inline-IPS-Mode mit funktionierender Passlist, oder pcap-Mode mit Cron-Guard und aktivem Monitoring auf alle kritischen IPs. Ein “die Passlist wird das schon regeln” reicht im pcap-Mode nicht.