# 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: ` 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: ```ts 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" `, 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 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 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.