9.7 KiB
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_REPLYTOauslöst, entsteht gar nicht erst — die Ausnahme inses_worker_forwardedkö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:
-
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=0ist derzeit gesetzt. -
DMARC bei externen Zielen — das ist der harte Punkt.
handleForwards()unterscheidet: interne Ziele gehen übersendInternalEmail()auf Port 25, externe überses.sendRawEmail(). Beide setzen den Envelope-From aufrecipient, also auf unsere eigene Domain. Heute ist das für externe Weiterleitungen sogar ein Vorteil: SES signiert mit unserem DKIM, SPF passt, und weilFrom: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=0ist 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 erFrom-Domain ==To-Domain voraussetzt — ohne die externe Zustellbarkeit anzufassen. -
Absenderdarstellung. Im Posteingang steht dann der Originalabsender, nicht mehr
support@. Ob das gewünscht ist, ist eine fachliche Entscheidung — Mailinglisten lösen es überFrom: "Name via support" <support@bayarea-cc.com>, was allerdingsSPOOF_REPLYTOteilweise 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 70–580 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 restart — ein 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.