rspamd rule
This commit is contained in:
232
DMS/README.md
Normal file
232
DMS/README.md
Normal file
@@ -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: <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 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.
|
||||||
@@ -28,12 +28,67 @@
|
|||||||
# Header stempelt, koennen sie hier per multimap wieder bepunktet werden.
|
# 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 <QID>
|
||||||
|
# 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:
|
# Der Worker setzt X-SES-Worker-Processed auf JEDER Mail, die er einliefert:
|
||||||
# delivered = Originalmail an das lokale Postfach
|
# delivered = Originalmail an das lokale Postfach -> diese Regel
|
||||||
# forwarded = neu komponierte Weiterleitung (rules-processor.ts)
|
# forwarded = neu komponierte Weiterleitung -> Regel 1
|
||||||
# ooo-reply = Abwesenheitsantwort
|
# 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
|
# Die IP-Bedingung ist die Absicherung: ohne sie koennte ein externer Absender
|
||||||
# den Header selbst setzen und die Auth-Pruefungen abschalten. Beide
|
# 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:
|
# Betrifft:
|
||||||
# - Sieve-Redirects fuer INTERNE Mail (accounting@ -> support@ -> Forwards).
|
# - 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
|
# Regel ersetzt sie - deutlich enger gefasst, weil sie an das Docker-Netz
|
||||||
# gebunden ist statt an eine faelschbare Absenderdomain.
|
# gebunden ist statt an eine faelschbare Absenderdomain.
|
||||||
#
|
#
|
||||||
# Niedrigere Prioritaet als Regel 1: rspamd wertet Regeln nach Prioritaet aus
|
# Niedrigste Prioritaet: rspamd wertet Regeln nach Prioritaet aus und nimmt
|
||||||
# und nimmt den ersten Treffer, Worker-Mail landet also nie hier.
|
# den ersten Treffer, Worker-Mail landet also immer in Regel 1 oder 2.
|
||||||
#
|
#
|
||||||
# ACHTUNG: Diese Regel setzt voraus, dass Port 25 NICHT oeffentlich erreichbar
|
# 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,
|
# ist (docker-compose.yml bindet ihn auf 127.0.0.1). Wird das je geaendert,
|
||||||
|
|||||||
Reference in New Issue
Block a user