IT-Blog von Sebastian van de Meer

Kategorie: E-Mail & Mailserver (Seite 1 von 7)

Mailserver-Praxis mit Postfix und Dovecot — SPF, DKIM, DMARC, DANE, MTA-STS und Spam-Abwehr mit Rspamd.

Post-Quantum TLS für E-Mail — Postfix und Dovecot mit X25519MLKEM768 auf FreeBSD 15

Visualisierung hybrider Post-Quantum-TLS-Verschlüsselung für E-Mail mit X25519MLKEM768 (ML-KEM-768 + X25519) auf Postfix und Dovecot unter FreeBSD 15

Nachdem ich im letzten Beitrag OpenSSH mit hybriden Post-Quantum-Algorithmen abgesichert habe, lag die Frage nahe: Was ist eigentlich mit E-Mail? Mein FreeBSD 15 liefert Postfix 3.10.6, Dovecot 2.3.21.1 und OpenSSL 3.5.4 – und genau diese Kombination bringt alles mit, was man für quantensichere Verschlüsselung im Mailverkehr braucht. Ohne zusätzliche Pakete, ohne Patches, ohne Gefrickel.

Warum überhaupt PQC für E-Mail?

Das „Store now, decrypt later„-Szenario, das ich beim SSH-Beitrag angesprochen habe, trifft auf E-Mail mindestens genauso zu. E-Mails werden über SMTP zwischen Servern transportiert – und dieser Transport ist grundsätzlich abfangbar. Wer heute TLS-verschlüsselten Mailverkehr mitschneidet und archiviert, könnte diesen in einigen Jahren mit einem ausreichend leistungsfähigen Quantencomputer entschlüsseln. Zumindest theoretisch.

Heißt das, morgen liest jemand eure Mails? Nein. Aber wenn ihr vertrauliche Kommunikation betreibt und die heute eingesetzte Kryptografie in zehn Jahren noch standhalten soll, ist jetzt der richtige Zeitpunkt zum Handeln. Zumal der Aufwand (wie ihr gleich seht) überschaubar ist.

Was steckt hinter X25519MLKEM768?

Kurz zur Einordnung: ML-KEM (ehemals CRYSTALS-Kyber) ist der vom NIST im August 2024 standardisierte Post-Quantum-Algorithmus für den Schlüsselaustausch (FIPS 203). X25519MLKEM768 ist ein sogenannter Hybrid-Algorithmus – er kombiniert das klassische X25519 (Curve25519 ECDH) mit ML-KEM-768 zu einem gemeinsamen Schlüssel.

Der Clou dabei: Selbst wenn ML-KEM irgendwann gebrochen werden sollte, bleibt die klassische X25519-Komponente intakt. Und umgekehrt. Man muss also nicht darauf vertrauen, dass der neue Algorithmus auch wirklich hält – man bekommt das Beste aus beiden Welten.

Wer Firefox nutzt, hat das übrigens vermutlich schon in Aktion gesehen: Seit Firefox 124 wird bei TLS 1.3 standardmäßig X25519MLKEM768 für den Schlüsselaustausch verwendet. Schaut mal in die Verbindungsdetails einer HTTPS-Seite – die Chancen stehen gut, dass dort bereits ein hybrider PQC-Schlüsselaustausch stattfindet. Also, wenn der Server das anbietet, wie dieser hier *zwinker*.

Voraussetzungen prüfen

Bevor ihr konfiguriert, solltet ihr sicherstellen, dass euer OpenSSL ML-KEM überhaupt kann. Auf meinem FreeBSD 15:

$ openssl version
OpenSSL 3.5.4 30 Sep 2025 (Library: OpenSSL 3.5.4 30 Sep 2025)

Und dann die entscheidende Frage – kennt OpenSSL die benötigten KEM-Algorithmen?

$ openssl list -kem-algorithms | grep -i mlkem
  ML-KEM-512
  ML-KEM-768
  ML-KEM-1024
  X25519MLKEM768
  SecP256r1MLKEM768

Wenn X25519MLKEM768 in der Liste auftaucht, seid ihr startklar. Das ist bei OpenSSL ab Version 3.5 der Fall – der ML-KEM-Support ist im Default-Provider enthalten, es wird kein zusätzlicher OQS-Provider und kein liboqs benötigt.

Noch ein Check – sind die Algorithmen auch als TLS-Gruppen verfügbar?

$ openssl list -tls-groups | grep -i mlkem
  X25519MLKEM768
  SecP256r1MLKEM768
  SecP384r1MLKEM1024

Perfekt. Weiter geht’s.

Postfix konfigurieren

Postfix steuert die verwendeten TLS-Gruppen für den Schlüsselaustausch über den Parameter tls_eecdh_auto_curves. Dieser gilt sowohl für eingehende (smtpd) als auch für ausgehende (smtp) Verbindungen.

Vorher:

tls_eecdh_auto_curves = X25519, prime256v1, secp384r1

Nachher:

tls_eecdh_auto_curves = X25519MLKEM768, X25519, prime256v1, secp384r1

Das war’s. Eine Zeile. X25519MLKEM768 wird als bevorzugte Gruppe an den Anfang gestellt, die klassischen Kurven bleiben als Fallback erhalten. Clients die kein ML-KEM beherrschen, verhandeln einfach X25519 oder prime256v1 – die Abwärtskompatibilität bleibt also vollständig gewahrt.

Die Änderung setzt ihr entweder direkt in /usr/local/etc/postfix/main.cf oder über:

# postconf "tls_eecdh_auto_curves = X25519MLKEM768, X25519, prime256v1, secp384r1"
# postfix reload

Wichtig: Dieser Parameter beeinflusst alle Postfix-Dienste – SMTP (Port 25), Submission (Port 587) und SMTPS (Port 465). Ihr müsst also nicht jeden Port einzeln konfigurieren.

Dovecot konfigurieren

Dovecot verwendet den Parameter ssl_curve_list um die TLS-Gruppen für IMAP-Verbindungen festzulegen. Standardmäßig ist dieser leer, was bedeutet, dass OpenSSL seine eigenen Defaults verwendet. Das kann funktionieren, muss aber nicht.

In /usr/local/etc/dovecot/conf.d/10-ssl.conf:

ssl_curve_list = X25519MLKEM768:X25519:prime256v1:secp384r1

Achtung: Dovecot verwendet Doppelpunkte als Trennzeichen (OpenSSL-Syntax), Postfix verwendet Kommas. Nicht verwechseln. Ja, passiert mir oft 😀

Danach:

# doveadm reload

Überprüfen

Jetzt wird’s spannend. Funktioniert es tatsächlich? Zum Testen verwende ich openssl s_client direkt auf dem Server; denn euer lokales Linux oder macOS hat möglicherweise noch kein OpenSSL 3.5 mit ML-KEM-Support. Mein Linux Mint 22.3 hat es leider noch nicht *schnief*

SMTP (Port 25, STARTTLS):

$ openssl s_client -connect smtp.kernel-error.de:25 -starttls smtp \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

SMTPS (Port 465):

$ openssl s_client -connect smtp.kernel-error.de:465 \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

Submission (Port 587, STARTTLS):

$ openssl s_client -connect smtp.kernel-error.de:587 -starttls smtp \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

IMAPS (Port 993):

$ openssl s_client -connect imap.kernel-error.de:993 \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

Alle vier Ports verhandeln TLSv1.3 mit X25519MLKEM768. Die hybride Post-Quantum-Verschlüsselung ist aktiv.

Wenn ihr testen wollt, was passiert wenn ein Client kein ML-KEM unterstützt:

$ openssl s_client -connect imap.kernel-error.de:465 \
    -groups X25519 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519

Fallback auf X25519 – funktioniert sauber.

Was das nicht leistet

Wie schon beim SSH-Beitrag muss ich auch hier einschränken: Wir sichern damit den Schlüsselaustausch ab, nicht die Authentifizierung. Die TLS-Zertifikate verwenden weiterhin klassische Algorithmen (RSA, ECDSA). Für Post-Quantum-Signaturen in Zertifikaten bräuchte man ML-DSA (ehemals CRYSTALS-Dilithium) – und obwohl OpenSSL 3.5 das theoretisch unterstützt, gibt es Stand heute keine öffentliche Zertifizierungsstelle, die ML-DSA-Zertifikate ausstellt. Das wird kommen, ist aber noch Zukunftsmusik. Hey, wie ECDSA bei S/MIME (oder ist das schon anders?).

Für die Praxis bedeutet das: Ein Angreifer mit einem Quantencomputer könnte theoretisch die Serverauthentifizierung angreifen (ECDSA/RSA brechen), müsste das aber in Echtzeit tun – hier greift „store now, decrypt later“ nicht, weil eine gefälschte Authentifizierung nur im Moment der Verbindung nützt. Der Schlüsselaustausch hingegen – und damit die eigentliche Vertraulichkeit der transportierten E-Mails – ist durch X25519MLKEM768 auch gegen zukünftige Quantenangriffe geschützt.

Zwei Zeilen Konfiguration, ein Reload pro Dienst – und euer Mailserver verhandelt quantensichere Verschlüsselung. Vollständig abwärtskompatibel, ohne Einschränkungen für bestehende Clients. Es gibt eigentlich keinen Grund, das nicht zu tun. Oder fällt euch etwas ein?

Viel Spaß beim Nachbauen – und wie immer: bei Fragen, fragen.

GPT in Rspamd aktivieren: so nutze ich das LLM-Signal im Score

Seit einiger Zeit nutze ich das GPT-Modul von Rspamd, um bei der Spam-Erkennung ein zusätzliches Signal zu bekommen. Es ersetzt nichts — kein Bayes, kein DKIM, kein RBL — sondern ist ein weiterer Sensor im Gesamtbild. Wer sich fragt, wie das in der Praxis aussieht und worauf man achten muss: hier mein aktuelles Setup.

Update 2026-02-13: Dieser Beitrag wurde komplett überarbeitet. Die ursprüngliche Version nutzte json=false, was zu Parse-Problemen führte. Außerdem fehlte ein Custom Prompt — und genau das ist der entscheidende Punkt, wie sich herausgestellt hat.

Voraussetzungen

  • Rspamd >= 3.12 mit GPT-Plugin (bei mir aktuell 3.14.0 auf FreeBSD 15.0)
  • Ein OpenAI API-Key (oder kompatibler Endpoint)
  • Grundverständnis von Rspamd Metrics und Actions

OpenAI API-Key anlegen

Wer noch keinen Key hat: Auf platform.openai.com einloggen, unter API Keys einen neuen Service-Account-Key erzeugen. Der Key wird nur einmal angezeigt — sicher ablegen. Den Verbrauch sieht man im Dashboard. Bei gpt-4o-mini und Mailfiltering sind die Kosten minimal.

Die Konfiguration: gpt.conf

Hier meine aktuelle /usr/local/etc/rspamd/local.d/gpt.conf:

enabled = true;
type = "openai";
model = "gpt-4o-mini";
api_key = "GEHEIMER-KEY";

model_parameters {
  gpt-4o-mini {
    max_tokens = 160;
    temperature = 0.0;
  }
}

timeout = 10s;
allow_ham = true;
allow_passthrough = false;
json = true;

prompt = "You are an email spam detector. Analyze the email and respond with ONLY a JSON object, no other text. The JSON must have these fields: "probability" (number 0.00-1.00 where 1.0=spam, 0.0=ham), "reason" (one sentence citing the strongest indicator). Example: {"probability": 0.85, "reason": "Unsolicited offer with urgent language and suspicious links."}  LEGITIMATE patterns: verification emails with codes, transactional emails (receipts, confirmations), newsletter unsubscribe links. Flag as spam only with MULTIPLE red flags: urgent threats, domain impersonation, requests for credentials, mismatched URLs.";


symbols_to_except {
  RCVD_IN_DNSWL_MED   = -0.1;
  RCVD_IN_DNSWL_HI    = -0.1;
  DWL_DNSWL_MED        = -0.1;
  WHITELIST_RECP_ADDR = -0.1;
  BAYES_HAM           = -0.1;
  SPAMTRAP            = 0;
  RCPT_IN_SPAMTRAP    = 0;
  SPAMTRAP_ADDR       = 0;
  RCVD_VIA_SMTP_AUTH  = 0;
  LOCAL_CLIENT        = 0;
  FROM_LOCAL          = 0;
}

Was hat sich gegenüber der alten Version geändert?

json = true und der Custom Prompt

Das ist die wichtigste Änderung. In meiner ursprünglichen Konfiguration stand json = false. Das funktionierte, hatte aber einen Haken: die Antwort des Modells wurde als Freitext geparst, was unzuverlässig war.

Mit json = true aktiviert Rspamd den JSON-Modus. Das Modell wird angewiesen, strukturiertes JSON zurückzuliefern, und der Parser erwartet ein Feld probability in der Antwort.

Und hier kommt der Fallstrick: Der Default-Prompt von Rspamd passt nicht zum JSON-Modus. Er fordert das Modell auf, nummerierte Textzeilen zurückzugeben:

Output ONLY 2 lines:
1. Numeric score: 0.00-1.00
2. One-sentence reason...

Der JSON-Parser erwartet aber:

{"probability": 0.85, "reason": "..."}

Das Ergebnis: cannot convert spam score im Log und GPT_UNCERTAIN(0.00) bei jeder Mail. Das GPT-Modul lief, lieferte aber nie ein verwertbares Ergebnis.

Lösung: ein Custom Prompt, der explizit JSON mit dem probability-Feld verlangt. Damit funktioniert die Kette:

  1. Rspamd sendet Mail + Prompt an OpenAI
  2. OpenAI antwortet mit {"probability": 0.9, "reason": "..."}
  3. Rspamd parst das JSON, findet probability, mappt auf GPT_SPAM/GPT_HAM/GPT_SUSPICIOUS

reason_header entfernt

In der alten Version hatte ich reason_header = "X-GPT-Reason" gesetzt. Das schrieb die GPT-Begründung als eigenen Header in die Mail. Mit json = true ist das nicht mehr nötig — die Reason steckt im JSON und taucht im Rspamd-Log auf. Außerdem entferne ich ohnehin GPT-Header per Milter-Config, damit keine internen Analyse-Details an den Empfänger durchsickern.

symbols_to_except angepasst

Änderungen gegenüber der alten Version:

  • GREYLIST entfernt: Greylisting ist kein Vertrauens-Signal. Eine Mail die Greylisting besteht, kann trotzdem Spam sein. GPT soll diese Mails weiterhin bewerten.
  • BAYES_HAM hinzugefügt: Wenn Bayes die Mail bereits sicher als Ham einstuft, spart man sich den GPT-Call. Sinnvoll für Newsletter und regelmäßige Korrespondenz.
  • SPAMTRAP-Symbole hinzugefügt: Mails an Spamtrap-Adressen brauchen keine GPT-Analyse, die sind per Definition Spam.

Scoring: Gewichte und Thresholds

Die GPT-Symbole und ihre Gewichte in der metrics.conf (bzw. local.d/groups.conf):

symbols {
  GPT_SPAM       { weight = 9.0;  description = "GPT: classified as SPAM"; }
  GPT_SUSPICIOUS { weight = 4.5;  description = "GPT: classified as SUSPICIOUS"; }
  GPT_HAM        { weight = -0.5; one_shot = true; description = "GPT: classified as HAM"; }
}

Warum diese Gewichte?

  • GPT_SPAM (9.0): Kräftig, aber alleine nicht genug zum Rejecten. Erst in Kombination mit anderen Signalen (Bayes, RBL, fehlende Auth) wird der Reject-Threshold erreicht.
  • GPT_SUSPICIOUS (4.5): Schiebt Grenzfälle in Richtung Greylist oder Add-Header. Genau dafür ist GPT am nützlichsten.
  • GPT_HAM (-0.5): Bewusst niedrig und one_shot. GPT soll Spam erkennen, nicht Ham retten.

Dazu die Action-Thresholds:

actions {
  greylist   = 4;
  add_header = 6;
  reject     = 12;
}

Reject-Threshold bei mir: 12 statt Default 15. Das geht, weil die traditionellen Checks (SPF, DKIM, DMARC, RBL, Bayes, DNSBL) bereits solide arbeiten. GPT kommt als zusätzliches Signal obendrauf.

Praxis-Beispiel

Hier eine echte Spam-Mail aus dem Log, bei der GPT korrekt angeschlagen hat:

rspamd_task_write_log: (default: T (reject): [13.83/12.00]
  [BAYES_SPAM(5.10){100.00%;},
   ABUSE_SURBL(5.00){next.schnapper-empfehlung.de:url;...},
   GPT_SPAM(2.40){0.9;},
   FROM_NEQ_ENVFROM(0.50){...},
   FORGED_SENDER(0.30){...},
   ...]

Was man hier sieht:

  • GPT_SPAM(2.40){0.9;} — GPT hat Probability 0.9 (90% Spam) zurückgeliefert. Rspamd mappt den Probability-Wert nicht 1:1 auf das konfigurierte Gewicht, sondern skaliert intern — hier ergeben sich 2.40 von maximal 9.0 Punkten.
  • Zusammen mit BAYES_SPAM (5.10) und ABUSE_SURBL (5.00) kommt die Mail auf 13.83 — deutlich über dem Reject-Threshold von 12.
  • GPT war hier nicht das ausschlaggebende Signal, hat aber zur Gesamtbewertung beigetragen.

Das ist genau das Verhalten, das ich will: GPT als ein Baustein unter vielen, der bei Grenzfällen den Ausschlag geben kann.

Datenschutz

Das muss gesagt werden: Mit diesem Setup fließen Mailinhalte an OpenAI. Wer personenbezogene Daten verarbeitet oder in einem regulierten Umfeld arbeitet, muss prüfen ob das zulässig ist. Alternative: selbst gehostete Modelle über Ollama oder kompatible lokale Endpoints. Rspamd unterstützt das über den type-Parameter.

Für meinen privaten Mailserver ist das Risiko vertretbar — und die Ergebnisse sprechen für sich.

Zusammenfassung

ParameterWertWarum
jsontrueStrukturiertes Parsing, zuverlässiger als Freitext
promptCustomPflicht bei json=true! Default-Prompt liefert Textformat, Parser erwartet JSON
temperature0.0Deterministische Antworten, kein Kreativitäts-Bonus beim Spamfiltern
allow_hamtrueKleines positives Signal für legitime Mails
symbols_to_exceptBAYES_HAM, DNSWL, Whitelists, SMTP_AUTH, SpamtrapsUnnötige API-Calls vermeiden
reason_headernicht gesetztNicht nötig mit json=true, interne Details gehören nicht in den Header

Die wichtigste Erkenntnis: json = true ohne Custom Prompt ist kaputt. Der Default-Prompt und der JSON-Parser sprechen unterschiedliche Sprachen. Wer json = true setzt, muss einen Prompt mitliefern, der JSON mit einem probability-Feld verlangt. Sonst steht im Log cannot convert spam score und GPT liefert nur GPT_UNCERTAIN(0.00).

Volksverschlüsselung wird eingestellt

An mir flog gerade die Information vorbei, dass die Volksverschlüsselung zum 31.01.2026 eingestellt wird. Ich habe mir dort vor ein paar Jahren mal ein S/MIME-Zertifikat für meine E-Mails geholt. Mir hat der Ansatz gefallen, dass man sich mit seinem neuen Personalausweis und dessen Online-Funktion dort legitimieren kann und im Anschluss sein Zertifikat bekommt. Für mich hat diese Abhängigkeit absolut Sinn ergeben. Wir haben ja schon alle einen Perso mit Online-Funktion und PIN und was weiß ich alles. Also warum nicht auch einfach und schnell Zertifikate darüber erstellen?

Screenshot der Meldung zur Einstellung des Dienstes Volksverschlüsselung.

Funktioniert hat das alles wirklich gut – leider mit dem gleichen Problemchen wie auch bei cacert.org: Die Root-Zertifikate sind nicht in den Trust Stores der Betriebssysteme, Browser, Mailclients usw. Signiert man also etwas damit, wird es beim Empfänger als ungültig und unsicher angezeigt, es sei denn, dieser installiert manuell die Root-Zertifikate. Das macht natürlich niemand. Damit war die Volksverschlüsselung für mich genauso raus wie leider auch cacert.org.

Nun hing hinter der Volksverschlüsselung ebenfalls das Fraunhofer-Institut. Meine Hoffnung war, dass über diesen Weg am Ende doch mal die Root-Zertifikate in die Trust Stores kommen. Aber leider nicht.

Die Diskussion, ob man Trust Stores wirklich braucht, ob man sie vor allem vorgefüllt braucht, mache ich genauso wenig auf wie DNSSEC und TLSA, ok? Denn uns allen ist ja inzwischen klar, dass wir es „sicher“ haben könnten – wenn man nur nicht so scheiß viel Geld mit den CAs verdienen könnte. Denn die ganzen CAs zahlen ja schon ein paar Euro, um in die Trust Stores zu kommen 😉

Soviel dann also zur Volksverschlüsselung.

S/MIME-Zertifikat per DNS veröffentlichen – SMIMEA

SMIMEA — S/MIME-Zertifikat per DNS veröffentlichen

Mal wieder soweit: Mein aktuelles S/MIME-Zertifikat zum Signieren von E-Mails läuft aus. Also habe ich mir ein neues besorgt. Da GlobalSign keine Class-2-Zertifikate mehr für Privatpersonen anbietet, musste ich die CA wechseln. Durch Zufall bin ich auf SSLplus gestoßen – die haben echt gute Angebote für alle möglichen Zertifikate. Aber darum soll es in diesem Beitrag nicht gehen.

Wie immer will ich mein Zertifikat öffentlich zugänglich machen, sonst müsste jeder erst eine von mir signierte E-Mail erhalten, bevor er mein Zertifikat hat. Erst dann könnten Absender mir verschlüsselte E-Mails schicken.

Dafür gibt es ein experimentelles RFC 8162, das beschreibt, wie sich ein solches Zertifikat in einer DNSSEC-geschützten Zone veröffentlichen lässt. Natürlich gibt es im Internet wieder zig verschiedene Anleitungen und Wege, um das zu realisieren. Aber nichts wirklich Zuverlässiges, was ich finden konnte. Den DNS-Record für meine Bind9-Zone wieder manuell zu erstellen, hatte ich jedenfalls keine Lust.

Also habe ich zwei kleine Python3-Skripte geschrieben:

smimea_generate_record.py

Erstellt einen kopierbaren RR für die DNS-Zone. Kann interaktiv genutzt werden: Fragt nach E-Mail-Adresse und PEM-Zertifikat. Oder direkt mit Parametern aufgerufen werden. Prüft, ob E-Mail-Adresse und Zertifikat zusammenpassen, und gibt den fertigen Record aus.

./smimea_generate_record.py
Enter the email address: kernel-error@kernel-error.com
Enter the path to the PEM certificate: mail.pem
✅ Email 'kernel-error@kernel-error.com' matches the certificate!

🔹 **Generated BIND9 DNS Record:**

70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._smimecert.kernel-error.com. 3600 IN SMIMEA 3 0 0 (
   30820714308204FCA003020102021073C13C478DA7B114B871F00737F1B0FB30
   0D06092A864886F70D01010B0500304E310B300906035504061302504C312130
   1F060355040A0C1841737365636F20446174612053797374656D7320532E412E
   [... komplettes Zertifikat in Hex ...]
   7573CA35477D59B98DE4852065F58FB60E0E620D3E2F5CAD
   )

smimea_lookup.py

Fragt den SMIMEA-Record im DNS ab, lädt das Zertifikat herunter und prüft es mit OpenSSL auf Gültigkeit. Funktioniert interaktiv oder mit übergebenen Werten.

./smimea_lookup.py
Enter the email address: kernel-error@kernel-error.com

Querying DNS for SMIMEA record:
  70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._smimecert.kernel-error.com

Certificate saved as smimea_cert.der
Certificate successfully retrieved and verified:

Certificate:
    Data:
        Version: 3 (0x2)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = PL, O = Asseco Data Systems S.A., CN = Certum SMIME RSA CA
        Validity
            Not Before: Mar 13 13:41:55 2025 GMT
            Not After : Mar 13 13:41:54 2027 GMT
        Subject: SN = van de Meer, GN = Sebastian, CN = Sebastian van de Meer,
                 emailAddress = kernel-error@kernel-error.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (4096 bit)
        X509v3 Extended Key Usage:
            E-mail Protection, TLS Web Client Authentication
        X509v3 Key Usage: critical
            Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment
        X509v3 Subject Alternative Name:
            email:kernel-error@kernel-error.com

Beide Skripte findet ihr auf GitHub, damit ihr sie nutzen oder verbessern könnt.

Warum viele Anleitungen falsch sind

Warum habe ich geschrieben, dass ich nichts Zuverlässiges finden konnte? Nun, oft stoße ich auf Anleitungen, die noch auf TYPE53 basieren. Das ist nötig, wenn Bind9 den eigentlichen RR-Type noch nicht kennt – also ein klares Zeichen dafür, dass es sich um eine sehr frühe Implementierung handelt.

Ein weiteres häufiges Problem: Der Hash des Local-Parts wird einfach weggelassen. Stattdessen erfolgen die Abfragen direkt auf _smimecert., was aber falsch ist. Ohne den SHA256-Hash des Local-Parts gibt es keine eindeutige Zuordnung zur jeweiligen E-Mail-Adresse.

Aufbau des SMIMEA-DNS-Records

Der erste Teil — der SHA256-Hash — sorgt dafür, dass nicht einfach jeder direkt aus der DNS-Zone die E-Mail-Adressen auslesen kann. Statt die E-Mail-Adresse im Klartext zu speichern, wird nur der SHA256-Hash des Local-Parts (also der Teil vor dem @) genutzt. Wer die genaue E-Mail-Adresse kennt, kann den passenden DNS-Eintrag finden — aber jemand, der blind durch die Zone scannt, sieht nur Hashes.

Der _smimecert-Prefix zeigt an, dass es sich um einen SMIMEA-Record handelt, ähnlich wie bei ._tcp. für SRV-Records oder _acme-challenge. für Let’s Encrypt. Und schließlich kommt die Domain, zu der die E-Mail-Adresse gehört.

Manuelle Abfrage mit dig

Möchte man die Abfrage manuell durchführen, muss man zuerst den Local-Part der E-Mail-Adresse mit SHA256 hashen. Laut RFC 8162, Abschnitt 3.1 wird der Hash auf die ersten 28 Bytes (56 Hex-Zeichen) gekürzt, um die DNS-Label-Längenbeschränkung von 63 Zeichen (RFC 1035, Abschnitt 2.3.4) einzuhalten:

echo -n "kernel-error" | sha256sum | awk '{print $1}' | cut -c1-56
70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0

Anschließend die dig-Abfrage:

dig +dnssec +short 70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._smimecert.kernel-error.com. SMIMEA
3 0 0 30820714308204FCA003020102021073C13C478DA7B114B871F00737
F1B0FB300D06092A864886F70D01010B0500304E310B30090603550406
[... Zertifikat in Hex ...]

Was bedeuten die Felder?

  • 3 — Usage: End-Entity-Zertifikat (DANE-EE), also für die tatsächliche E-Mail-Verschlüsselung und Signatur
  • 0 — Selector: Das komplette Zertifikat wird gespeichert (alternativ: 1 für nur den Public Key)
  • 0 — Matching Type: Keine Hash-Funktion, das Zertifikat liegt im Klartext vor (alternativ: 1 für SHA-256, 2 für SHA-512)
  • Hex-Werte — Der eigentliche Zertifikatsinhalt in hexadezimaler Darstellung

Manuelle Prüfung auf der Konsole

Den kompletten DNS-Record abrufen, die SMIMEA-Parameter (3 0 0) entfernen und als Hex-Datei speichern:

dig +short 70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._smimecert.kernel-error.com SMIMEA | sed 's/^3 0 0 //' | tr -d '[:space:]' > dns_cert.hex

Hex in eine binäre DER-Datei umwandeln und mit OpenSSL anzeigen:

# Hex → DER
xxd -r -p dns_cert.hex dns_cert.der

# Zertifikat anzeigen
openssl x509 -inform DER -in dns_cert.der -text -noout

Verbreitung und Ausblick

SMIMEA ist leider noch immer nicht besonders weit verbreitet. Das liegt daran, dass das RFC noch immer experimental ist, aber auch daran, dass es auf weiteren Techniken aufbaut, die ebenfalls eher selten genutzt werden. Man braucht SMIMEA nur, wenn man überhaupt ein S/MIME-Zertifikat zur Signatur und Verschlüsselung von E-Mails verwendet. Zusätzlich muss die Domain per DNSSEC geschützt sein — und dann muss auch noch der zusätzliche Mehrwert von SMIMEA verstanden werden.

Denn SMIMEA verteilt nicht nur die Zertifikate, sondern macht einen direkt initial verschlüsselt erreichbar. Wenn man der Empfänger einer solchen signierten Nachricht ist, kann man das Zertifikat zudem gegen eine vertrauenswürdige DNS-Zone halten und sich so vergewissern, dass es wirklich die Signatur des Absenders ist — ähnlich wie bei TLSA/DANE.

Die Implementierung ist aktuell sehr überschaubar. Es gibt Milter für beispielsweise Postfix oder Plugins für Thunderbird, aber vor allem im Enterprise-Umfeld ist mir momentan keine funktionierende Lösung bekannt.

Eigentlich wollte ich doch nur schnell schreiben, dass ich da zwei Python-Skripte zusammengebastelt habe — und am Ende ist es doch wieder so ein riesiges Ding geworden. Aber ich denke, vor allem der Teil mit dem gekürzten Hash des Local-Parts ist wichtig zu erklären. Das ist echt eine verrückte Konstruktion. Klar, das hat seinen Sinn, aber zumindest ich bin damals genau an diesem Punkt hängen geblieben.


Das einzig korrekt funktionierende Online-Tool, das ich finden konnte: co.tt/smimea.cgi. Alle anderen sind nicht erreichbar, halten sich nicht ans RFC oder ich war zu blöde, sie zu bedienen. Fragen? Einfach melden.

Arduino und die jammernde Pflanze: Technik trifft Humor

Auf irgendeinem CCC Event bin ich über eine lustige Projektidee einer jammernde Pflanze gestoßen. Diese hat mir und auch meiner größeren Tochter so gut gefallen, dass wir es zusammen nachbauen wollten.

Geöffnetes Gehäuse mit der gesamten Technik für das Arduino-Projekt: Die jammernde Pflanze – Technik trifft Humor

Ziel ist ein kleines „Gerät“, welches den Feuchtigkeitsgehalt der Blumenerde einer Pflanze misst. Ist der Wert zu „trocken“, soll mittels eines kleinen MP3 Players eine Audiodatei abgespielt werden. So bekommt man als Pflanzeneigentümer mit, wenn die Pflanze gegossen werden muss. Damit die Pflanze sich nur beschwert, wenn auch jemand da ist (sonst hört es ja keiner) gibt es noch einen kleinen Bewegungsmelder. Ist also die Blumenerde zu trocken und es wird eine Bewegung erkannt, jammert die Pflanze und schon weiß man, dass die Pflanze Wasser braucht.

Da es das erste Projekt dieser Art für meine Tochter ist, sollte es so übersichtlich und einfach wie möglich sein. Daher wird es auch nicht bis ins letzte Detail ausgeklügelt sein.

Beim Arduino haben wir uns für den Nano entschieden, denn er hat fast die gleichen Möglichkeiten, wie der große UNO, ist aber halt deutlich kleiner. Der MP3 Player ist ein kleines DFPlayer Modul, als Bewegungsmelder arbeitet der HC-SR312 und der normale Feuchtigkeitssensor.

Gestartet haben wir mit einem einfachen Breadboard um die Verschaltung, Modul für Modul, zu setzen und die Ansteuerung mit dem Arduino anhand der Beispiele zu testen. So ist es einfacher nachzuvollziehen und man konnte jedes Modul einfach testen.

Beim DFPlayer haben wir per TTS Texte in verschiedene MP3s umgewandelt und auf der SD Karte im Ordner mp3 gespeichert. Diese MP3s werden random abgespielt, wenn die Erde zu trocken ist und eine Bewegung erkannt wurde.

Nachdem die elektronische Verschaltung klar war und zusammen mit dem Code funktionierte.

Screenshot der Arduino IDE mit dem Code für das Arduino-Projekt: Die jammernde Pflanze – Technik trifft Humor

Haben wir mit KiCad eine kleine Platine designt um diese „drucken“ zu lassen, damit am Ende alle Bauteile auf dieser verlötet werden können. So hat man weniger Kabelsalat und alles ist platzsparend aufgehoben.

Elektrischer Schaltplan für das Arduino-Projekt: Die jammernde Pflanze – Technik trifft Humor

Im Anschluss haben wir noch mittels FreeCAD ein Gehäuse für die Elektronik designt und es mit dem 3D Drucker gedruckt. Die einzelnen Teile haben wir, um es einfach zu halten, jeweils mit einem Tropfen Sekundenkleber fixiert.

Screenshot von FreeCAD 0.19 beim Design des Gehäuses für das Arduino-Projekt: Die jammernde Pflanze – Technik trifft Humor

Inzwischen steckt das Teil in der Blume und meldet sich erfolgreich wenn es Zeit ist, die Blume zu wässern. Da es abhängig von den MP3s auf dem Player ist, was die Pflanze „sagt“… Sind damit lustige Reaktionen fast schon garantiert. Die Pflanze kann dich im Vorbeigehen voll jammern, um Wasser betteln oder beginnen zu schimpfen.

Natürlich wird meine Tochter nach dem Projekt nicht in der Lage sein, dieses vollständig ohne Hilfe zu wiederholen aber die einzelnen Schritte sind klar. Wie so ein Gerät entsteht, was für Punkte im groben nötig sind… All diese Dinge sind nun deutlich transparenter. Schnell findet man Dinge, welche man verbessern könnte. Feuchtigkeitssensor und die restliche Elektronik trennen oder mit einem NodeMCU ESP8266 mit WIFI Statusdaten auf einem Webserver oder so etwas senden. Oder einfach alles mittels Li-Ion Polymer Akkus und einem kleinen BMS unabhängig vom Stromnetz werden. Usw usw usw….

Wenn jemand dieses kleine Projekt selbst nachbauen möchte, kommen ab hier die nötigen Dinge.

Arduino Quellcode für die Jammernde Pflanze:

#include <Arduino.h>
#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>

/* --- Pins ---------------------------------------------------- */
const uint8_t PIN_DF_RX   = 10;
const uint8_t PIN_DF_TX   = 11;
const uint8_t PIN_PIR     = 7;
const uint8_t PIN_SENSOR = A0;

/* --- Parameter ----------------------------------------------- */
const int schwellwert = 380;
const unsigned long PLAY_COOLDOWN_MS = 15000;
const uint8_t TRACK_JAMMERN = 1;

/* --- Objekte ------------------------------------------------- */
SoftwareSerial dfSerial(PIN_DF_RX, PIN_DF_TX);
DFRobotDFPlayerMini dfPlayer;

/* --- Laufzeitstatus ------------------------------------------ */
unsigned long lastPlay = 0;

/* ------------------------------------------------------------- */

void setup() {
  pinMode(PIN_PIR, INPUT);

  Serial.begin(115200);
  dfSerial.begin(9600);

  Serial.println(F("Initializing DFPlayer ..."));

  if (!dfPlayer.begin(dfSerial)) {
    Serial.println(F("DFPlayer init failed"));
    while (true);
  }

  dfPlayer.volume(20);
  dfPlayer.outputDevice(DFPLAYER_DEVICE_SD);
  dfPlayer.EQ(DFPLAYER_EQ_NORMAL);
  dfPlayer.setTimeOut(500);

  Serial.println(F("DFPlayer Mini online"));
}

/* ------------------------------------------------------------- */

void loop() {
  /* DFPlayer Events immer zuerst abholen */
  if (dfPlayer.available()) {
    handleDFPlayerEvent(dfPlayer.readType(), dfPlayer.read());
  }

  int messwert = analogRead(PIN_SENSOR);
  bool bewegung = digitalRead(PIN_PIR) == HIGH;
  bool trocken = messwert > schwellwert;

  unsigned long now = millis();

  if (trocken && bewegung) {
    if (now - lastPlay >= PLAY_COOLDOWN_MS) {
      Serial.println(F("Bewegung + Erde trocken -> spiele Sound"));
      dfPlayer.play(TRACK_JAMMERN);
      lastPlay = now;
    }
  } else {
    logStatus(trocken, bewegung, messwert);
  }

  delay(100);  // leichte Entlastung – kein Logik-Delay
}

/* ------------------------------------------------------------- */

void logStatus(bool trocken, bool bewegung, int messwert) {
  if (!bewegung && trocken) {
    Serial.print(F("Keine Bewegung, Erde trocken: "));
  } else if (bewegung && !trocken) {
    Serial.print(F("Bewegung, Erde ok: "));
  } else if (!bewegung && !trocken) {
    Serial.print(F("Keine Bewegung, Erde ok: "));
  }
  Serial.println(messwert);
}

/* ------------------------------------------------------------- */

void handleDFPlayerEvent(uint8_t type, int value) {
  if (type == DFPlayerPlayFinished) {
    Serial.print(F("Track "));
    Serial.print(value);
    Serial.println(F(" beendet"));
  }
}

Für den DFPlayer benötigt man noch die nötige library, diese gibt es zum Beispiel bei GitHub und wird einfach lokal unter /home/USERNAME/Arduino/libraries abgelegt.

Die STL Dateien für den 3D Drucker gibt es hier:

STL File Gehäuse / STL File Deckel

Eine Amazon Einkaufsliste haben wir hier:

Optional oder zum Test:

Dann fehlen nur noch die >>Gerber Dateien<< zum bestellen der Platine.

Windows Server mit Exchange: SSL Labs A+ erreichen

Qualis A+ Icon.

Ich hatte hier einen Windows Server 2012 R2 mit Exchange 2016 stehen. Out of the Box sprach das Ding TLS 1.0, SSLv3 und RC4. Da gehen einem die Haare hoch. Kann man so ein System auf ein A+ bei Qualys SSL Labs bekommen und es funktioniert danach noch? Kleiner Spoiler: Ja, geht.

Ich muss zugeben, Microsoft-Produkte strengen mich in dieser Hinsicht immer an. Die haben ihre Daseinsberechtigung, keine Frage. Aber TLS-Hardening unter Windows fühlt sich an wie Zahnmedizin mit Handschuhen aus Pappe.

Hinweis: Windows Server 2012 R2 und Exchange 2016 sind inzwischen End of Life. Der Ansatz (Registry-Änderungen für TLS, HSTS im IIS) funktioniert auf Windows Server 2016/2019/2022 aber genauso.

HSTS im IIS setzen

Für ein A+ braucht man neben sauberen Ciphern und Protokollen auch HTTP Strict Transport Security (HSTS). Das ist im Grunde nur ein HTTP-Response-Header. Im IIS-Manager konfiguriert man ihn ganz oben auf Server-Ebene, damit er überall vererbt wird:

IIS-Manager → HTTP-Antwortheader → Hinzufügen:

Screenshot der Internetinformationsdienste (IIS)-Manager.
Name:  strict-transport-security
Wert:  max-age=31536000; includeSubdomains

TLS-Protokolle und Cipher härten

Jetzt der eigentlich spannende Teil. Man muss eine ganze Reihe von Registry-Änderungen vornehmen: MD5 und RC4 deaktivieren, SSLv3, TLS 1.0 und TLS 1.1 abschalten, TLS 1.2 aktivieren, schwache Cipher raus und eine sinnvolle Cipher-Reihenfolge vorgeben. Ich habe dafür ein Registry-File vorbereitet. Herunterladen, ausführen, Server neu starten (Microsoft halt).

Download: Registry-File Download

Ergebnis

Qualis A+ Wertung.
Qualis Wertung mit Blick auf Cipher Suites und Protocols.

A+ steht. Ich hätte gerne noch schönere Cipher gehabt, aber mehr war mit diesem Setup nicht drin. Immerhin: Kein RC4, kein 3DES, kein TLS unter 1.2. Und Exchange funktioniert danach noch, was bei Microsoft-Produkten ja nie selbstverständlich ist.

Wer auch die VPN-Cipher auf dem Windows Server härten will, findet die Anleitung im Beitrag RRAS mit sicheren Cipher Suites. Fragen? Einfach melden.

Bash-Skript: Automatisiertes Erstellen von Abuse-E-Mails

Wenn man ein System mit öffentlicher IP Adresse hat, klopfen irgendwelche Bots oder ein Script Kiddie direkt nach dem Einschalten die Dienste auf mögliche Schwachstellen, schwache Zugangsdaten oder default logins ab. Kennt wohl jeder…

Davor schützt gepatchte Software, Dienste möglichst unerreichbar machen oder ein IDS (Intrusion detaction system). Im einfachsten Fall ist dieses Fail2Ban.

Screenshot der Konsolenausgabe vom Abuse Scipt.

Was tun, wenn Fail2Ban Brute Force Angriffe auf SSH oder Postfix (Mailserver) feststellt? Tjo, die Adresse an weiteren Versuchen hindern natürlich. OK, darum kümmert sich Fail2Ban… Am besten noch so, das Fail2Ban dieses seinen anderen Kisten ebenfalls mitteilt, dann wird diese IP es dort schon nicht mehr probieren können. Ebenfalls könnte es eine gute Idee sein, dieses Verhalten der IP Adresse irgendwo zu melden. Dienste wie AbuseIPDB lassen sich gut mit Fail2Ban verbinden. So können andere Admins auf der Welt direkt davon profitieren.

Könnte man nicht zusätzlich noch den ISP oder Hoster hinter der „bösen“ IP Adresse informieren? Meist ist es ja ein Root-Server oder VPS oder, oder… Vielleicht weiß der Admin ja noch nichts von seinem kompromittierten Glück?!? Ja kann man. Dazu sollte es im whois zur IP Adresse immer eine „abuse-mailbox“ also einen „abuse-contact“ geben.

Als kleines Beispiel:

➜  ~ whois 8.8.8.8|grep -i abuse
Comment:        All abuse reports MUST include: 
OrgAbuseHandle: IPADD5-ARIN
OrgAbuseName:   ipaddressing
OrgAbusePhone:  +1-877-453-8353 
OrgAbuseEmail:  ipaddressing@level3.com
OrgAbuseRef:    https://rdap.arin.net/registry/entity/IPADD5-ARIN
Comment:        Please note that the recommended way to file abuse complaints are located in the following links. 
Comment:        To report abuse and illegal activity: https://www.google.com/contact/
OrgAbuseHandle: ABUSE5250-ARIN
OrgAbuseName:   Abuse
OrgAbusePhone:  +1-650-253-0000 
OrgAbuseEmail:  network-abuse@google.com
OrgAbuseRef:    https://rdap.arin.net/registry/entity/ABUSE5250-ARIN

An diese Adresse kann man sich nun mit seinem Anliegen wenden. Damit der angeschriebene ~Admin~ etwas mit der Meldung anfangen kann sollte sie die folgenden Informationen enthalten:

– die IP Adresse des eigenen Systems.
– die IP Adresse von welcher die Störungen kommen/gekommen sind.
– Ein paar Zeilen aus dem Logfile um das Problem verständlich zu machen.
– Die Zeitzone des eigenen Systems, damit die Datums und Zeitangaben im Logfile sinnvoll nutzbar sind.
– Eine kurze Beschreibung, warum man dieses als Problem empfindet.

Da ein Hoster diesen Abuse natürlich an seinen Kunden weitergeben wird und dieser möglicherweise noch Fragen dazu haben könnte, hilft es, wenn man direkt erlaubt die Kontaktdaten weiter zu geben.

Leider findet sich die AbuseMailbox im whois nicht immer und vor allem nicht einheitlich. Gibt man sich nun die Mühe und sammelt alle nötigen Informationen zusammen, wird man nach dem Absender der eigentlichen AbuseMail oft enttäuscht. Denn der überwiegende Teil dieser E-Mails landet einfach beim Empfänger in /dev/null. Rückmeldungen bekommt man fast nie und oft hat es einfach überhaupt keinen Effekt. Klar hin und wieder hilft es jemandem… Hier und da schiebe ich selbst so einen Abuse an. Dabei möchte ich selbstredend so wenig wie möglich Arbeit in das Thema stecken.

Da mich Systeme wie Fail2Ban direkt mit dem jeweiligen whois zur IP Adresse informieren, brauche ich nur noch etwas, was mir die anderen Informationen einsammelt und am besten direkt abschickt. Dafür habe ich folgendes bash script geschrieben.

An dieses kann ich direkt beim Aufruf durch andere scripte oder durch mich die „böse“ IP Adresse und die E-Mail-Adresse der AbuseMailbox übergeben. Das script prüft dann ob es sich um eine brauchbare E-Mail-Adresse und IP handelt, sammel zum Beispiel für SSH Brute Force die nötigen Informationen aus dem auth.log, generiert mir dann meine AbuseMail und sendet diese nach einer Sichtkontrolle ab. Ich selbst bekomme diese E-Mail dann noch als BCC. Das script ist alt, stumpf und sehr einfach. Weil die Frage nach dem script kam, hier das script:

#!/usr/local/bin/bash

if [ "$1" == "-h" ] ; then
    printf "You can add abuse ip and mail on start or interactive by request."
    printf "Usage 1: abusemail.sh 1.2.3.4 example@example.org"
    printf "Usage 2: abusemail.sh"
    exit 0
fi

myip="1.2.3.4"
bcc="my@mail.com"


function validateMail()
 {
         local mail=$1
         local stat=1
         if [[ "$mail" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$ ]]; then
                stat=0
        fi
        return $stat
}

function validateIP()
 {
         local ip=$1
         local stat=1
         if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
                OIFS=$IFS
                IFS='.'
                ip=($ip)
                IFS=$OIFS
                [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
                && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
                stat=$?
        fi
        return $stat
}
clear
printf "\x1b[31;1;4mCreate und send abuse e-mail!\x1b[0m\n\n"
printf "Enter IP Address"

if [ "$1" == "" ] ; then
    read ip
else
    ip=$1
fi

validateIP $ip

if [[ $? -ne 0 ]];then
  printf "Invalid IP Address ($ip)"
  exit 1
else
  printf "$ip ==> \x1b[32;1;4mOK\x1b[0m\n\n"
fi
printf "\n"
printf "Enter Abuse E-Mail Address"

if [ "$1" == "" ] ; then
    read mail
else
    mail=$2
fi

validateMail $mail

if [[ $? -ne 0 ]];then
  printf "Invalid Abuse E-Mail Address ($mail)"
  exit 1
else
  printf "$mail  ==> \x1b[32;1;4mOK\x1b[0m"
fi

printf "Dear abuse team,\n\nI got some SSH connections from one of your IPv4 addresses. It seems to\nbe a bot or something....\n\nYes, you can forward this E-Mail with contact informations to your \ncustomer to solve this case, if necessary.\n\nMy timezone: Europe/Berlin\nMy IPv4: $myip\nYour IPv4: $ip\n\nSome log snippets:\n" > /tmp/abusemail
grep $ip /var/log/auth.log >> /tmp/abusemail
printf "\nBest regards\nSebastian van de Meer\n" >> /tmp/abusemail


printf "\n\n\n\x1b[31;1;4mCheck e-mail before sending!\x1b[0m\n\n"
printf "\x1b[32;1;4mRecipient:\x1b[0m $mail\n\n"
printf "\x1b[32;1;4mSubject:\x1b[0m Abuse against: $ip - Brute Force\n\n"
cat /tmp/abusemail
printf "\n\n"

while true
do
 printf "\x1b[33;1;4mSend Abuse Mail? [Y/n]\x1b[0m\n"
 read -r input
 
 case $input in
     [yY][eE][sS]|[yY])
 cat /tmp/abusemail | /usr/local/bin/mailx -s "Abuse against: $ip - Brute Force" -r "My Name <myfrom@address.com>" -b $bcc $mail
 printf "Abuse Mail is on it´s way..."
 break
 ;;
     [nN][oO]|[nN])
 printf "Abuse Mail aborted"
 break
        ;;
     *)
 printf "Invalid input..."
 ;;
 esac
done

Noch Fragen? Dann fragen!

Rspamd: Automatisches Spam/Ham-Lernen mit Dovecot und IMAPSieve

Rspamd hat ein Webinterface. Da kann man E-Mails reinkopieren und als Spam oder Ham markieren. Klingt erstmal praktisch. Ist es aber nicht. Niemand kopiert ernsthaft den Quellcode jeder fehlklassifizierten Mail in ein Webformular. Das macht man einmal zum Testen und dann nie wieder.

Automatisches Spam-Training mit Rspamd über Dovecot IMAPSieve – Mail wird zwischen Inbox und Junk verschoben

Was man eigentlich will: Wenn ein Benutzer eine Mail in den Junk-Ordner verschiebt, soll rspamd das automatisch als Spam lernen. Und wenn eine Mail aus dem Junk-Ordner rausgeholt wird, soll rspamd sie als Ham lernen. Kein Webinterface, kein manueller Eingriff. Der Benutzer sortiert einfach seine Mails — und rspamd lernt mit.

Genau das geht mit Dovecot und IMAPSieve. Hier beschreibe ich, wie ich das bei mir eingerichtet habe. Die Konfiguration läuft seit Mai 2020 unverändert — über sechs Jahre, ohne eine einzige Anpassung. Das darf man ruhig als stabil bezeichnen.

Was passiert da eigentlich

Der Datenfluss ist simpel:

  • Benutzer verschiebt eine Mail in den Ordner „Junk“
  • Dovecot erkennt die Verschiebung per IMAPSieve
  • IMAPSieve startet ein Sieve-Script
  • Das Sieve-Script ruft ein Shell-Script auf
  • Das Shell-Script übergibt die Mail per rspamc an rspamd
  • Rspamd lernt die Mail als Spam (Bayes-Klassifikator)

In die andere Richtung genauso: Mail raus aus Junk, Dovecot erkennt es, rspamd lernt Ham. Egal ob der Benutzer über Thunderbird, Roundcube, ein Smartphone oder was auch immer sortiert — solange es IMAP ist, greift das.

Voraussetzungen

  • Dovecot mit Sieve-Support (dovecot-pigeonhole unter FreeBSD, dovecot-sieve unter Debian/Ubuntu)
  • Rspamd mit laufendem Controller-Worker
  • rspamc CLI-Tool (kommt mit rspamd mit)

Mein Setup läuft auf FreeBSD. Die Pfade beginnen daher mit /usr/local/. Unter Linux ist es /etc/dovecot/ statt /usr/local/etc/dovecot/ und /usr/lib/dovecot/ statt /usr/local/libexec/dovecot/. Ansonsten ist alles identisch.

Mein rspamd läuft in einer eigenen Jail und lauscht auf 127.0.0.3:11334. Wer rspamd lokal auf dem gleichen System hat, nimmt stattdessen 127.0.0.1:11334 oder den Unix-Socket.

Dovecot konfigurieren

Zuerst muss das Sieve-Plugin für IMAP aktiviert werden.

20-imap.conf:

protocol imap {
  mail_plugins = $mail_plugins sieve
}

Dann die IMAPSieve-Konfiguration. Hier wird festgelegt, welche Ordner-Aktionen welches Sieve-Script auslösen.

90-plugin.conf:

plugin {
  sieve_plugins = sieve_imapsieve sieve_extprograms

  # Wenn eine Mail in den Junk-Ordner kopiert oder dort ein Flag geaendert wird
  imapsieve_mailbox1_name = Junk
  imapsieve_mailbox1_causes = COPY FLAG
  imapsieve_mailbox1_before = file:/usr/local/etc/dovecot/sieve/report-spam.sieve

  # Wenn eine Mail AUS dem Junk-Ordner woanders hin verschoben wird
  imapsieve_mailbox2_name = *
  imapsieve_mailbox2_from = Junk
  imapsieve_mailbox2_causes = COPY
  imapsieve_mailbox2_before = file:/usr/local/etc/dovecot/sieve/report-ham.sieve

  sieve_pipe_bin_dir = /usr/local/libexec/dovecot

  sieve_global_extensions = +vnd.dovecot.pipe
}

Zwei Trigger: Einer für „Mail landet im Junk“ (→ Spam lernen), einer für „Mail verlässt Junk“ (→ Ham lernen). COPY deckt Verschieben ab, FLAG fängt den Fall ab, dass ein Mail-Client den Junk-Status per Flag statt per Verschieben setzt.

Sieve-Scripts

Jetzt die beiden Sieve-Scripts, die von IMAPSieve aufgerufen werden.

report-spam.sieve — wird ausgelöst, wenn eine Mail im Junk-Ordner landet:

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "imap4flags"];

if environment :is "imap.cause" "COPY" {
    pipe :copy "sa-learn-spam.sh";
}

# Beantworteten oder weitergeleiteten Spam ebenfalls lernen
elsif anyof (allof (hasflag "\\Answered",
                    environment :contains "imap.changedflags" "\\Answered"),
             allof (hasflag "$Forwarded",
                    environment :contains "imap.changedflags" "$Forwarded")) {
    pipe :copy "sa-learn-spam.sh";
}

Der erste Block fängt das normale Verschieben ab. Der zweite Block ist für einen Sonderfall: Wenn jemand auf eine Mail im Junk-Ordner antwortet oder sie weiterleitet, ändert sich das Flag — und auch das sollte als Spam gelernt werden.

report-ham.sieve — wird ausgelöst, wenn eine Mail den Junk-Ordner verlässt:

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.mailbox" "*" {
  set "mailbox" "${1}";
}

if string "${mailbox}" [ "Trash", "train_ham", "train_prob", "train_spam" ] {
  stop;
}

pipe :copy "sa-learn-ham.sh";

Hier passiert etwas Wichtiges: Bevor die Mail als Ham gelernt wird, prüfen wir wohin sie verschoben wurde. Wenn sie im Papierkorb landet, war das vermutlich kein „Das ist kein Spam“ sondern ein „Ich lösche den Spam“. Deshalb: stop; für Trash und die Trainingsordner. Nur wenn die Mail in einen echten Ordner verschoben wird, ist es ein Ham-Signal.

Beide Scripts müssen kompiliert werden:

sievec /usr/local/etc/dovecot/sieve/report-spam.sieve
sievec /usr/local/etc/dovecot/sieve/report-ham.sieve

Shell-Scripts für rspamc

Die Sieve-Scripts rufen Shell-Scripts auf, die die Mail per rspamc an rspamd übergeben. Simpel — jeweils ein Einzeiler.

/usr/local/libexec/dovecot/sa-learn-spam.sh:

#!/bin/sh
exec /usr/local/bin/rspamc -h 127.0.0.3:11334 learn_spam

/usr/local/libexec/dovecot/sa-learn-ham.sh:

#!/bin/sh
exec /usr/local/bin/rspamc -h 127.0.0.3:11334 learn_ham

Die Dateinamen sa-learn-* kommen historisch von SpamAssassin. Verwirrend, wenn man rspamd nutzt. Man könnte sie auch rspamd-learn-spam.sh nennen — funktional ist es egal. Ich habe sie so gelassen, weil man funktionierende Dinge nicht anfasst.

Beide ausführbar machen:

chmod +x /usr/local/libexec/dovecot/sa-learn-spam.sh /usr/local/libexec/dovecot/sa-learn-ham.sh

Wer rspamd lokal laufen hat, ersetzt 127.0.0.3 durch 127.0.0.1 oder nutzt den Unix-Socket (-h /var/run/rspamd/rspamd.sock). Unter Linux liegen die Scripts in /usr/lib/dovecot/ statt /usr/local/libexec/dovecot/. Der Pfad in sieve_pipe_bin_dir muss natürlich dazu passen.

Wichtig: Damit rspamc ohne Passwort trainieren darf, muss die IP im rspamd Controller-Worker als vertrauenswürdig eingetragen sein. In /usr/local/etc/rspamd/local.d/worker-controller.inc (FreeBSD) bzw. /etc/rspamd/local.d/worker-controller.inc (Linux):

secure_ip = "127.0.0.0/8";
secure_ip = "::1";

Ohne das schlägt rspamc learn_spam mit einem Authentifizierungsfehler fehl. Bei Jail-Setups wie meinem muss die Jail-IP (127.0.0.3) in der Liste stehen.

Testen

Dovecot neu laden:

service dovecot reload

Dann eine beliebige Mail in den Junk-Ordner verschieben und im rspamd-Log nachschauen:

rspamd_controller_learn_fin_task: <127.0.0.3> learned message as spam: MESSAGE-ID

Mail wieder raus aus Junk in den Posteingang:

rspamd_controller_learn_fin_task: <127.0.0.3> learned message as ham: MESSAGE-ID

Wenn das im Log steht, funktioniert alles. Kein Neustart nötig, kein Cache-Flush, kein Warten.

Wie viel Training braucht rspamd

Rspamd nutzt einen Bayes-Klassifikator. Der braucht eine Mindestmenge an gelernten Nachrichten, bevor er aktiv wird. Die Standardeinstellung ist 200 — also mindestens 200 Spam-Mails und 200 Ham-Mails. Vorher ignoriert rspamd die Bayes-Ergebnisse komplett.

Das klingt nach viel, geht aber schneller als man denkt. Wer ein paar Dutzend Benutzer auf dem Server hat, kommt da in wenigen Wochen hin. Und danach wird rspamd mit jeder sortierten Mail ein bisschen besser.

Den aktuellen Stand kann man jederzeit prüfen:

rspamc stat

Unter Statfile sieht man wie viele Nachrichten rspamd bereits gelernt hat.

Rspamd trainiert standardmäßig einen globalen Bayes-Klassifikator — alle Benutzer lernen in denselben Pool. Wer das pro Benutzer trennen will, setzt in der classifier-bayes.conf:

per_user = true;

Für die meisten Setups mit einer Handvoll Domains ist der globale Pool sinnvoller — mehr Trainingsdaten, schneller gute Ergebnisse.

Hinweise

Die Konfiguration ist stabil — Dovecot-Updates, rspamd-Updates, FreeBSD-Upgrades, alles durchgelaufen ohne Anpassung.

Wer rspamd danach noch eine Stufe weiter bringen will: Ich habe einen eigenen Beitrag geschrieben, wie man GPT-basierte Spam-Erkennung in rspamd integriert. Das läuft zusätzlich zum Bayes-Klassifikator und fängt die Mails ab, die durch das statistische Netz rutschen.

Fragen? Schreib mir über die Kontaktseite.

internet.nl verschärft die TLS-Anforderungen für Mailserver

E-Mail Test der Niederlande für die E-Mail Domain kernel-error.de
Die Domain kernel-error.de ist in der Hall of Fame der niederländischen IT Security Tests.

Der niederländische internet.nl Mailserver-Test hat die Anforderungen verschärft. Die neuen Guidelines orientieren sich an den aktuellen niederländischen IT-Sicherheitsrichtlinien und ziehen die Messlatte deutlich an.

Was sich geändert hat

TLS 1.0 und 1.1 geben jetzt Abzug. Vorher wurden sie toleriert, jetzt gibt es eine explizite Warnung. Schwache Diffie-Hellman-Parameter und weitere veraltete Cipher sind herausgefallen. Alles um TLS 1.3 den Weg zu ebnen.

Die Hall of Fame hat eine neue Sektion für Champions bekommen: Domains die sowohl beim Webserver als auch beim Mailserver 100 Prozent erreichen.

Warum das relevant ist

internet.nl ist kein akademisches Testtool. Die Ergebnisse fließen in die Bewertung niederländischer Behörden und Dienstleister ein. Wenn dort die Standards steigen, zieht das langfristig auch die Anforderungen an internationale Kommunikationspartner nach oben. Wer mit niederländischen Behörden oder Unternehmen per E-Mail kommuniziert, sollte seinen Score im Auge behalten.

Fragen? Einfach melden.

Automatische E-Mail-Archivierung mit cleanup-maildir und Dovecot

IMAP-Postfächer wachsen mit der Zeit. Irgendwann hat man tausende Mails im Posteingang und in Sent, die Suche wird langsam und die Übersicht geht verloren. cleanup-maildir ist ein Python-Script das Mails nach Alter automatisch in Archiv-Ordner sortiert. Es läuft als Cronjob und arbeitet direkt auf dem Maildir-Verzeichnis.

Installation

# FreeBSD
pkg install cleanup-maildir

# Debian/Ubuntu
pip install cleanup-maildir

Grundlegender Aufruf

Alle Mails die älter als 365 Tage sind aus der Inbox ins Archiv verschieben:

# Inbox archivieren
sudo -u vmail cleanup-maildir --age=365 \
  --archive-folder='Archive.Inbox' \
  --maildir-root='/var/mail/vhosts/example.com/user' \
  archive ''

# Gesendete archivieren
sudo -u vmail cleanup-maildir --age=365 \
  --archive-folder='Archive.Sent' \
  --maildir-root='/var/mail/vhosts/example.com/user' \
  archive 'Sent/'

--age=365 fasst nur Mails an die älter als ein Jahr sind. --archive-folder gibt den Zielordner im IMAP-Postfach an. archive ist der Modus (alternativ delete). Der leere String '' steht für die Inbox, 'Sent/' für die gesendeten Mails.

Das Script legt automatisch Unterordner nach Jahr und Monat an. Die Struktur im Postfach sieht dann so aus:

Archive/
├── Inbox/
│   ├── 2024-01/
│   ├── 2024-02/
│   └── ...
└── Sent/
    ├── 2024-01/
    ├── 2024-02/
    └── ...

Cronjob

Als Cronjob einmal pro Nacht laufen lassen. Wichtig ist dass der Cronjob als der Benutzer läuft der Zugriff auf die Maildir-Verzeichnisse hat:

# /etc/crontab
30 3 * * * vmail cleanup-maildir --age=365 --archive-folder='Archive.Inbox' --maildir-root='/var/mail/vhosts/example.com/user' archive ''

Mehrere Postfächer mit LDAP

Bei mehreren Postfächern will man nicht für jeden Benutzer einen eigenen Cronjob-Eintrag pflegen. Ein Wrapper-Script kann die Benutzer, Maildir-Pfade und die Option ob archiviert werden soll aus dem LDAP holen. Das LDAP-Attribut (z.B. autoarchive=1) steuert pro Benutzer ob die Archivierung aktiv ist. So lässt sich die Archivierung zentral verwalten ohne auf jedem Mailserver Cronjobs anzupassen.

Python 3.11+ und kaputte Header

Ab Python 3.11 nutzt cleanup-maildir den strikten RFC-Header-Parser aus email.policy.default. Das führt zu einem Problem: Mails mit fehlerhaften Headern (z.B. Microsoft Exchange Message-IDs wie <[b378dfc5...]@microsoft.com>) lassen das Script mit einem IndexError abstürzen. Alle Mails nach der fehlerhaften werden nicht mehr verarbeitet.

Den Fix dafür habe ich als Pull Request eingereicht. Ein _safe_header()-Wrapper fängt Parse-Fehler ab und überspringt kaputte Header, statt das ganze Script abzubrechen. Bei mir hat das Script vorher bei Mail #8 aufgehört, danach liefen alle 2.986 Mails sauber durch.

Fragen? Einfach melden.

« Ältere Beiträge

© 2026 -=Kernel-Error=-RSS

Theme von Anders NorénHoch ↑