Files
email-amazon/DMS/README.md
2026-08-15 15:09:41 +02:00

9.7 KiB
Raw Blame History

Spam-Scoring im Mailfluss — Stand und offene Punkte

Dieses Dokument hält fest, warum die rspamd-Konfiguration hier vom Standard abweicht und was noch offen ist. Hintergrund, der sich weder aus der Config noch aus der Git-History erschließt, gehört hierher.

Mailfluss

Externe Mail → SES → S3/SQS → email-worker (Container) → SMTP :25 → DMS

Daraus folgt die zentrale Eigenheit: rspamd sieht als Client-IP immer den Worker-Container, nie den echten Absender-Server. Jede verbindungsbezogene Prüfung misst damit unsere eigene Infrastruktur. Ohne Gegenmaßnahme fallen systematisch Strafpunkte an, die nichts mit der Mail zu tun haben:

Symbol Gewicht
HFILTER_HOSTNAME_UNKNOWN 6.0 (kein PTR für 172.x — exakt die add_header-Schwelle)
R_SPF_FAIL / R_SPF_SOFTFAIL 4.5 / 2.5
R_DKIM_NA / R_DKIM_PERMFAIL 1.0 / 4.5

SPF/DKIM/DMARC gehen dadurch nicht verloren: SES prüft sie bereits mit der echten Absender-IP, das Ergebnis liegt im SQS-Payload (receipt.spamVerdict, spfVerdict, dkimVerdict, dmarcVerdict).

Aktueller Stand

Schwellwerte in docker-data/dms/config/rspamd/override.d/actions.conf: add_header = 6, reject = 500 (praktisch nie), greylist = 500 (aus). Mit MOVE_SPAM_TO_JUNK=1 bedeutet ein Score ab 6 also: Mail landet im Junk.

Die Settings-Regeln stehen in docker-data/dms/config/rspamd/append.d/settings.conf, ausgewertet nach Priorität, es greift nur der erste Treffer:

Prio Regel Gilt für Wirkung
20 ses_worker_forwarded X-SES-Worker-Processed: forwarded|ooo-reply Auth-Gruppen aus plus SPOOF_REPLYTO, FREEMAIL_REPLYTO*
10 ses_worker_injected Worker-Header vorhanden (also delivered) nur Auth-Gruppen aus
5 local_reinjection Docker-Netz ohne Worker-Header (Sieve-Redirects, Roundcube) ersetzt die alte DOCKER_WL-Whitelist

Alle drei sind zusätzlich an 172.16.0.0/12 / 127.0.0.0/8 gebunden. Ohne diese IP-Bedingung könnte ein externer Absender den Header selbst setzen und die Prüfungen abschalten.

Was bewusst entfernt wurde: die frühere DOCKER_WL-Regel (-50 Punkte für jede Mail mit eigener Absenderdomain) samt erzwungenem „no action" aus force_actions.conf. Sie war ein Spoofing-Loch — mit SPOOF_PROTECTION=0 genügte ein MAIL FROM: <x@bayarea-cc.com> für garantierte Zustellung — und hat echten Spam an weitergeleitete Adressen durchgereicht.


Offener Punkt B: Weiterleitungen ohne Neukomposition

Status: offen, bewusst zurückgestellt. Punkt A (die Settings-Regel ses_worker_forwarded) ist am 2026-08-15 umgesetzt worden und behebt das Symptom. B ist die strukturelle Lösung.

Das Problem

buildForwardMessage() in email-worker-nodejs/src/email/rules-processor.ts:212 baut für jede Weiterleitung eine neue Mail:

from: recipient,          // support@bayarea-cc.com   — die getroffene lokale Adresse
to: forwardTo,            // ebrunks@bayarea-cc.com   — das Weiterleitungsziel
subject: `FWD: ${originalSubject}`,
replyTo: originalFrom,    // andreas.knuth@gmail.com  — der Originalabsender

Bei einer Weiterleitung an einen Kollegen in derselben Domain ergibt das formal die Signatur eines CEO-Fraud-Angriffs: Absender und Empfänger intern, die Antwort geht nach außen an eine Freemail-Adresse.

Belegt an Queue-ID F228E320085 (2026-08-15 07:53:38):

6.00  SPOOF_REPLYTO              bayarea-cc.com;gmail.com
2.00  FREEMAIL_REPLYTO_NEQ_FROM
-0.10 MIME_GOOD
0.05  MANY_INVISIBLE_PARTS
----
7.95  → add header → Junk

Dieselbe Mail als Original an support@ (EC702320085, eine Sekunde später): -1.05, no action. Der Unterschied kommt allein aus der Bauform der Weiterleitung, nicht aus dem Inhalt.

Der Vorschlag

Statt eine neue Mail zu komponieren, die Originalmail unverändert weiterreichen (Remailing): From: bleibt der Originalabsender, Envelope-From wird eine lokale Bounce-Adresse, kein Reply-To mehr, kein FWD:-Präfix.

Vorteile über die Spam-Bewertung hinaus:

  • Threading in den Clients bleibt intakt (Message-ID, References)
  • Originalbetreff und Originalheader bleiben erhalten
  • Anhänge müssen nicht neu verpackt werden
  • Das Muster, das SPOOF_REPLYTO auslöst, entsteht gar nicht erst — die Ausnahme in ses_worker_forwarded könnte danach wieder enger gefasst oder ganz entfernt werden

Warum es nicht einfach nur „besser" ist

Das ist ein echter Eingriff mit Folgen, die vor der Umsetzung geklärt werden müssen:

  1. Bounces. Envelope-From muss auf eine Adresse zeigen, die wir selbst auswerten — sonst gehen Zustellfehler an den Originalabsender, der mit unserer Weiterleitung nichts zu tun hat. Klassische Lösung ist SRS; ENABLE_SRS=0 ist derzeit gesetzt.

  2. DMARC bei externen Zielen — das ist der harte Punkt. handleForwards() unterscheidet: interne Ziele gehen über sendInternalEmail() auf Port 25, externe über ses.sendRawEmail(). Beide setzen den Envelope-From auf recipient, also auf unsere eigene Domain. Heute ist das für externe Weiterleitungen sogar ein Vorteil: SES signiert mit unserem DKIM, SPF passt, und weil From: ebenfalls unsere Domain ist, ist DMARC sauber ausgerichtet.

    Genau das würde Remailing zerstören. Mit From: = Originalabsender prüft Gmail DMARC gegen dessen Domain — und wir erfüllen weder deren SPF (die Mail kommt von SES) noch deren DKIM (wir haben den Body umgebaut). Das ist das klassische Forwarding-Problem. SRS löst den SPF-Teil, ENABLE_SRS=0 ist derzeit gesetzt; für DKIM hilft nur, die Mail wirklich byte-identisch durchzureichen, inklusive Originalheader.

    Praktische Konsequenz: B ließe sich zunächst nur für interne Ziele umsetzen und für externe beim jetzigen Verhalten bleiben. Das löst den SPOOF_REPLYTO-Fall — der tritt ohnehin nur intern auf, weil er From-Domain == To-Domain voraussetzt — ohne die externe Zustellbarkeit anzufassen.

  3. Absenderdarstellung. Im Posteingang steht dann der Originalabsender, nicht mehr support@. Ob das gewünscht ist, ist eine fachliche Entscheidung — Mailinglisten lösen es über From: "Name via support" <support@bayarea-cc.com>, was allerdings SPOOF_REPLYTO teilweise zurückholt.

Abnahmekriterium

Eine Weiterleitung einer externen Mail an eine interne Adresse zeigt in rspamd_report.sh --detail einen Score unter 6 ohne die Ausnahme für SPOOF_REPLYTO, und ein Reply des Empfängers erreicht den Originalabsender.


Weitere offene Punkte

fuzzy_check wieder einschalten

Abgeschaltet in docker-data/dms/config/rspamd/local.d/fuzzy_check.conf, weil UDP 11335 ausgehend nicht durchkommt (vermutlich Egress-Filter des Providers). Jede Mail lief in denselben Timeout und kostete konstant ~4,3 s Scanzeit ohne jeden Gegenwert. Nach dem Abschalten liegen die Scans bei 70580 ms.

Fuzzy erkennt Massen-Spam über Hashes — genau die Klasse, die weder Auth-Prüfungen noch ein frisch angelernter Bayes zuverlässig erwischt. Das lohnt sich mit dem Provider zu klären. Test:

docker exec mailserver rspamadm fuzzy_ping -s rspamd.com

Bayes anlernen

BAYES_* feuert erst ab 200 gelernten Nachrichten je Klasse, aktuell also gar nicht. docker-data/dms/config/train_bayes.sh lernt einmalig aus den vorhandenen Postfächern (Junk → Spam, INBOX → Ham), Default ist Trockenlauf.

Wichtig: nur kuratierte Postfächer über --only einbeziehen. Ohne Filter läuft das Skript über alle Postfächer aller Domains unter /var/mail — bei Weiterleitungsadressen ist die INBOX ein ungefilterter Durchlauf und als Ham-Quelle unbrauchbar.

Reihenfolge beachten: erst falsch einsortierte Mail abstellen, dann lernen. Solange legitime Weiterleitungen im Junk landen, lernt RSPAMD_LEARN=1 sie dort automatisch als Spam an und vergiftet den Klassifikator.

SES-Verdicts als Header stempeln

Der Worker könnte receipt.spfVerdict / dkimVerdict / dmarcVerdict aus dem SQS-Payload als Header setzen. Dann ließen sich die echten, mit der richtigen Absender-IP ermittelten Auth-Ergebnisse per multimap wieder bepunkten, statt sie ersatzlos abzuschalten.


Betrieb

Auswertung

docker exec mailserver bash /tmp/docker-mailserver/rspamd_report.sh -n 20
docker exec mailserver bash /tmp/docker-mailserver/rspamd_report.sh --detail <QID>
docker exec mailserver bash /tmp/docker-mailserver/rspamd_report.sh --slow 1000

Das Feld Regel: in der Detailansicht zeigt, welche Settings-Regel gegriffen hat — der schnellste Weg zu prüfen, ob eine Änderung überhaupt aktiv ist. Voraussetzung ist level = "info" in rspamd/local.d/logging.inc; der DMS-Default silent protokolliert nichts.

Änderungen an append.d/ ausrollen

user-patches.sh hängt die Dateien aus rspamd/append.d/ an die local.d-Pendants an und überspringt das, wenn der Marker DMS-CUSTOM-APPEND dort schon steht. /etc/rspamd liegt im Container-Dateisystem und überlebt docker restartein Neustart übernimmt Änderungen also nicht. Nötig ist:

docker compose up -d --force-recreate mailserver

Das Anhängen statt Überschreiben ist zwingend: DMS legt in settings.conf selbst eine Regel für authentifizierte User ab (DMS::SED_TAG::1), die ein cp löschen würde — womit RSPAMD_CHECK_AUTHENTICATED=0 wirkungslos wäre.

DNS

rspamds Blocklisten brauchen einen eigenen Recursor. Über Dockers eingebautes DNS (127.0.0.11) stufte Spamhaus die Anfragen als von einem offenen Resolver kommend ein und antwortete nur mit DBL_BLOCKED_OPENRESOLVER. Gelöst über den Service dns-recursor (PowerDNS) im eigenen Netz dms_dns mit fester IP — fest deshalb, weil Dockers dns:-Option nur IPs akzeptiert.

ENABLE_UNBOUND existiert in DMS nicht und war ein No-Op.