From 0d49d5fa72bc2d5dbc7db851f1d792328e83e6e8 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 15 Aug 2026 15:09:41 +0200 Subject: [PATCH] rspamd rule --- DMS/README.md | 232 ++++++++++++++++++ .../dms/config/rspamd/append.d/settings.conf | 69 +++++- 2 files changed, 294 insertions(+), 7 deletions(-) create mode 100644 DMS/README.md diff --git a/DMS/README.md b/DMS/README.md new file mode 100644 index 0000000..565bd31 --- /dev/null +++ b/DMS/README.md @@ -0,0 +1,232 @@ +# 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. diff --git a/DMS/docker-data/dms/config/rspamd/append.d/settings.conf b/DMS/docker-data/dms/config/rspamd/append.d/settings.conf index bd8e4bc..cdc1024 100644 --- a/DMS/docker-data/dms/config/rspamd/append.d/settings.conf +++ b/DMS/docker-data/dms/config/rspamd/append.d/settings.conf @@ -28,12 +28,67 @@ # Header stempelt, koennen sie hier per multimap wieder bepunktet werden. # --------------------------------------------------------------------------- -# 1) Einlieferungen des email-workers (externe Mail + dessen Forwards/OOO) +# 1) Vom Worker SELBST komponierte Mail: Weiterleitungen und OOO-Antworten +# +# Zusaetzlich zu den Auth-Symbolen aus Regel 2 fallen hier inhaltliche Symbole +# an, die auf die Bauform der Weiterleitung reagieren. buildForwardMessage() +# in email-worker-nodejs/src/email/rules-processor.ts setzt: +# +# From: die getroffene lokale Adresse (support@bayarea-cc.com) +# To: das Weiterleitungsziel (ebrunks@bayarea-cc.com) +# Reply-To: den Originalabsender (andreas.knuth@gmail.com) +# +# Damit sieht jede Weiterleitung an einen Kollegen in derselben Domain formal +# aus wie CEO-Fraud: Absender und Empfaenger intern, Antwort geht nach aussen +# an eine Freemail-Adresse. Rspamd bepunktet das - zu Recht, nur ist die +# Praemisse hier falsch, weil wir die Mail selbst gebaut haben: +# +# SPOOF_REPLYTO 6.0 From-Domain == To-Domain != Reply-To-Domain +# FREEMAIL_REPLYTO_NEQ_FROM 2.0 Reply-To ist Freemail und != From +# +# Belegt an F228E320085 (2026-08-15 07:53:38): Score 7.95, also ueber +# add_header = 6 - die Weiterleitung landete im Junk, waehrend dieselbe Mail +# als Original an support@ mit -1.05 sauber durchlief (EC702320085). +# +# WARUM EINE EIGENE REGEL statt symbols_disabled in Regel 2: +# Regel 2 gilt auch fuer "delivered", also fuer echte eingehende Fremdmail. +# Dort ist SPOOF_REPLYTO genau das Signal, das BEC-Angriffe erkennt - das +# darf nicht verloren gehen. Diese Regel prueft deshalb den WERT des Headers +# und greift nur fuer Mail aus unserer eigenen Komposition. +# +# Die Auth-Gruppen muessen wiederholt werden: rspamd wertet nach Prioritaet +# aus und wendet nur die erste passende Regel an, Regel 2 greift hier also +# nicht mehr. +# +# Falls der Regexp-Vergleich nicht zieht, faellt die Mail auf Regel 2 zurueck +# und behaelt ihre 7.95 - der Fehler ist damit sichtbar, nicht still. Pruefen +# laesst sich das am Feld "Regel:" in: +# docker exec mailserver bash /tmp/docker-mailserver/rspamd_report.sh --detail +# Erwartet wird ses_worker_forwarded. +# --------------------------------------------------------------------------- +ses_worker_forwarded { + priority = 20; + ip = ["172.16.0.0/12", "127.0.0.0/8"]; + header = { + "X-SES-Worker-Processed" = "^(forwarded|ooo-reply)$"; + } + apply { + groups_disabled = ["spf", "dkim", "dmarc", "hfilter"]; + symbols_disabled = ["RDNS_NONE", "SPOOF_REPLYTO", + "FREEMAIL_REPLYTO_NEQ_FROM", "FREEMAIL_REPLYTO"]; + } +} + +# --------------------------------------------------------------------------- +# 2) Uebrige Einlieferungen des email-workers (externe Mail, "delivered") # # Der Worker setzt X-SES-Worker-Processed auf JEDER Mail, die er einliefert: -# delivered = Originalmail an das lokale Postfach -# forwarded = neu komponierte Weiterleitung (rules-processor.ts) -# ooo-reply = Abwesenheitsantwort +# delivered = Originalmail an das lokale Postfach -> diese Regel +# forwarded = neu komponierte Weiterleitung -> Regel 1 +# ooo-reply = Abwesenheitsantwort -> Regel 1 +# +# Hier werden ausschliesslich die Transportpruefungen abgeschaltet. Alle +# inhaltlichen Symbole bleiben aktiv, insbesondere SPOOF_REPLYTO. # # Die IP-Bedingung ist die Absicherung: ohne sie koennte ein externer Absender # den Header selbst setzen und die Auth-Pruefungen abschalten. Beide @@ -52,7 +107,7 @@ ses_worker_injected { } # --------------------------------------------------------------------------- -# 2) Lokale Re-Injection ohne Worker-Header +# 3) Lokale Re-Injection ohne Worker-Header # # Betrifft: # - Sieve-Redirects fuer INTERNE Mail (accounting@ -> support@ -> Forwards). @@ -64,8 +119,8 @@ ses_worker_injected { # Regel ersetzt sie - deutlich enger gefasst, weil sie an das Docker-Netz # gebunden ist statt an eine faelschbare Absenderdomain. # -# Niedrigere Prioritaet als Regel 1: rspamd wertet Regeln nach Prioritaet aus -# und nimmt den ersten Treffer, Worker-Mail landet also nie hier. +# Niedrigste Prioritaet: rspamd wertet Regeln nach Prioritaet aus und nimmt +# den ersten Treffer, Worker-Mail landet also immer in Regel 1 oder 2. # # ACHTUNG: Diese Regel setzt voraus, dass Port 25 NICHT oeffentlich erreichbar # ist (docker-compose.yml bindet ihn auf 127.0.0.1). Wird das je geaendert,