Compare commits
7 Commits
93faa527fe
...
contabo
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d49d5fa72 | |||
| 691c7bd1ec | |||
| f10cc230af | |||
| 1ef0b96be4 | |||
| 0978154f43 | |||
| 9fdd3408a0 | |||
| 1eb46d98ef |
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.
|
||||
@@ -26,7 +26,13 @@ services:
|
||||
- ./docker-data/dms/mail-state/:/var/mail-state/
|
||||
- ./docker-data/dms/mail-logs/:/var/log/mail/
|
||||
- ./docker-data/dms/config/:/tmp/docker-mailserver/
|
||||
- ./docker-data/dms/config/dovecot/conf.d/95-sieve-redirect.conf:/etc/dovecot/conf.d/95-sieve-redirect.conf:ro
|
||||
# ENTFERNT: Mount von dovecot/conf.d/95-sieve-redirect.conf
|
||||
# Der Host-Pfad existiert nicht, Docker hat daraufhin ein VERZEICHNIS
|
||||
# unter /etc/dovecot/conf.d/95-sieve-redirect.conf angelegt (nachweisbar
|
||||
# per `docker exec mailserver cat ...` -> "Is a directory"). Der Mount war
|
||||
# also wirkungslos. Falls dort spaeter wirklich etwas gebraucht wird
|
||||
# (z.B. sieve_redirect_envelope_from), erst die Datei auf dem Host
|
||||
# anlegen, dann den Mount wieder aufnehmen.
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- ./sync_dynamodb_to_sieve.py:/scripts/sync.py:ro
|
||||
- ./sieve-cron:/etc/cron.d/sieve-sync:ro
|
||||
@@ -65,7 +71,12 @@ services:
|
||||
- SSL_KEY_PATH=/etc/mail/certs/${NODE_HOSTNAME}/${NODE_HOSTNAME}.key
|
||||
|
||||
# SPAM / Rspamd
|
||||
- ENABLE_OPENDKIM=1
|
||||
# OpenDKIM deaktiviert: es liegen keine Keys unter config/opendkim/, der
|
||||
# Milter hat also nie etwas signiert. Ausgehende Mail wird von Amazon SES
|
||||
# signiert (Easy DKIM), eingehende DKIM-Pruefung macht rspamd selbst.
|
||||
# DMS warnt beim Start ausdruecklich davor, OpenDKIM parallel zu rspamd
|
||||
# zu betreiben.
|
||||
- ENABLE_OPENDKIM=0
|
||||
- ENABLE_OPENDMARC=0
|
||||
- ENABLE_POLICYD_SPF=0
|
||||
- ENABLE_RSPAMD=1
|
||||
@@ -80,7 +91,10 @@ services:
|
||||
|
||||
# Sicherheit
|
||||
- ENABLE_FAIL2BAN=1
|
||||
- ENABLE_UNBOUND=1
|
||||
# ENABLE_UNBOUND entfernt: die Variable existiert in DMS nicht (mehr) und
|
||||
# war wirkungslos - rspamd hat ueber Dockers eingebautes DNS (127.0.0.11)
|
||||
# aufgeloest und bekam von Spamhaus nur DBL_BLOCKED_OPENRESOLVER zurueck.
|
||||
# Ersetzt durch den dns-recursor-Service weiter unten.
|
||||
|
||||
# Sonstige
|
||||
- ENABLE_MANAGESIEVE=0
|
||||
@@ -95,7 +109,10 @@ services:
|
||||
|
||||
# Amazon SES Relay
|
||||
- RELAY_HOST=email-smtp.us-east-2.amazonaws.com
|
||||
- RELAY_PORT=587
|
||||
# Node-spezifisch: manche Provider blockieren ausgehend 587.
|
||||
# Wert pro Server in der .env setzen (z.B. RELAY_PORT=2587),
|
||||
# ohne Eintrag bleibt es bei 587.
|
||||
- RELAY_PORT=${RELAY_PORT:-587}
|
||||
- RELAY_USER=${SES_SMTP_USER}
|
||||
- RELAY_PASSWORD=${SES_SMTP_PASSWORD}
|
||||
|
||||
@@ -116,11 +133,47 @@ services:
|
||||
- NET_ADMIN
|
||||
- SYS_PTRACE
|
||||
restart: unless-stopped
|
||||
|
||||
# -------------------------------------------------------
|
||||
# DNS: rspamd fragt DNS-Blocklisten (Spamhaus DBL/ZEN, SURBL) ab. Diese
|
||||
# Dienste beantworten keine Anfragen, die ueber oeffentliche Resolver
|
||||
# kommen - man bekommt DBL_BLOCKED_OPENRESOLVER statt eines Ergebnisses.
|
||||
# Deshalb ein eigener rekursiver Resolver, siehe Service dns-recursor.
|
||||
#
|
||||
# ACHTUNG: Damit ersetzt der Recursor Dockers eingebautes DNS (127.0.0.11)
|
||||
# fuer DIESEN Container. Der mailserver kann dann keine Container-Namen
|
||||
# mehr aufloesen (roundcube-db, email-worker-ts, ...). Das ist hier
|
||||
# unkritisch, weil er von sich aus nur nach aussen verbindet (SES-Relay).
|
||||
# Faellt der Recursor aus, hat der mailserver kein DNS mehr - deshalb
|
||||
# restart: always beim Recursor.
|
||||
# -------------------------------------------------------
|
||||
dns:
|
||||
- 10.99.0.53
|
||||
depends_on:
|
||||
- dns-recursor
|
||||
|
||||
networks:
|
||||
mail_network:
|
||||
aliases:
|
||||
- mailserver
|
||||
- ${NODE_HOSTNAME}
|
||||
dns_network:
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Rekursiver DNS-Resolver ausschliesslich fuer den mailserver.
|
||||
# Er fragt die Root-Server selbst ab, statt an einen Upstream-Resolver
|
||||
# weiterzuleiten - nur so akzeptiert Spamhaus die Anfragen.
|
||||
# Braucht keine Konfiguration: das Image laeuft per Default als Recursor
|
||||
# und erlaubt Anfragen aus den RFC1918-Netzen (10.99.0.0/24 liegt darin).
|
||||
# ---------------------------------------------------------
|
||||
dns-recursor:
|
||||
image: powerdns/pdns-recursor-53:5.3.10
|
||||
container_name: dns-recursor
|
||||
restart: always
|
||||
stop_grace_period: 0s
|
||||
networks:
|
||||
dns_network:
|
||||
ipv4_address: 10.99.0.53
|
||||
|
||||
roundcube:
|
||||
image: roundcube/roundcubemail:latest
|
||||
@@ -176,3 +229,16 @@ services:
|
||||
networks:
|
||||
mail_network:
|
||||
external: true
|
||||
|
||||
# Eigenes Netz nur fuer mailserver <-> dns-recursor.
|
||||
# Bewusst NICHT im externen mail_network: dort vergibt Docker die Adressen
|
||||
# dynamisch, eine feste IP koennte mit einem anderen Container kollidieren.
|
||||
# Das Subnetz muss fest sein, weil Dockers `dns:`-Option nur IPs akzeptiert.
|
||||
# Falls 10.99.0.0/24 bei euch schon belegt ist (VPN, AWS-VPC), hier und bei
|
||||
# `dns:` sowie `ipv4_address` oben gemeinsam auf ein freies Netz aendern.
|
||||
dns_network:
|
||||
name: dms_dns
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 10.99.0.0/24
|
||||
|
||||
135
DMS/docker-data/dms/config/rspamd/append.d/settings.conf
Normal file
135
DMS/docker-data/dms/config/rspamd/append.d/settings.conf
Normal file
@@ -0,0 +1,135 @@
|
||||
# DMS-CUSTOM-APPEND
|
||||
# ===========================================================================
|
||||
# WICHTIG: Diese Datei wird von user-patches.sh an /etc/rspamd/local.d/settings.conf
|
||||
# ANGEHAENGT und nicht darueber kopiert. DMS legt in dieser Datei selbst eine
|
||||
# Regel fuer authentifizierte User ab (DMS::SED_TAG::1), die erhalten bleiben muss.
|
||||
# ===========================================================================
|
||||
#
|
||||
# Hintergrund
|
||||
# -----------
|
||||
# Externe Mail erreicht DMS nie direkt: SES -> S3/SQS -> email-worker -> SMTP.
|
||||
# Rspamd sieht als Client-IP daher immer den Worker-Container (172.x) statt des
|
||||
# echten Absender-Servers. Alle verbindungsbezogenen Pruefungen messen damit
|
||||
# unsere eigene Infrastruktur und liefern systematisch Strafpunkte:
|
||||
#
|
||||
# HFILTER_HOSTNAME_UNKNOWN 6.0 (kein PTR fuer 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
|
||||
#
|
||||
# Diese Gruppen werden fuer Einlieferungen aus dem Docker-Netz deaktiviert.
|
||||
# Inhaltsbezogene Pruefungen (Bayes, Fuzzy, URL-/Surbl-Blocklisten, MIME,
|
||||
# Attachment-Checks) bleiben vollstaendig aktiv - echter Spam wird also weiter
|
||||
# erkannt, nur eben anhand des Inhalts statt anhand einer Transportstrecke,
|
||||
# die rspamd hier gar nicht beurteilen kann.
|
||||
#
|
||||
# SPF/DKIM/DMARC gehen dadurch nicht verloren: SES prueft sie bereits mit der
|
||||
# echten Absender-IP. Das Ergebnis liegt im SQS-Payload (receipt.spamVerdict,
|
||||
# spfVerdict, dkimVerdict, dmarcVerdict). Sobald der Worker diese Verdicts als
|
||||
# Header stempelt, koennen sie hier per multimap wieder bepunktet werden.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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:
|
||||
# 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
|
||||
# Bedingungen werden von rspamd implizit mit UND verknuepft.
|
||||
# ---------------------------------------------------------------------------
|
||||
ses_worker_injected {
|
||||
priority = 10;
|
||||
ip = ["172.16.0.0/12", "127.0.0.0/8"];
|
||||
header = {
|
||||
"X-SES-Worker-Processed" = true;
|
||||
}
|
||||
apply {
|
||||
groups_disabled = ["spf", "dkim", "dmarc", "hfilter"];
|
||||
symbols_disabled = ["RDNS_NONE"];
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3) Lokale Re-Injection ohne Worker-Header
|
||||
#
|
||||
# Betrifft:
|
||||
# - Sieve-Redirects fuer INTERNE Mail (accounting@ -> support@ -> Forwards).
|
||||
# Diese Mail laeuft nie ueber SES/Worker, hat also keinen Worker-Header.
|
||||
# - Roundcube, das ohne SASL-Auth aus dem Docker-Netz einliefert
|
||||
# (ROUNDCUBEMAIL_SMTP_USER ist bewusst leer).
|
||||
#
|
||||
# Beides wurde bisher von der DOCKER_WL-Whitelist (-50) aufgefangen. Diese
|
||||
# Regel ersetzt sie - deutlich enger gefasst, weil sie an das Docker-Netz
|
||||
# gebunden ist statt an eine faelschbare Absenderdomain.
|
||||
#
|
||||
# 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,
|
||||
# muss diese Regel weg - sonst umgeht jeder externe Absender die Pruefung.
|
||||
# ---------------------------------------------------------------------------
|
||||
local_reinjection {
|
||||
priority = 5;
|
||||
ip = ["172.16.0.0/12", "127.0.0.0/8"];
|
||||
apply {
|
||||
groups_enabled = ["dkim"];
|
||||
}
|
||||
}
|
||||
42
DMS/docker-data/dms/config/rspamd/local.d/fuzzy_check.conf
Normal file
42
DMS/docker-data/dms/config/rspamd/local.d/fuzzy_check.conf
Normal file
@@ -0,0 +1,42 @@
|
||||
# documentation: https://rspamd.com/doc/modules/fuzzy_check.html
|
||||
#
|
||||
# Das Modul ist abgeschaltet, weil die oeffentliche Fuzzy-Storage von
|
||||
# rspamd.com von diesem Server aus nicht erreichbar ist. Im Log lief JEDE
|
||||
# eingehende Mail in denselben Timeout:
|
||||
#
|
||||
# fuzzy_check_timer_callback: got IO timeout with server
|
||||
# fuzzy1.rspamd.com:11335(80.241.57.6:11335), after 1/1 retransmits
|
||||
#
|
||||
# Die Server werden sauber aufgeloest, es scheitert also nicht an DNS,
|
||||
# sondern an der UDP-Verbindung auf Port 11335 (vermutlich Egress-Filter
|
||||
# des Providers).
|
||||
#
|
||||
# Kosten des Defaults: timeout = 2s bei retransmits = 1, also zwei Versuche
|
||||
# a 2 Sekunden = ~4 Sekunden Wartezeit pro Mail - genau die konstanten
|
||||
# 4,3 Sekunden Scanzeit, die im Log zu sehen waren. Ohne jeden Gegenwert,
|
||||
# da nie eine Antwort kommt.
|
||||
#
|
||||
# Zusaetzlich ein Betriebsrisiko: DMS setzt soft_reject_on_timeout = true.
|
||||
# Solange nur dieses eine Modul haengt, passiert nichts; laeuft der Task als
|
||||
# Ganzes in den Timeout, wird Mail temporaer abgelehnt statt zugestellt.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# WIEDER EINSCHALTEN, sobald UDP 11335 ausgehend erreichbar ist:
|
||||
# Pruefen laesst sich das direkt mit rspamds eigenem Werkzeug:
|
||||
#
|
||||
# docker exec mailserver rspamadm fuzzy_ping -s rspamd.com
|
||||
#
|
||||
# Antwortet die Storage, diese Datei loeschen (dann gilt wieder der Default)
|
||||
# oder die Zeile unten auskommentieren und stattdessen die Timeouts knapp
|
||||
# halten, damit ein erneuter Ausfall nicht wieder 4 Sekunden kostet:
|
||||
#
|
||||
# timeout = 1s;
|
||||
# retransmits = 0;
|
||||
#
|
||||
# Fuzzy ist inhaltlich wertvoll - es erkennt Massen-Spam ueber Hashes, also
|
||||
# genau die Klasse, die weder von Auth-Pruefungen noch von einem frisch
|
||||
# angelernten Bayes zuverlaessig erwischt wird. Es lohnt sich, das mit dem
|
||||
# Provider zu klaeren, statt es dauerhaft abgeschaltet zu lassen.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
enabled = false;
|
||||
19
DMS/docker-data/dms/config/rspamd/local.d/logging.inc
Normal file
19
DMS/docker-data/dms/config/rspamd/local.d/logging.inc
Normal file
@@ -0,0 +1,19 @@
|
||||
# documentation: https://rspamd.com/doc/configuration/logging.html
|
||||
#
|
||||
# Uebersteuert den DMS-Default (level = "silent"), bei dem rspamd ueberhaupt
|
||||
# nichts protokolliert und Spam-Scores nicht nachvollziehbar sind.
|
||||
#
|
||||
# type = "console" bleibt bewusst stehen: Supervisor leitet stdout/stderr von
|
||||
# rspamd bereits nach /var/log/mail/rspamd.log um (siehe dms-services.conf).
|
||||
# Ein eigenes type = "file" mit demselben Pfad haette zwei Schreiber auf
|
||||
# derselben Datei zur Folge.
|
||||
#
|
||||
# Mit level = "info" schreibt rspamd pro Mail eine Zeile mit Aktion, Score und
|
||||
# allen ausgeloesten Symbolen samt Einzelgewicht:
|
||||
#
|
||||
# docker exec mailserver sh -c 'grep rspamd_task_write_log /var/log/mail/rspamd.log | tail -20'
|
||||
|
||||
type = "console";
|
||||
level = "info";
|
||||
color = false;
|
||||
systemd = false;
|
||||
@@ -1,8 +1,18 @@
|
||||
DOCKER_WL {
|
||||
type = "from";
|
||||
filter = "email:domain";
|
||||
map = "/etc/rspamd/override.d/docker_whitelist.map";
|
||||
symbol = "DOCKER_WHITELIST";
|
||||
description = "Whitelist fuer eigene Domains";
|
||||
score = -50.0;
|
||||
}
|
||||
# documentation: https://rspamd.com/doc/modules/multimap.html
|
||||
#
|
||||
# Die fruehere Regel DOCKER_WL (-50 Punkte fuer jede Mail, deren Envelope-From
|
||||
# auf eine unserer eigenen Domains zeigt) ist bewusst entfernt worden:
|
||||
#
|
||||
# 1. Sie war ein Spoofing-Loch. Mit SPOOF_PROTECTION=0 genuegte einem
|
||||
# beliebigen externen Absender ein "MAIL FROM: <x@bayarea-cc.com>", um
|
||||
# -50 Punkte plus das erzwungene "no action" aus force_actions.conf zu
|
||||
# bekommen - also garantierte Zustellung in die Inbox.
|
||||
# 2. Sie hat echten Spam an weitergeleitete Adressen durchgereicht, weil der
|
||||
# email-worker seine Weiterleitungen mit Envelope-From = Empfaengeradresse
|
||||
# (also eigene Domain) einliefert.
|
||||
# 3. Sie hat nur ein Symptom kompensiert: die falschen Auth-/Hostname-Scores,
|
||||
# die entstehen, weil alle Mail aus dem Worker-Container kommt. Das wird
|
||||
# jetzt an der Ursache geloest - siehe rspamd/append.d/settings.conf.
|
||||
#
|
||||
# Die Datei bleibt (leer) bestehen, damit user-patches.sh sie beim Start ueber
|
||||
# eine evtl. noch im Container liegende alte Version kopiert.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
rules {
|
||||
DOCKER_WHITELIST_FORCE {
|
||||
action = "no action";
|
||||
expression = "DOCKER_WHITELIST";
|
||||
}
|
||||
}
|
||||
# documentation: https://rspamd.com/doc/modules/force_actions.html
|
||||
#
|
||||
# Die Regel DOCKER_WHITELIST_FORCE ("no action" sobald DOCKER_WHITELIST greift)
|
||||
# ist entfernt. Sie hat die Spam-Entscheidung fuer jede Mail mit eigener
|
||||
# Absenderdomain hart ueberschrieben - inklusive echtem Spam an weitergeleitete
|
||||
# Adressen. Das dazugehoerige Symbol existiert nicht mehr, siehe
|
||||
# rspamd/local.d/multimap.conf.
|
||||
#
|
||||
# Keine rules{}-Sektion = force_actions ist wirkungslos. Datei bleibt bestehen,
|
||||
# damit DMS sie beim Start ueber eine alte Version im Container kopiert.
|
||||
|
||||
179
DMS/docker-data/dms/config/rspamd_report.sh
Executable file
179
DMS/docker-data/dms/config/rspamd_report.sh
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/bin/bash
|
||||
# ===========================================================================
|
||||
# rspamd_report.sh - lesbare Auswertung von /var/log/mail/rspamd.log
|
||||
#
|
||||
# Die Logzeilen von rspamd enthalten alles, sind aber unlesbar. Dieses Skript
|
||||
# macht daraus zwei Ansichten:
|
||||
#
|
||||
# Uebersicht: eine Zeile je Mail mit Score, Aktion, Scanzeit, Von, An
|
||||
# Detail: die Symbolaufschluesselung einer Mail, nach Gewicht sortiert,
|
||||
# mit Summe - also warum genau diese Mail diesen Score hat
|
||||
#
|
||||
# AUSFUEHRUNG - immer IM Container:
|
||||
#
|
||||
# docker exec mailserver bash /tmp/docker-mailserver/rspamd_report.sh
|
||||
# docker exec mailserver bash /tmp/docker-mailserver/rspamd_report.sh -n 40
|
||||
# docker exec mailserver bash /tmp/docker-mailserver/rspamd_report.sh --detail
|
||||
# docker exec mailserver bash /tmp/docker-mailserver/rspamd_report.sh --detail C0D8A320066
|
||||
#
|
||||
# Optionen:
|
||||
# -n N Anzahl Mails in der Uebersicht (Default 15)
|
||||
# --detail [ID] Symbolaufschluesselung. ID ist ein beliebiges Fragment aus
|
||||
# Queue-ID oder Message-ID. Ohne ID die letzte Mail.
|
||||
# --slow MS Nur Mails, deren Scan laenger als MS Millisekunden dauerte
|
||||
# --log PFAD Anderes Logfile (Default /var/log/mail/rspamd.log)
|
||||
#
|
||||
# Hinweis: Das Log enthaelt nur Mails, die seit dem Umstellen auf
|
||||
# level = "info" (rspamd/local.d/logging.inc) verarbeitet wurden.
|
||||
# ===========================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
LOGFILE="/var/log/mail/rspamd.log"
|
||||
COUNT=15
|
||||
DETAIL=0
|
||||
DETAIL_ID=""
|
||||
SLOW_MS=0
|
||||
|
||||
usage() { sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' ; }
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n) COUNT="${2:-15}"; shift 2 ;;
|
||||
--detail) DETAIL=1
|
||||
if [[ ${2:-} != "" && ${2:-} != -* ]]; then DETAIL_ID="$2"; shift 2; else shift; fi ;;
|
||||
--slow) SLOW_MS="${2:-0}"; shift 2 ;;
|
||||
--log) LOGFILE="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unbekannte Option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -r ${LOGFILE} ]]; then
|
||||
echo "FEHLER: ${LOGFILE} nicht lesbar. Laeuft das Skript wirklich IM Container?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TASKS=$(grep 'rspamd_task_write_log' "${LOGFILE}" 2>/dev/null)
|
||||
if [[ -z ${TASKS} ]]; then
|
||||
echo "Keine ausgewerteten Mails im Log gefunden."
|
||||
echo "Steht in rspamd/local.d/logging.inc wirklich level = \"info\"?"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detailansicht einer einzelnen Mail
|
||||
# ---------------------------------------------------------------------------
|
||||
if (( DETAIL )); then
|
||||
if [[ -n ${DETAIL_ID} ]]; then
|
||||
LINE=$(grep -F "${DETAIL_ID}" <<<"${TASKS}" | tail -1)
|
||||
[[ -z ${LINE} ]] && { echo "Keine Mail mit '${DETAIL_ID}' im Log gefunden." >&2; exit 1; }
|
||||
else
|
||||
LINE=$(tail -1 <<<"${TASKS}")
|
||||
fi
|
||||
|
||||
awk '
|
||||
{
|
||||
# Kopfdaten
|
||||
ts = substr($0, 1, 19)
|
||||
match($0, /qid: <[^>]*>/); qid = extract()
|
||||
match($0, /from: <[^>]*>/); from = extract()
|
||||
match($0, /rcpts: <[^>]*>/); rcpt = extract()
|
||||
match($0, /settings_id: [^,]*/); sid = substr($0, RSTART+13, RLENGTH-13)
|
||||
match($0, /time: [0-9.]+ms/); tm = substr($0, RSTART+6, RLENGTH-8)
|
||||
match($0, /dns req: [0-9]+/); dns = substr($0, RSTART+9, RLENGTH-9)
|
||||
|
||||
# Aktion und Score: (default: F (no action): [4.74/500.00]
|
||||
if (match($0, /\([a-z ]+\): \[-?[0-9.]+\//)) {
|
||||
chunk = substr($0, RSTART, RLENGTH)
|
||||
action = substr(chunk, 2, index(chunk, ")") - 2)
|
||||
score = substr(chunk, index(chunk, "[") + 1, length(chunk) - index(chunk, "[") - 1)
|
||||
}
|
||||
|
||||
printf "Zeit: %s\n", ts
|
||||
printf "Queue-ID: %s\n", qid
|
||||
printf "Von: %s\n", from
|
||||
printf "An: %s\n", rcpt
|
||||
printf "Regel: %s\n", sid
|
||||
printf "Aktion: %s Score: %s Scanzeit: %s ms DNS: %s\n\n", action, score, tm, dns
|
||||
|
||||
# Symbolblock zwischen "] [" und "]), len:"
|
||||
i = index($0, "] [")
|
||||
if (i == 0) { print " (keine Symbole im Log)"; exit }
|
||||
rest = substr($0, i + 3)
|
||||
j = index(rest, "]), len:")
|
||||
if (j == 0) j = length(rest)
|
||||
syms = substr(rest, 1, j - 1)
|
||||
|
||||
# auf Kommas der obersten Ebene splitten (Klammern koennen Kommas enthalten)
|
||||
depth = 0; cur = ""; n = 0
|
||||
for (k = 1; k <= length(syms); k++) {
|
||||
c = substr(syms, k, 1)
|
||||
if (c == "{") depth++
|
||||
else if (c == "}") depth--
|
||||
if (c == "," && depth == 0) { if (cur != "") entries[++n] = cur; cur = ""; continue }
|
||||
cur = cur c
|
||||
}
|
||||
if (cur != "") entries[++n] = cur
|
||||
|
||||
printf " %8s %-38s %s\n", "GEWICHT", "SYMBOL", "DETAIL"
|
||||
printf " %8s %-38s %s\n", "-------", "--------------------------------------", "------"
|
||||
|
||||
total = 0
|
||||
# zwei Durchlaeufe: erst die mit Gewicht, dann die neutralen
|
||||
for (pass = 1; pass <= 2; pass++) {
|
||||
for (e = 1; e <= n; e++) {
|
||||
entry = entries[e]
|
||||
p1 = index(entry, "(")
|
||||
if (p1 == 0) continue
|
||||
name = substr(entry, 1, p1 - 1)
|
||||
p2 = index(entry, ")")
|
||||
w = substr(entry, p1 + 1, p2 - p1 - 1)
|
||||
payload = ""
|
||||
b1 = index(entry, "{")
|
||||
if (b1 > 0) payload = substr(entry, b1 + 1, length(entry) - b1 - 1)
|
||||
gsub(/;$/, "", payload)
|
||||
|
||||
if (pass == 1 && w + 0 == 0) continue
|
||||
if (pass == 2 && w + 0 != 0) continue
|
||||
if (pass == 1) total += w
|
||||
|
||||
printf " %8.2f %-38s %s\n", w, name, substr(payload, 1, 60)
|
||||
}
|
||||
if (pass == 1) printf " %8s %s\n", "-------", ""
|
||||
if (pass == 1) printf " %8.2f %s\n\n neutrale Symbole (Gewicht 0):\n", total, "SUMME"
|
||||
}
|
||||
}
|
||||
function extract( s) { s = substr($0, RSTART, RLENGTH); return substr(s, index(s, "<") + 1, length(s) - index(s, "<") - 1) }
|
||||
' <<<"${LINE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Uebersicht
|
||||
# ---------------------------------------------------------------------------
|
||||
printf '%-19s %7s %-12s %7s %-30s %-30s\n' "ZEIT" "SCORE" "AKTION" "MS" "VON" "AN"
|
||||
printf -- '--------------------------------------------------------------------------------------------------------------------\n'
|
||||
|
||||
awk -v slow="${SLOW_MS}" '
|
||||
{
|
||||
ts = substr($0, 1, 19)
|
||||
match($0, /from: <[^>]*>/); from = extract()
|
||||
match($0, /rcpts: <[^>]*>/); rcpt = extract()
|
||||
match($0, /time: [0-9.]+ms/); tm = substr($0, RSTART+6, RLENGTH-8) + 0
|
||||
|
||||
if (match($0, /\([a-z ]+\): \[-?[0-9.]+\//)) {
|
||||
chunk = substr($0, RSTART, RLENGTH)
|
||||
action = substr(chunk, 2, index(chunk, ")") - 2)
|
||||
score = substr(chunk, index(chunk, "[") + 1, length(chunk) - index(chunk, "[") - 1) + 0
|
||||
} else { action = "?"; score = 0 }
|
||||
|
||||
if (tm < slow) next
|
||||
printf "%-19s %7.2f %-12s %7.0f %-30.30s %-30.30s\n", ts, score, action, tm, from, rcpt
|
||||
}
|
||||
function extract( s) { s = substr($0, RSTART, RLENGTH); return substr(s, index(s, "<") + 1, length(s) - index(s, "<") - 1) }
|
||||
' <<<"${TASKS}" | tail -n "${COUNT}"
|
||||
|
||||
echo
|
||||
echo "Detail zu einer Mail: $0 --detail <Queue-ID>"
|
||||
echo "Nur langsame Scans: $0 --slow 1000"
|
||||
222
DMS/docker-data/dms/config/train_bayes.sh
Executable file
222
DMS/docker-data/dms/config/train_bayes.sh
Executable file
@@ -0,0 +1,222 @@
|
||||
#!/bin/bash
|
||||
# ===========================================================================
|
||||
# train_bayes.sh - Bayes-Klassifikator von rspamd aus den vorhandenen
|
||||
# Postfaechern anlernen.
|
||||
#
|
||||
# Hintergrund: rspamd traegt mit BAYES_* erst dann zum Score bei, wenn genug
|
||||
# gelernt wurde (Default: 200 Spam UND 200 Ham). Bis dahin liefert der
|
||||
# Klassifikator gar nichts - genau das war im Log zu sehen, dort tauchte kein
|
||||
# einziges BAYES_-Symbol auf. Statt monatelang auf manuelles Verschieben in
|
||||
# den Junk-Ordner zu warten (das macht RSPAMD_LEARN=1 bereits automatisch),
|
||||
# lernt dieses Skript einmalig aus dem, was ohnehin auf der Platte liegt.
|
||||
#
|
||||
# AUSFUEHRUNG - immer IM Container:
|
||||
#
|
||||
# docker exec mailserver bash /tmp/docker-mailserver/train_bayes.sh
|
||||
# -> Trockenlauf: zaehlt nur, lernt nichts. Das ist der Default.
|
||||
#
|
||||
# docker exec mailserver bash /tmp/docker-mailserver/train_bayes.sh --apply
|
||||
# -> lernt tatsaechlich
|
||||
#
|
||||
# Optionen:
|
||||
# --apply Wirklich lernen (ohne das passiert nichts)
|
||||
# --max N Max. Nachrichten je Postfach und Klasse (Default 400)
|
||||
# --include-sent Gesendete Mails zusaetzlich als Ham lernen
|
||||
# --only MUSTER Nur Postfaecher, deren Adresse eines der Muster
|
||||
# enthaelt. Mehrfach angebbar oder kommasepariert:
|
||||
# --only support@,andreas.knuth@
|
||||
# WICHTIG: Ohne diese Option laeuft das Skript ueber
|
||||
# ALLE Postfaecher aller Domains unter /var/mail.
|
||||
# --host HOST:PORT rspamd-Controller (Default 127.0.0.1:11334)
|
||||
#
|
||||
# Passwort: Falls der Controller eines verlangt (bei euch in
|
||||
# rspamd/override.d/worker-controller.inc gesetzt), vorweg setzen:
|
||||
# docker exec -e RSPAMC_PASSWORD='...' mailserver bash /tmp/.../train_bayes.sh
|
||||
#
|
||||
# WICHTIG zur Datenqualitaet:
|
||||
# - Ham kommt aus der INBOX. Liegt dort unerkannter Spam, wird er als Ham
|
||||
# gelernt und verschlechtert die Erkennung. Vorher grob durchsehen lohnt.
|
||||
# - Spam kommt aus Junk. Liegt dort faelschlich einsortierte legitime Mail,
|
||||
# gilt dasselbe umgekehrt.
|
||||
# - Ein sehr schiefes Verhaeltnis (z.B. 8000 Ham zu 50 Spam) macht den
|
||||
# Klassifikator einseitig. Der Trockenlauf zeigt das Verhaeltnis an -
|
||||
# danach ggf. mit --max nachsteuern.
|
||||
# ===========================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
MAILBASE="${MAILBASE:-/var/mail}"
|
||||
RSPAMC_HOST="127.0.0.1:11334"
|
||||
MAX_PER_CLASS=400
|
||||
MAX_BYTES=5242880 # 5 MB - groessere Mails bringen fuers Lernen nichts
|
||||
BATCH=50 # Dateien pro rspamc-Aufruf
|
||||
APPLY=0
|
||||
INCLUDE_SENT=0
|
||||
ONLY_PATTERNS=()
|
||||
|
||||
usage() { sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' ; }
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--apply) APPLY=1; shift ;;
|
||||
--max) MAX_PER_CLASS="${2:-}"; shift 2 ;;
|
||||
--include-sent) INCLUDE_SENT=1; shift ;;
|
||||
--only) IFS=',' read -ra _pat <<<"${2:-}"
|
||||
ONLY_PATTERNS+=("${_pat[@]}"); shift 2 ;;
|
||||
--host) RSPAMC_HOST="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unbekannte Option: $1" >&2; echo "Hilfe: $0 --help" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -d ${MAILBASE} ]]; then
|
||||
echo "FEHLER: ${MAILBASE} existiert nicht. Laeuft das Skript wirklich IM Container?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RSPAMC=(rspamc -h "${RSPAMC_HOST}")
|
||||
[[ -n ${RSPAMC_PASSWORD:-} ]] && RSPAMC+=(-P "${RSPAMC_PASSWORD}")
|
||||
|
||||
if ! command -v rspamc >/dev/null 2>&1; then
|
||||
echo "FEHLER: rspamc nicht gefunden." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! "${RSPAMC[@]}" stat >/dev/null 2>&1; then
|
||||
echo "FEHLER: Controller unter ${RSPAMC_HOST} nicht erreichbar oder Passwort noetig." >&2
|
||||
echo " Versuche es mit: docker exec -e RSPAMC_PASSWORD='...' mailserver bash $0 ..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dateien einsammeln: bis LIMIT regulaere Dateien unterhalb der uebergebenen
|
||||
# Maildir-Verzeichnisse, groessenbegrenzt. Neueste zuerst waere schoener, ist
|
||||
# aber bei zehntausenden Dateien zu teuer - Maildir-Namen beginnen mit dem
|
||||
# Timestamp, daher liefert die Sortierung eine brauchbare Naeherung.
|
||||
# ---------------------------------------------------------------------------
|
||||
gather() {
|
||||
local limit="$1"; shift
|
||||
local dir
|
||||
for dir in "$@"; do
|
||||
[[ -d ${dir} ]] || continue
|
||||
find "${dir}" -maxdepth 1 -type f -size -"${MAX_BYTES}"c -print 2>/dev/null
|
||||
done | sort -r | head -n "${limit}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lernen in Batches. Gibt "gelernt fehlgeschlagen" zurueck.
|
||||
# ---------------------------------------------------------------------------
|
||||
learn() {
|
||||
local cmd="$1"; shift
|
||||
local -a files=("$@")
|
||||
local total=${#files[@]}
|
||||
local ok=0 fail=0 idx=0 out
|
||||
|
||||
while (( idx < total )); do
|
||||
out=$("${RSPAMC[@]}" "${cmd}" "${files[@]:idx:BATCH}" 2>&1)
|
||||
ok=$(( ok + $(grep -c 'success = true' <<<"${out}") ))
|
||||
fail=$(( fail + $(grep -ci 'error' <<<"${out}") ))
|
||||
idx=$(( idx + BATCH ))
|
||||
done
|
||||
|
||||
echo "${ok} ${fail}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hauptlauf
|
||||
# ---------------------------------------------------------------------------
|
||||
if (( APPLY )); then
|
||||
echo "=== Bayes-Training (SCHREIBEND) ==="
|
||||
else
|
||||
echo "=== Bayes-Training (Trockenlauf - es wird nichts gelernt) ==="
|
||||
fi
|
||||
echo "Basis: ${MAILBASE} | max ${MAX_PER_CLASS} je Postfach und Klasse"
|
||||
if (( ${#ONLY_PATTERNS[@]} > 0 )); then
|
||||
echo "Filter: nur Postfaecher mit '${ONLY_PATTERNS[*]}'"
|
||||
else
|
||||
echo "Filter: KEINER - es werden ALLE Postfaecher unter ${MAILBASE} erfasst"
|
||||
fi
|
||||
printf '\n%-42s %8s %8s\n' "POSTFACH" "SPAM" "HAM"
|
||||
printf -- '---------------------------------------------------------------\n'
|
||||
|
||||
total_spam=0
|
||||
total_ham=0
|
||||
learned_spam=0
|
||||
learned_ham=0
|
||||
failed=0
|
||||
|
||||
for domain_dir in "${MAILBASE}"/*/; do
|
||||
[[ -d ${domain_dir} ]] || continue
|
||||
domain=$(basename "${domain_dir}")
|
||||
|
||||
for user_dir in "${domain_dir}"*/; do
|
||||
user_dir="${user_dir%/}"
|
||||
[[ -d ${user_dir}/cur ]] || continue # keine Maildir -> ueberspringen
|
||||
|
||||
mailbox="$(basename "${user_dir}")@${domain}"
|
||||
|
||||
if (( ${#ONLY_PATTERNS[@]} > 0 )); then
|
||||
matched=0
|
||||
for pat in "${ONLY_PATTERNS[@]}"; do
|
||||
[[ ${mailbox} == *"${pat}"* ]] && { matched=1; break; }
|
||||
done
|
||||
(( matched )) || continue
|
||||
fi
|
||||
|
||||
mapfile -t spam_files < <(gather "${MAX_PER_CLASS}" \
|
||||
"${user_dir}/.Junk/cur" "${user_dir}/.Junk/new" \
|
||||
"${user_dir}/.Spam/cur" "${user_dir}/.Spam/new")
|
||||
|
||||
ham_dirs=("${user_dir}/cur" "${user_dir}/new")
|
||||
if (( INCLUDE_SENT )); then
|
||||
ham_dirs+=("${user_dir}/.Sent/cur" "${user_dir}/.Sent Messages/cur")
|
||||
fi
|
||||
mapfile -t ham_files < <(gather "${MAX_PER_CLASS}" "${ham_dirs[@]}")
|
||||
|
||||
n_spam=${#spam_files[@]}
|
||||
n_ham=${#ham_files[@]}
|
||||
(( n_spam == 0 && n_ham == 0 )) && continue
|
||||
|
||||
printf '%-42s %8d %8d\n' "${mailbox}" "${n_spam}" "${n_ham}"
|
||||
total_spam=$(( total_spam + n_spam ))
|
||||
total_ham=$(( total_ham + n_ham ))
|
||||
|
||||
if (( APPLY )); then
|
||||
if (( n_spam > 0 )); then
|
||||
read -r ok fail <<<"$(learn learn_spam "${spam_files[@]}")"
|
||||
learned_spam=$(( learned_spam + ok )); failed=$(( failed + fail ))
|
||||
fi
|
||||
if (( n_ham > 0 )); then
|
||||
read -r ok fail <<<"$(learn learn_ham "${ham_files[@]}")"
|
||||
learned_ham=$(( learned_ham + ok )); failed=$(( failed + fail ))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
printf -- '---------------------------------------------------------------\n'
|
||||
printf '%-42s %8d %8d\n\n' "SUMME" "${total_spam}" "${total_ham}"
|
||||
|
||||
if (( total_spam > 0 )); then
|
||||
ratio=$(( total_ham / (total_spam > 0 ? total_spam : 1) ))
|
||||
if (( ratio >= 5 )); then
|
||||
echo "WARNUNG: Ham/Spam-Verhaeltnis etwa ${ratio}:1 - sehr einseitig."
|
||||
echo " Besser mit kleinerem --max fahren, z.B. --max $(( total_spam / 4 + 10 ))."
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( ! APPLY )); then
|
||||
echo "Trockenlauf beendet. Zum echten Lernen dasselbe Kommando mit --apply."
|
||||
echo "rspamd braucht je Klasse mindestens 200 Nachrichten, bevor BAYES_* feuert."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Gelernt: ${learned_spam} Spam, ${learned_ham} Ham, ${failed} Fehler/Duplikate."
|
||||
echo
|
||||
echo "=== rspamc stat ==="
|
||||
"${RSPAMC[@]}" stat 2>/dev/null | grep -iE 'learn|classifier|statfile' || true
|
||||
echo
|
||||
echo "Kontrolle an der naechsten eingehenden Mail:"
|
||||
echo " docker exec mailserver sh -c 'grep rspamd_task_write_log /var/log/mail/rspamd.log | tail -3'"
|
||||
echo "In der Symbolliste sollte jetzt BAYES_HAM oder BAYES_SPAM auftauchen."
|
||||
@@ -1,47 +1,61 @@
|
||||
#!/bin/bash
|
||||
# user-patches.sh laeuft bei jedem Start von DMS automatisch
|
||||
# user-patches.sh laeuft bei jedem Start von DMS automatisch,
|
||||
# nach dem Setup und bevor die Daemons gestartet werden.
|
||||
|
||||
ACCOUNTS_FILE="/tmp/docker-mailserver/postfix-accounts.cf"
|
||||
WHITELIST_FILE="/etc/rspamd/override.d/docker_whitelist.map"
|
||||
SRC_BASE="/tmp/docker-mailserver/rspamd"
|
||||
LOCAL_D="/etc/rspamd/local.d"
|
||||
|
||||
# --- Rspamd Whitelist generieren ---
|
||||
STATIC_DOMAINS=(
|
||||
"bayarea-cc.com"
|
||||
"ruehrgedoens.de"
|
||||
"annavillesda.org"
|
||||
"bizmatch.net"
|
||||
"biz-match.com"
|
||||
"qrmaster.net"
|
||||
"nqsltd.com"
|
||||
"iitwelders.com"
|
||||
)
|
||||
|
||||
echo "Patching: Generiere Rspamd Whitelist aus Accounts + statischen Domains..."
|
||||
|
||||
{
|
||||
for domain in "${STATIC_DOMAINS[@]}"; do
|
||||
echo "$domain"
|
||||
done
|
||||
if [ -f "$ACCOUNTS_FILE" ]; then
|
||||
awk -F'|' '{print $1}' "$ACCOUNTS_FILE" | cut -d'@' -f2
|
||||
fi
|
||||
} | sort | uniq > "$WHITELIST_FILE"
|
||||
|
||||
chmod 644 "$WHITELIST_FILE"
|
||||
chown _rspamd:_rspamd "$WHITELIST_FILE" 2>/dev/null || true
|
||||
echo "Whitelist erstellt:"
|
||||
cat "$WHITELIST_FILE"
|
||||
|
||||
# --- local.d configs manuell kopieren (DMS kopiert local.d nicht automatisch) ---
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1) local.d configs kopieren
|
||||
# DMS kopiert nur rspamd/override.d automatisch, local.d nicht.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "Patching: Kopiere custom rspamd local.d configs..."
|
||||
SRC="/tmp/docker-mailserver/rspamd/local.d"
|
||||
DST="/etc/rspamd/local.d"
|
||||
if [ -d "$SRC" ]; then
|
||||
for f in "$SRC"/*; do
|
||||
if [ -d "${SRC_BASE}/local.d" ]; then
|
||||
for f in "${SRC_BASE}/local.d"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
cp "$f" "$DST/$(basename "$f")"
|
||||
chown root:root "$DST/$(basename "$f")"
|
||||
chmod 644 "$DST/$(basename "$f")"
|
||||
echo " Kopiert: $(basename "$f") -> $DST/"
|
||||
base=$(basename "$f")
|
||||
cp "$f" "${LOCAL_D}/${base}"
|
||||
chown root:root "${LOCAL_D}/${base}"
|
||||
chmod 644 "${LOCAL_D}/${base}"
|
||||
echo " Kopiert: ${base} -> ${LOCAL_D}/"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2) append.d: an bestehende local.d-Dateien ANHAENGEN statt sie zu ersetzen
|
||||
#
|
||||
# Zwingend fuer settings.conf: DMS legt dort selbst die Regel fuer
|
||||
# authentifizierte User ab (DMS::SED_TAG::1). Ein cp darueber wuerde sie
|
||||
# loeschen und RSPAMD_CHECK_AUTHENTICATED=0 wirkungslos machen.
|
||||
#
|
||||
# Der Marker DMS-CUSTOM-APPEND macht das Anhaengen idempotent - /etc/rspamd
|
||||
# liegt im Container-Dateisystem und ueberlebt einen `docker restart`.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "Patching: Haenge custom rspamd Snippets an local.d configs an..."
|
||||
if [ -d "${SRC_BASE}/append.d" ]; then
|
||||
for f in "${SRC_BASE}/append.d"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
base=$(basename "$f")
|
||||
target="${LOCAL_D}/${base}"
|
||||
|
||||
if grep -q 'DMS-CUSTOM-APPEND' "$target" 2>/dev/null; then
|
||||
echo " Bereits vorhanden, uebersprungen: ${base}"
|
||||
continue
|
||||
fi
|
||||
|
||||
printf '\n' >>"$target"
|
||||
cat "$f" >>"$target"
|
||||
chown root:root "$target"
|
||||
chmod 644 "$target"
|
||||
echo " Angehaengt: ${base} -> ${target}"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HINWEIS: Die frueher hier erzeugte Rspamd-Whitelist
|
||||
# (/etc/rspamd/override.d/docker_whitelist.map aus postfix-accounts.cf +
|
||||
# statischer Domainliste) ist entfernt. Die zugehoerige multimap-Regel
|
||||
# DOCKER_WL (-50) und das erzwungene "no action" aus force_actions.conf gibt
|
||||
# es nicht mehr - sie waren ein Spoofing-Loch und haben echten Spam an
|
||||
# weitergeleitete Adressen durchgereicht. Ersatz: rspamd/append.d/settings.conf.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -32,13 +32,27 @@ class email_config extends rcube_plugin
|
||||
function email_config_form()
|
||||
{
|
||||
$rcmail = rcube::get_instance();
|
||||
$email = $rcmail->user->get_username();
|
||||
$email = $rcmail->user->get_username();
|
||||
|
||||
$secret_key = 'SHARED_SECRET_KEY_987654321';
|
||||
$config_url = 'https://config.email-bayarea.com';
|
||||
$expires = time() + 3600;
|
||||
$data = $email . '|' . $expires;
|
||||
|
||||
// mailadmin.<domain der adresse>, z.B. support@bayarea-cc.com
|
||||
// -> https://mailadmin.bayarea-cc.com
|
||||
$at = strrpos($email, '@');
|
||||
if ($at === false) {
|
||||
// Roundcube liefert ausnahmsweise keinen vollen Namen: hier kann kein
|
||||
// sinnvoller Link gebaut werden.
|
||||
return '';
|
||||
}
|
||||
$domain = strtolower(substr($email, $at + 1));
|
||||
$config_url = 'https://mailadmin.' . $domain;
|
||||
|
||||
$expires = time() + 3600;
|
||||
$data = $email . '|' . $expires;
|
||||
$signature = hash_hmac('sha256', $data, $secret_key);
|
||||
$url = $config_url . '/?email=' . urlencode($email) . '&expires=' . $expires . '&signature=' . $signature;
|
||||
$url = $config_url . '/?email=' . urlencode($email)
|
||||
. '&expires=' . $expires
|
||||
. '&signature=' . $signature;
|
||||
|
||||
$out = '
|
||||
<div class="box" style="max-width: 600px; margin: 40px auto; padding: 30px; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
|
||||
Reference in New Issue
Block a user