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

233 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```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" <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 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.