IT security, FreeBSD, Linux, mail server hardening, post-quantum crypto, DNS, retro computing & hands-on hardware hacks. Privater Tech-Blog seit 2003.

Schlagwort: DMARC (Seite 1 von 2)

RFC 9989 löst RFC 7489 ab: was sich für DMARC wirklich ändert

Beitragsbild zu RFC 9989 und DMARC, Vergleich mit RFC 7489, neuen DMARC-Tags und E-Mail-Sicherheitsdarstellung

Stefan Haun von cyberscale.io hat mir per Mail einen Hinweis geschickt: mein alter Beitrag zu DMARC bezieht sich auf RFC 7489, das seit Mai 2026 durch RFC 9989 abgelöst ist. Der Hinweis kam, so fühlt es sich für mich an, ggf. im Rahmen von Linkbuilding, mit Verweis auf seinen eigenen Guide zu DKIM und DMARC. Das schmälert die fachliche Korrektheit des Hinweises nicht, deshalb hier der ehrliche Dank und der längst überfällige Nachtrag. Mitbekommen hatte ich die neue RFC schon vor einer Weile, geschrieben aber noch nichts, weil es bei einem frischen RFC erstmal eine Zeit braucht, bis sich das in der Praxis herumspricht und Implementierungen nachziehen. Genau dieser Verzug taucht unten beim Praxis-Check nochmal auf.

Was RFC 9989 überhaupt ist

In der Community läuft das Projekt unter dem Namen DMARCbis. Anders als der Name vermuten lässt, ist daraus keine einzelne neue RFC geworden, sondern drei:

  • RFC 9989, die Kernspezifikation: Policy, Alignment, die Tags im Record.
  • RFC 9990, die aggregierten Reports, also die täglichen XML-Berichte.
  • RFC 9991, die Failure Reports, das ARF-Format pro Nachricht.

Alle drei tragen offiziell „Obsoletes: 7489“ im Kopf, RFC 9989 zusätzlich noch „Obsoletes: 9091“ (der 2021 veröffentlichte, experimentelle Vorläufer, der np und die Public-Suffix-Domain-Idee einführte und dessen Inhalt RFC 9989 jetzt vollständig integriert und ersetzt). Dazu ein Reifegrad-Sprung, der eine eigene Erwähnung wert ist: RFC 7489 war „Informational“, RFC 9989 ist „Standards Track“, also auf dem offiziellen Weg zum Internet Standard. DMARC verlässt damit den Status der informellen Beschreibung eines etablierten Verfahrens und wird zu einer echten IETF-Spezifikation.

Die Tags: drei raus, drei gegenüber RFC 7489 neu

Der eigentliche technische Kern steckt in der Menge der Policy-Tags im Record. Drei Tags sind aus der aktiven Spezifikation gestrichen (in der IANA-Registry stehen sie weiterhin, nur als historic markiert), drei sind gegenüber RFC 7489 neu, wobei np davon eigentlich ein Wiedersehen ist:

Entfernt (RFC 7489)Neu gegenüber RFC 7489 (RFC 9989)
pct, Prozent-Rollout der Policyt, expliziter Test-Modus
rf, Report-Format, quasi nie genutztnp, Policy für nicht existierende Subdomains, importiert aus RFC 9091
ri, Report-Intervall, von Empfängern ignoriertpsd, Public-Suffix-Domain-Flag

rf und ri werden von einem RFC-9989-konformen Empfänger nicht mehr ausgewertet, RFC 9989 selbst nennt für die Streichung keine ausführlichere Begründung als das Fehlen praktischer Relevanz. pct ist der interessantere Fall, dazu gleich mehr.

t: der Testmodus, der pct ersetzt

t=n ist der Default und bedeutet normale Durchsetzung. t=y setzt die in p, sp oder np konfigurierte Policy eine Stufe herab: aus reject wird quarantine, aus quarantine wird none. Reports laufen dabei unverändert weiter, man sieht also weiterhin, wer durchfallen würde. RFC 9989 selbst nennt t ausdrücklich nur einen Ersatz für einen Teil der alten pct-Funktionalität, nämlich für die beiden Randwerte: t=n und t=y sollen sich bei Empfängern und Zwischenstationen analog zu pct=100 und pct=0 verhalten, nicht zu jedem beliebigen Prozentwert dazwischen. pct=0 unter RFC 7489 führte nach Abschnitt 6.6.4 bei reject ebenfalls schon zu quarantine statt zu gar keiner Reaktion, t=y übernimmt diesen einen Spezialfall, nur ohne Prozentrechnung. Für einen echten Teil-Rollout mit Werten zwischen 0 und 100 gibt es dagegen keinen direkten Nachfolger, dazu unten mehr.

np: eine eigene Stufe für nicht existierende Subdomains

Unter RFC 7489 galt für Mail von einer Subdomain, egal ob sie einen eigenen DNS-Eintrag hat oder nicht, immer dieselbe Regel: sp, falls gesetzt, sonst p. Eine Subdomain, die gar nicht existiert, zum Beispiel irgendwas.example.com, wo irgendwas nie angelegt wurde, bekam also exakt dieselbe Policy wie eine echte, konfigurierte Subdomain. Genau das ist ein bekanntes Spoofing-Einfallstor, weil sich beliebige, nie existierende Namen fälschen lassen, für die es nie eine eigene Policy gab. np schafft jetzt erstmals eine eigene, von sp und p unterscheidbare Stufe genau für diesen Fall. Neu erfunden ist das Tag dabei nicht: RFC 9989 übernimmt es wörtlich aus dem 2021 veröffentlichten, experimentellen RFC 9091, das mit RFC 9989 ebenfalls obsolet wird, und macht daraus einen festen Bestandteil der Hauptspezifikation. Fehlt np weiterhin, fällt die Regel auf sp zurück, falls gesetzt, sonst auf p, genau wie in RFC 9989 Abschnitt 4.7 definiert. Wer schon vorher ein striktes sp=reject gesetzt hatte, bekommt also weiterhin exakt das gleiche Verhalten wie zuvor, nur jetzt mit der Möglichkeit, es bei Bedarf davon zu lösen.

DNS-Tree-Walk statt Public Suffix List, psd markiert die Grenze

Die eigentliche Änderung ist strukturell: RFC 9989 löst die Bestimmung der Organizational Domain von der statischen, extern gepflegten Public Suffix List und ersetzt sie durch einen DNS-Tree-Walk-Algorithmus. psd (Werte y, n, u, Default u) liefert dafür die expliziten Grenzmarkierungen: psd=y setzt ein Public Suffix Operator auf seinem eigenen Record, um zu sagen, dass darunter fremde, eigenständige Organizational Domains beginnen, psd=n sagt umgekehrt, dass genau diese Domain die Organizational Domain für sich und ihre Subdomains ist. Relevant ist das vor allem für Registries und Hoster, die selbst öffentlich registrierbare Second-Level-Domains betreiben, also Strukturen wie bei .co.uk. RFC 9989 weist ausdrücklich darauf hin, dass Public-Suffix-List-Auflösung und Tree-Walk in Einzelfällen zu unterschiedlichen Ergebnissen kommen können. Für eine gewöhnliche Domain wie diese hier, ohne PSO-Rolle, ändert sich am eigenen Verhalten dadurch nichts.

Macht das bestehende Records ungültig? Nein, by design, mit einer Ausnahme in beide Richtungen

DMARC trägt seit RFC 7489 die Regel, dass unbekannte Tags ignoriert werden müssen. RFC 9989 übernimmt das wörtlich und baut die ganze Abwärtskompatibilität darauf auf. Ein alter, nur RFC-7489-konformer Validator, der t oder np nicht kennt, überliest sie einfach und wendet weiterhin p und sp an. Er stürzt dabei nicht ab und verhält sich nicht falsch, er kennt nur die feinere neue Steuerung noch nicht. Das hat aber eine Kehrseite, die man beim Einsatz von t=y kennen sollte: genau derselbe alte Validator ignoriert auch t=y und wendet bei p=reject weiterhin reject in voller Härte an. t=y schützt also nur gegenüber Empfängern, die RFC 9989 bereits verstehen, nicht gegenüber alten Implementierungen. Umgekehrt ignoriert ein RFC-9989-konformer Validator ein vorhandenes pct, weil das Tag schlicht nicht mehr definiert ist.

Genau an dieser Stelle steckt die Nuance, die wehtun kann: wer noch mit pct zwischen 1 und 99 einen echten Teil-Rollout fährt, das klassische schrittweise Hochfahren auf reject mit einem Bruchteil der Nachrichten, bekommt bei einem RFC-9989-konformen Empfänger keine reduzierte Durchsetzung mehr. Das Tag wird ignoriert, die volle in p konfigurierte Policy greift sofort. Einen direkten Nachfolger für genau diesen Zwischenbereich gibt es nicht, t kennt nur an und aus. Wer so einen Teil-Rollout fährt, muss stattdessen auf eine echte Stufenmigration ausweichen, etwa p=none, dann p=quarantine, dann p=reject, mit t=y jeweils als zusätzliche Bremse innerhalb einer Stufe. Nur der Randfall pct=0 hat mit t=y einen echten, direkten Ersatz. Für alle anderen gilt: bestehende v=DMARC1-Records bleiben syntaktisch gültig, niemand muss etwas ändern, um funktionsfähig zu bleiben. Weitgehend kompatibel ist die richtige Beschreibung, additiv dagegen nicht ganz, weil eben doch drei Tags aus der aktiven Auswertung verschwinden und die Organizational-Domain-Bestimmung sich strukturell ändert.

Mailinglisten und die p=reject-Warnung

Abschnitt 7.4 rät Domains, deren Nutzer möglicherweise auf öffentlichen Mailinglisten schreiben, ausdrücklich von p=reject ab. Der Grund ist alt und in RFC 7960 im Detail beschrieben: Listen-Reflektoren brechen die SPF-Alignment, weil die Mail über einen fremden Server läuft, und selbst DKIM-Signaturen überleben nicht jede Listen-Software unverändert. Der konkrete, im RFC-Text selbst genannte Weg ist eine gestufte Migration: zuerst mindestens einen Monat p=none, dann mindestens ebenso lange p=quarantine, jeweils mit Auswertung der Reports, bevor überhaupt über p=reject nachgedacht wird. t=y wird an dieser Stelle im RFC nicht namentlich erwähnt, würde sich als zusätzliche Bremse innerhalb einer Stufe aber inhaltlich genauso eignen wie der frühere pct-Ansatz, das ist an dieser Stelle meine eigene Einordnung, keine RFC-Vorgabe. Wichtig zusätzlich, weil es die Wucht von p=reject etwas relativiert: RFC 9989 verpflichtet Empfänger, nicht allein aufgrund eines p=reject abzulehnen, und schreibt vor, einen DMARC-Fail ohne weitere eigene Erkenntnisse wie quarantine zu behandeln.

RFC 9990: DKIM-Ergebnisse und der Selector werden verbindlicher

In den regelmäßigen, in der Praxis meist täglichen aggregierten XML-Reports war das DKIM-Auth-Ergebnis unter RFC 7489 eher beiläufig spezifiziert. RFC 9990 macht daraus eine klare Pflicht, allerdings nur bedingt: wurde für eine Nachricht überhaupt eine DKIM-Signatur validiert, muss das Ergebnis im Report stehen, eine komplett unsignierte Nachricht braucht weiterhin kein eigenes DKIM-Element. Neu und für die Praxis besonders nützlich ist zusätzlich, dass der verwendete Selector jetzt ebenfalls verpflichtend im Report steht, unter RFC 7489 war er optional. Damit lässt sich aus den Reports direkt ablesen, welcher Selector bei welchem berichtenden Mail-Receiver (nicht) verifiziert, was Diagnosen bei DKIM-Key-Rotationen deutlich erleichtert.

RFC 9991: Failure Reports und die Datenschutzfrage

Das ARF-Format für die einzelnen Failure Reports ist nicht neu, das nutzte schon RFC 7489. Neu ist, dass RFC 9991 dieses Thema aus der Kernspezifikation herauslöst, in eine eigene RFC packt und dabei ein altbekanntes Problem konkreter adressiert: Failure Reports können personenbezogene Daten enthalten, Empfängeradressen und im schlechtesten Fall Teile des Mail-Inhalts. Genau deshalb liefern nach eigener Beobachtung viele große Empfänger schon seit Jahren kaum bis gar keine ruf-Reports aus, RFC 9991 selbst schreibt, dass die Datenschutzbedenken viele Betreiber dazu gebracht haben, den Einsatz von Failure Reports einzuschränken. RFC 9991 bringt jetzt konkrete Redaction-, Datenminimierungs- und Transport-Security-Empfehlungen mit, formuliert als starke Empfehlung, nicht als Pflicht oder Garantie. Ob das in der Praxis tatsächlich mehr Empfänger dazu bringt, ruf überhaupt zu bedienen, bleibt abzuwarten, ein Schritt in die richtige Richtung ist es trotzdem.

Praxis-Check an den eigenen sechs Domains

Live abgefragt mit dig +short TXT _dmarc.<domain>, Stand heute:

kernel-error.de:        v=DMARC1; p=reject; rua=mailto:postmaster@kernel-error.de; ruf=mailto:postmaster@kernel-error.de; fo=1; sp=reject; aspf=s;
kernel-error.com:       (identisch)
kernel-error.org:       (identisch)
vandemeer.de:           (identisch)
fuchs-meckenheim.de:    (identisch)
heidbreders.de:         (identisch)

Drei Beobachtungen dazu. Erstens: pct=100 stand ursprünglich noch auf allen sechs Records, ein Überbleibsel aus der Zeit vor dem eigentlich gewünschten vollen Rollout, dazu weder rf noch ri. Weil überall ohnehin die volle Durchsetzung gewollt war und kein Teil-Rollout lief, war die neue Bedeutungslosigkeit von pct genau der Anlass, das Tag von allen sechs Records ersatzlos zu streichen, kein Muss, aber ein guter Moment dafür.

Zweitens: heidbreders.de stand bislang als einzige der sechs Domains auf sp=none, die anderen fünf auf sp=reject. Beim Aufräumen gleich vereinheitlicht, jetzt tragen alle sechs sp=reject. np explizit zu setzen würde am eigenen Verhalten trotzdem nichts ändern, weil der Default, der Rückfall auf sp, für alle sechs Domains ohnehin schon reject ist. Es besteht kein Handlungsbedarf, ein expliziterer Record wäre höchstens Geschmackssache.

Drittens, und das ist der Punkt, der zur eingangs erwähnten Adoptionsverzögerung passt: rspamd, hier in Version 4.1.0 der eigene DMARC-Validator für eingehende Mail, unterstützt np nach aktuellem Stand von September 2026 noch nicht. Bekommt der eigene Filter eine gespoofte Mail von einer nicht existierenden Subdomain einer fremden Marke, deren Domain-Owner über np strenger sein wollte als über sp, wendet rspamd aktuell trotzdem die laxere sp-Policy an. Eine kleine, aber reale Lücke auf der Empfänger-Seite, bis rspamd nachzieht. Vier Monate nach Publikation der RFC haben ohnehin die wenigsten Absender-Domains np überhaupt gesetzt, akuten Grund zur Sorge gibt das nicht, einen Blick in die rspamd-Release-Notes aber schon.

Die Frage aus Abschnitt 7.4 ist dabei nicht, ob ich selbst eine Mailingliste betreibe, sondern ob Absender meiner Domains an fremden Mailinglisten oder ähnlichen indirekten Mail-Flows teilnehmen. Bei mir ist das praktisch nicht der Fall, für die eigene Infrastruktur bleibt die gestufte Migration also Theorie. Für Leser mit Nutzern, die auf Mailinglisten schreiben, ist sie es sehr wohl.

Kurzfassung: Kein akuter Handlungsbedarf für Domains mit bestehendem p=reject und pct=100. Wer noch einen Teil-Rollout mit einem pct-Wert zwischen 1 und 99 fährt, findet in t keinen direkten Ersatz und muss auf eine echte Stufenmigration über p ausweichen, nur der Randfall pct=0 lässt sich eins zu eins durch t=y ersetzen. np ist ein sinnvolles Sicherheits-Add-on, dessen Wirkung von der Update-Geschwindigkeit der empfängerseitigen Implementierungen abhängt, ein gutes Beispiel dafür, dass ein neues RFC nicht am Tag der Veröffentlichung live ist, sondern über Monate und Jahre durch Software-Updates bei allen Beteiligten durchsickert.

Siehe auch

Eigene Erfahrungen mit der Umstellung auf RFC 9989, oder Fragen zu DMARC allgemein? Dann darfst du mich sehr gerne fragen.

internet.nl: Mailserver-Sicherheit testen mit dem niederländischen Standard

Die niederländische Regierung betreibt mit internet.nl ein kostenloses Testtool für Webserver und Mailserver. Im Gegensatz zu Qualys SSL Labs, das sich auf TLS-Konfiguration konzentriert, prüft internet.nl das gesamte E-Mail-Sicherheitsbild einer Domain.

Was getestet wird

Der Mailserver-Test prüft:

STARTTLSOb der MX TLS anbietet und welche Protokollversionen und Cipher unterstützt werden
ZertifikatGültigkeit, Kette, Hostname-Match
DANE/TLSAOb TLSA-Records im DNS vorhanden und korrekt sind
SPFOb ein SPF-Record existiert und syntaktisch korrekt ist
DKIMOb ausgehende Mails DKIM-signiert sind
DMARCOb eine DMARC-Policy veröffentlicht ist und welche Einstellung sie hat
MTA-STSOb MTA-STS konfiguriert ist und die Policy konsistent ist
DNSSECOb die Domain mit DNSSEC gesichert ist

Für jede Kategorie gibt es Punkte. 100 Prozent erreicht man nur wenn alles korrekt konfiguriert ist. Domains die sowohl beim Web- als auch beim Mailtest 100 Prozent erreichen, landen in der Hall of Fame.

Strenge Anforderungen

internet.nl ist strenger als die meisten anderen Testtools. TLS 1.0 und 1.1 geben Abzug. Ohne DANE ist kein voller Score möglich. Die Cipher-Anforderungen orientieren sich an den niederländischen IT-Sicherheitsrichtlinien, die auch für Behörden gelten.

Das macht den Test besonders nützlich als Benchmark. Wer dort 100 Prozent hat, hat sein E-Mail-Setup nach aktuellem Stand abgesichert. Wer Abzüge bekommt, sieht genau wo es hakt.

Fremde Domains testen

Man kann beliebige Domains testen, nicht nur die eigene. Das ist praktisch um Dienstleister, Geschäftspartner oder den eigenen Provider zu prüfen. Ein kurzer Test zeigt schnell ob der Mailserver auf der anderen Seite zeitgemäß konfiguriert ist oder ob dort noch SSLv3 mit RC4 läuft.

Siehe auch: MTA-STS einrichten

Fragen? Einfach melden.

Testtool Liste für schnelle und einfache Tests

Ich bin schon ein paar mal darum gebeten worden, doch bitte eine gesammelte Liste der verschiedenen Testtools zu erstellen.

Hiermit möchte ich dieser Bitte nachkommen.

Bitte beachtet aber, dass diese Tests/Checks in den meisten Fällen grob sind und nur verlässlich mitteilen können, ob etwas klappt oder nicht. Dennoch ist es hier und da eine schnelle und einfache Hilfe für Experimente und/oder um zu prüfen ob eine Änderungen greift!

So long….


Tests für Mail Server

SSL/TLS/TLSA/DANE/DNSSEC/SMTP Tests:
https://de.ssl-tools.net/mailservers/kernel-error.de

SSL/TLS/SMTP/POP/IMAP Tests:
http://www.checktls.com/

DANE SMTP Validator:
https://dane.sys4.de/smtp/kernel-error.de

DMARC TEST:
https://dmarcian.com/dmarc-inspector/kernel-error.de

SPF TEST:
https://dmarcian.com/spf-survey/kernel-error.de

http://www.kitterman.com/spf/validate.html

Mailserver Blacklist check:
http://mxtoolbox.com/blacklists.aspx

https://talosintelligence.com/

http://www.barracudacentral.org/lookups

DKIM / SPF / DMARC E-Mail Testadresse:
check-auth@verifier.port25.com

check-auth2@verifier.port25.com

Microsoft Tests:

https://testconnectivity.microsoft.com


Tests für Web Server

SSL/TLS Test:
https://www.ssllabs.com/ssltest/analyze.html?d=kernel-error.de

https://de.ssl-tools.net/webservers/www.kernel-error.de

SPDY Test:
https://spdycheck.org/#www.kernel-error.de


Tests für DNS Server

DNSserver:
http://www.dnsinspect.com/kernel-error.de

DNSSEC:
http://dnssec-debugger.verisignlabs.com/www.kernel-error.de

http://dnsviz.net/d/kernel-error.de/dnssec/


Tests für Jabber/XMPP Server

Jabber/XMMPP-Security Test:
http://www.xmpp.net/


Tests für Clients

IPv6 Client-Test:
http://test-ipv6.com

Webbrowser Test:
https://www.ssllabs.com/ssltest/viewMyClient.html

OPENGPGKEY RR:
https://openpgpkey.info/?email=kernel-error%40kernel-error.com

Siehe auch: Hardenize Security Scanner

Fragen? Einfach melden.

mailgraph Graphen um DANE erweitern

Wie sich die grafische Logfile Auswertung für, unter anderem, Postfix um Dinge wie SPF, DKIM und DMARC erweitern lässt, habe ich ja bereits vor kurzem hier gezeigt: mailgraph Graphen um SPF, DMARC und DKIM erweitern

Seit ein paar Monaten ist an meinem Mailserver DANE aktiviert, sprich es lassen sich die TLSA RECORDS für die Zertifikate am Postfix gegen einen DNSSEC gesicherten DNS Server abgleichen.

In diesem Zuge habe ich selbstverständlich die ausgehende Prüfung am Postfix aktiviert. Mein Postfix nutzt nun also auch DANE um die Verbindung zu anderen Mailserver zu prüfen. Dabei gibt es im Grunde nur vier mögliche Ergebnisse einer Prüfung der TLS Verbindung:

Verified

Der bisher seltenste aber beste Fall. Denn die ausgehende Verbindung zum anderen Server ist per DANE gesichert. Das Zertifikat ist also nicht nur gültig sondern konnte auch mit den TLSA-RECORDS abgeglichen werden.

Trusted

Der OK Fall… Das Zertifikat ist gültig.

Anonymous

Na ja… Es gibt ein Zertifikat und dieses ist passend zum Hostname, es konnte aber keine Vertrauenskette gebildet werden.

Untrusted

Schlecht! Es gibt zwar ein Zertifikat und somit auch eine verschlüsselte Verbindung, denn noch stimmt etwas mit dem Zertifikat nicht. Es ist abgelaufen, passt nicht zum Hostname oder ähnliches.

Im Logfile findet es sich wie folgt:

Aug  3 18:34:08 servername postfix/smtp[1234]: Verified TLS connection established to mx01.domain.tld[1.2.3.4]:25: TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)

Aug  9 12:07:28 servername postfix/smtp[1234]: Trusted TLS connection established to mx01.domain.tld[1.2.3.4]:25: TLSv1.2 with cipher DHE-RSA-AES256-GCM-SHA384 (256/256 bits)

Aug  8 22:15:34 servername postfix/smtp[1234]: Anonymous TLS connection established to mx01.domain.tld[1.2.3.4]:25: TLSv1 with cipher ADH-AES256-SHA (256/256 bits)

Aug  7 21:48:48 servername postfix/smtp[1234]: Untrusted TLS connection established to mx01.domain.tld[1.2.3.4]:25: TLSv1.2 with cipher DHE-RSA-AES256-SHA256 (256/256 bits)

Was mich nun interessiert, ist die Tendenz der einzelnen Prüfungen. Wie sicher ist die Kommunikation zu anderen Mailservern wirklich, rein bezogen auf die Vertrauenswürdigkeit der eingesetzten Zertifikate. Eine schnelle Übersicht soll mir hier wieder der mailgraph liefern.

Den Mailgraph habe ich also, wie mit SPF, DKIM und DMARC ebenfalls, erweitert. Das nötige Patchset gibt es natürlich wieder unten. Dieses Mal habe ich darauf geachtet für die TLS Auswertung ein eigenes File für die RRD-Daten anzulegen. So kann ein bestehender Mailgraph einfach erweitert werden, ohne die vorhandenen Daten zu verlieren.

Heraus kommt am Ende dieser Graph.

Für interessiere finden sich die beiden nötigen Patchfiles hier:

/usr/sbin/mailgrapht

/usr/lib/cgi-bin/mailgraph.cgi

Viel Spaß! Bei Fragen, fragen.

Fragen? Einfach melden.

Google, IPv6 und geblockte E-Mails…

Google lehnt hin und wieder E-Mails ab, die per IPv6 eingeliefert werden. Der Mailserver ist korrekt konfiguriert, SPF, DKIM, DMARC und DANE sind vorhanden, kein Eintrag auf irgendeiner Blacklist. Per IPv4 geht die gleiche Mail problemlos durch.

Die Fehlermeldung

550-5.7.1 Our system has detected that this message is likely
550-5.7.1 unsolicited mail. To reduce the amount of spam sent
550-5.7.1 to Gmail, this message has been blocked.

Der verlinkte Support-Artikel hilft nicht weiter. Alle Vorgaben sind erfüllt. Googles Antwort auf Nachfrage: „Warum genau eine E-Mail abgewiesen wird, können wir Ihnen leider nicht mitteilen.“ Das betrifft gmail.com, googlemail.com und jede bei Google gehostete Domain gleichermaßen.

Workaround: IPv4-only für Google

Da Google per IPv4 keine Probleme macht, lässt sich Postfix so konfigurieren, dass E-Mails an Google-Domains nur über IPv4 zugestellt werden. Das ist unnötig, ärgerlich und widerspricht dem Gedanken hinter IPv6. Aber es funktioniert.

In /etc/postfix/master.cf einen eigenen SMTP-Service anlegen, der nur IPv4 spricht:

smtp4     unix  -       -       -       -       -       smtp -o inet_protocols=ipv4

In /etc/postfix/main.cf die Transport Map aktivieren:

transport_maps = hash:/etc/postfix/transport

Die betroffenen Domains in /etc/postfix/transport eintragen:

googlemail.com smtp4:
gmail.com smtp4:

Anwenden und Postfix neu laden:

postmap /etc/postfix/transport
service postfix reload

E-Mails an Google-Domains gehen ab sofort nur noch über IPv4 raus. Wer weitere Domains pro Ziel steuern will, findet in Postfix TLS Policy Maps einen ähnlichen Ansatz.

Fragen? Einfach melden.

AOL macht jetzt auch Mailinglisten „kaputt“ / DMARC

Das gefällt mir laughing… Nachdem nun Yahoo die Eier hatte einfach mal ihre DMARC Policy „scharf“ (reject) zu schalten und sich so viel Schimpfe von aller Welt dafür eingesammelt hat und beschuldigt wurde die Mailinglisten „kaputt“ zu machen…. Tja, da zieht AOL nun in gewisser Weise nach: https://web.archive.org/web/20151222062444/http://postmaster-blog.aol.com/2014/04/22/aol-mail-updates-dmarc-policy-to-reject/ AOL hat nämlich gerade ein tierisches Spam-Problem. Also jetzt nicht direkt, eher haben andere das Problem, es betrifft AOL nur in soweit das deren Absenderdomain dafür missbraucht wird. Die Spam E-Mails sehen also so aus als wenn sie von AOL kommen. AOL hat nun also ihre DMARC Policy scharf geschaltet um das Problem damit etwas eindämmen zu können. Ob nun wohl so langsam Google und Microsoft ebenfalls nachziehen? Inzwischen sollten die Admins der Mailinglisten ja schon mal „warm“ gelaufen sein tongue-out

Fragen? Einfach melden.

Mailgraph um SPF-, DMARC- und DKIM-Graphen erweitern

Mailgraph von David Schweikert ist ein simples Perl-Script, das Postfix-Logdateien parst und daraus RRDtool-Graphen erzeugt. Empfangene, gesendete, abgelehnte Mails auf einen Blick. Für kleinere Mailserver oder Testsysteme reicht das völlig.

Was mailgraph nicht kann: SPF-, DMARC– und DKIM-Ergebnisse darstellen. Wie viele Mails bestehen den SPF-Check? Wie oft schlägt DMARC fehl? Gibt es einen Trend? Ich habe zwar einen existierenden SPF-Patch für mailgraph gefunden, wollte aber alle drei Protokolle in einem Aufwasch.

Die folgenden Patches erweitern mailgraph 1.14 um drei zusätzliche Graphen: SPF (pass/none/fail), DMARC (pass/none/fail) und DKIM (pass/none/fail). Ausgelegt für Postfix mit postfix-policyd-spf, opendkim und opendmarc.

Installation

Auf einem Debian-System müssen zwei Dateien ersetzt werden: /usr/sbin/mailgraph (der Daemon) und /usr/lib/cgi-bin/mailgraph.cgi (die Weboberfläche). Vorher sichern.

Die Patches als Download: mailgraph-dmarc-spf-dkim.patch.tar.gz

Patch 1: /usr/sbin/mailgraph

Der Daemon-Patch fügt neun RRD-Datasources hinzu (spfnone, spffail, spfpass, dmarcnone, dmarcfail, dmarcpass, dkimnone, dkimfail, dkimpass) und parst die Logzeilen von policy-spf, opendmarc und opendkim:

*** mailgraph	2012-06-17 00:00:00.000000000 +0200
--- mailgraph	2014-04-24 08:59:58.964977886 +0200
***************
*** 4,9 ****
--- 4,10 ----
  # copyright (c) 2000-2007 ETH Zurich
  # copyright (c) 2000-2007 David Schweikert <david@schweikert.ch>
  # released under the GNU General Public License
+ # with dkim-, dmarc, spf-patch Sebastian van de Meer <kernel-error@kernel-error.de>

  ######## Parse::Syslog 1.09 (automatically embedded) ########
  package Parse::Syslog;
***************
*** 382,388 ****
  my $rrd_greylist = "mailgraph_greylist.rrd";
  my $year;
  my $this_minute;
! my %sum = ( sent => 0, received => 0, bounced => 0, rejected => 0, virus => 0, spam => 0, greylisted => 0, delayed => 0);
  my $rrd_inited=0;

  my %opt = ();
--- 383,389 ----
  my $rrd_greylist = "mailgraph_greylist.rrd";
  my $year;
  my $this_minute;
! my %sum = ( sent => 0, received => 0, bounced => 0, rejected => 0, spfnone => 0, spffail => 0, spfpass => 0, dmarcnone => 0, dmarcfail => 0, dmarcpass => 0, dkimnone => 0, dkimfail => 0, dkimpass => 0, virus => 0, spam => 0, greylisted => 0, delayed => 0);
  my $rrd_inited=0;

  my %opt = ();
***************
*** 396,401 ****
--- 397,411 ----
  sub event_rejected($);
  sub event_virus($);
  sub event_spam($);
+ sub event_spfnone($);
+ sub event_spffail($);
+ sub event_spfpass($);
+ sub event_dmarcnone($);
+ sub event_dmarcfail($);
+ sub event_dmarcpass($);
+ sub event_dkimnone($);
+ sub event_dkimfail($);
+ sub event_dkimpass($);
  sub event_greylisted($);
  sub event_delayed($);
  sub init_rrd($);
***************
*** 533,538 ****
--- 543,557 ----
  				'DS:recv:ABSOLUTE:'.($rrdstep*2).':0:U',
  				'DS:bounced:ABSOLUTE:'.($rrdstep*2).':0:U',
  				'DS:rejected:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:spfnone:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:spffail:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:spfpass:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dmarcnone:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dmarcfail:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dmarcpass:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dkimnone:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dkimfail:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dkimpass:ABSOLUTE:'.($rrdstep*2).':0:U',
  				"RRA:AVERAGE:0.5:$day_steps:$realrows",   # day
  				"RRA:AVERAGE:0.5:$week_steps:$realrows",  # week
  				"RRA:AVERAGE:0.5:$month_steps:$realrows", # month
***************
*** 614,619 ****
--- 633,649 ----
  				event($time, 'bounced');
  			}
  		}
+ 	  elsif ($prog eq 'policy-spf') {
+ 	    if ($text =~ /Received-SPF: none/) {
+ 	      event($time, 'spfnone');
+ 	    }
+ 	    elsif($text =~ /Received-SPF: pass/) {
+ 				event($time, 'spfpass');
+ 			}
+ 	    elsif($text =~ /Received-SPF:/) {
+ 				event($time, 'spffail');
+ 			}
+ 	  }
  		elsif($prog eq 'local') {
  			if($text =~ /\bstatus=bounced\b/) {
  				event($time, 'bounced');
***************
*** 862,867 ****
--- 892,919 ----
  			event($time, 'virus');
  		}
  	}
+ 	elsif ($prog eq 'opendmarc') {
+ 		if ($text =~ /pass/) {
+ 			event($time, 'dmarcpass');
+ 		}
+ 		elsif($text =~ /none/) {
+ 			event($time, 'dmarcnone');
+ 		}
+ 		elsif($text =~ /fail/) {
+ 			event($time, 'dmarcfail');
+ 		}
+ 	}
+ 	elsif ($prog eq 'opendkim') {
+ 		if ($text =~ /DKIM verification successful/) {
+ 			event($time, 'dkimpass');
+ 		}
+ 		elsif($text =~ /no signature data/) {
+ 			event($time, 'dkimnone');
+ 		}
+ 		elsif($text =~ /bad signature data/) {
+ 			event($time, 'dkimfail');
+ 		}
+ 	}
  	elsif($prog eq 'avmilter') {
  		# AntiVir Milter
  		if($text =~ /^Alert!/) {
***************
*** 918,931 ****
  	return 1 if $m == $this_minute;
  	return 0 if $m < $this_minute;

! 	print "update $this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}:$sum{virus}:$sum{spam}:$sum{greylisted}:$sum{delayed}\n" if $opt{verbose};
! 	RRDs::update $rrd, "$this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}" unless $opt{'no-mail-rrd'};
  	RRDs::update $rrd_virus, "$this_minute:$sum{virus}:$sum{spam}" unless $opt{'no-virus-rrd'};
  	RRDs::update $rrd_greylist, "$this_minute:$sum{greylisted}:$sum{delayed}" unless $opt{'no-greylist-rrd'};
  	if($m > $this_minute+$rrdstep) {
  		for(my $sm=$this_minute+$rrdstep;$sm<$m;$sm+=$rrdstep) {
! 			print "update $sm:0:0:0:0:0:0:0:0 (SKIP)\n" if $opt{verbose};
! 			RRDs::update $rrd, "$sm:0:0:0:0" unless $opt{'no-mail-rrd'};
  			RRDs::update $rrd_virus, "$sm:0:0" unless $opt{'no-virus-rrd'};
  			RRDs::update $rrd_greylist, "$sm:0:0" unless $opt{'no-greylist-rrd'};
  		}
--- 970,983 ----
  	return 1 if $m == $this_minute;
  	return 0 if $m < $this_minute;

! 	print "update $this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}:$sum{spfnone}:$sum{spffail}:$sum{spfpass}:$sum{dmarcnone}:$sum{dmarcfail}:$sum{dmarcpass}:$sum{dkimnone}:$sum{dkimfail}:$sum{dkimpass}:$sum{virus}:$sum{spam}:$sum{greylisted}:$sum{delayed}\n" if $opt{verbose};
! 	RRDs::update $rrd, "$this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}:$sum{spfnone}:$sum{spffail}:$sum{spfpass}:$sum{dmarcnone}:$sum{dmarcfail}:$sum{dmarcpass}:$sum{dkimnone}:$sum{dkimfail}:$sum{dkimpass}" unless $opt{'no-mail-rrd'};
  	RRDs::update $rrd_virus, "$this_minute:$sum{virus}:$sum{spam}" unless $opt{'no-virus-rrd'};
  	RRDs::update $rrd_greylist, "$this_minute:$sum{greylisted}:$sum{delayed}" unless $opt{'no-greylist-rrd'};
  	if($m > $this_minute+$rrdstep) {
  		for(my $sm=$this_minute+$rrdstep;$sm<$m;$sm+=$rrdstep) {
! 			print "update $sm:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0 (SKIP)\n" if $opt{verbose};
! 			RRDs::update $rrd, "$sm:0:0:0:0:0:0:0:0:0:0:0:0:0" unless $opt{'no-mail-rrd'};
  			RRDs::update $rrd_virus, "$sm:0:0" unless $opt{'no-virus-rrd'};
  			RRDs::update $rrd_greylist, "$sm:0:0" unless $opt{'no-greylist-rrd'};
  		}
***************
*** 935,940 ****
--- 987,1001 ----
  	$sum{received}=0;
  	$sum{bounced}=0;
  	$sum{rejected}=0;
+ 	$sum{spfnone}=0;
+ 	$sum{spffail}=0;
+ 	$sum{spfpass}=0;
+ 	$sum{dmarcnone}=0;
+ 	$sum{dmarcfail}=0;
+ 	$sum{dmarcpass}=0;
+ 	$sum{dkimnone}=0;
+ 	$sum{dkimfail}=0;
+ 	$sum{dkimpass}=0;
  	$sum{virus}=0;
  	$sum{spam}=0;
  	$sum{greylisted}=0;

Patch 2: /usr/lib/cgi-bin/mailgraph.cgi

Der CGI-Patch erzeugt die drei neuen Graphen (SPF, DMARC, DKIM) und bindet sie in die HTML-Ausgabe ein. Jeder Graph zeigt pass als Fläche, none als Stack und fail als Linie:

*** mailgraph.cgi	2012-06-17 00:00:00.000000000 +0200
--- mailgraph.cgi	2014-04-24 09:03:11.917988368 +0200
***************
*** 4,9 ****
--- 4,10 ----
  # copyright (c) 2000-2007 ETH Zurich
  # copyright (c) 2000-2007 David Schweikert <david@schweikert.ch>
  # released under the GNU General Public License
+ # with dkim-, dmarc, spf-patch Sebastian van de Meer <kernel-error@kernel-error.de>

  use RRDs;
  use POSIX qw(uname);
***************
*** 15,21 ****
--- 16,25 ----
  my $xpoints = 540;
  my $points_per_sample = 3;
  my $ypoints = 160;
+ my $ypoints_spf = 96;
  my $ypoints_err = 96;
+ my $ypoints_dmarc = 96;
+ my $ypoints_dkim = 96;
  my $ypoints_grey = 96;
  my $rrd = '/var/lib/mailgraph/mailgraph.rrd';
  my $rrd_virus = '/var/lib/mailgraph/mailgraph_virus.rrd';
***************
*** 32,37 ****
--- 36,50 ----
  	sent       => '000099',
  	received   => '009900',
  	rejected   => 'AA0000',
+ 	spfnone    => '000AAA',
+ 	spffail    => '12FF0A',
+ 	spfpass    => 'D15400',
+ 	dmarcnone  => 'FFFF00',
+ 	dmarcfail  => 'FF00EA',
+ 	dmarcpass  => '00FFD5',
+ 	dkimnone   => '3013EC',
+ 	dkimfail   => '006B3A',
+ 	dkimpass   => '491503',
  	bounced    => '000000',
  	virus      => 'DDBB00',
  	spam       => '999999',
***************
*** 154,159 ****
--- 167,292 ----
  	);
  }

+ sub graph_spf($)
+ {
+ 	my ($range, $file) = @_;
+ 	my $step = $range*$points_per_sample/$xpoints;
+ 	rrd_graph($range, $file, $ypoints_spf,
+ 		"DEF:spfpass=$rrd:spfpass:AVERAGE",
+ 		"DEF:mspfpass=$rrd:spfpass:MAX",
+ 		"CDEF:rspfpass=spfpass,60,*",
+ 		"CDEF:dspfpass=spfpass,UN,0,spfpass,IF,$step,*",
+ 		"CDEF:sspfpass=PREV,UN,dspfpass,PREV,IF,dspfpass,+",
+ 		"CDEF:rmspfpass=mspfpass,60,*",
+ 		"AREA:rspfpass#$color{spfpass}:SPF pass",
+ 		'GPRINT:sspfpass:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rspfpass:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmspfpass:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:spfnone=$rrd:spfnone:AVERAGE",
+ 		"DEF:mspfnone=$rrd:spfnone:MAX",
+ 		"CDEF:rspfnone=spfnone,60,*",
+ 		"CDEF:dspfnone=spfnone,UN,0,spfnone,IF,$step,*",
+ 		"CDEF:sspfnone=PREV,UN,dspfnone,PREV,IF,dspfnone,+",
+ 		"CDEF:rmspfnone=mspfnone,60,*",
+ 		"STACK:rspfnone#$color{spfnone}:SPF none",
+ 		'GPRINT:sspfnone:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rspfnone:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmsrspfnone:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:spffail=$rrd:spffail:AVERAGE",
+ 		"DEF:mspffail=$rrd:spffail:MAX",
+ 		"CDEF:rspffail=spffail,60,*",
+ 		"CDEF:dspffail=spffail,UN,0,spffail,IF,$step,*",
+ 		"CDEF:sspffail=PREV,UN,dspffail,PREV,IF,dspffail,+",
+ 		"CDEF:rmspffail=mspffail,60,*",
+ 		"LINE2:rspffail#$color{spffail}:SPF fail",
+ 		'GPRINT:sspffail:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rspffail:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmspffail:MAX:max\: %4.0lf msgs/min\l',
+ 	);
+ }
+
+ sub graph_dmarc($)
+ {
+ 	my ($range, $file) = @_;
+ 	my $step = $range*$points_per_sample/$xpoints;
+ 	rrd_graph($range, $file, $ypoints_dmarc,
+ 		"DEF:dmarcpass=$rrd:dmarcpass:AVERAGE",
+ 		"DEF:mdmarcpass=$rrd:dmarcpass:MAX",
+ 		"CDEF:rdmarcpass=dmarcpass,60,*",
+ 		"CDEF:ddmarcpass=dmarcpass,UN,0,dmarcpass,IF,$step,*",
+ 		"CDEF:sdmarcpass=PREV,UN,ddmarcpass,PREV,IF,ddmarcpass,+",
+ 		"CDEF:rmdmarcpass=mdmarcpass,60,*",
+ 		"AREA:rdmarcpass#$color{dmarcpass}:DMARC pass",
+ 		'GPRINT:sdmarcpass:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdmarcpass:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdmarcpass:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:dmarcnone=$rrd:dmarcnone:AVERAGE",
+ 		"DEF:mdmarcnone=$rrd:dmarcnone:MAX",
+ 		"CDEF:rdmarcnone=dmarcnone,60,*",
+ 		"CDEF:ddmarcnone=dmarcnone,UN,0,dmarcnone,IF,$step,*",
+ 		"CDEF:sdmarcnone=PREV,UN,ddmarcnone,PREV,IF,ddmarcnone,+",
+ 		"CDEF:rmdmarcnone=mdmarcnone,60,*",
+ 		"STACK:rdmarcnone#$color{dmarcnone}:DMARC none",
+ 		'GPRINT:sdmarcnone:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdmarcnone:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdmarcnone:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:dmarcfail=$rrd:dmarcfail:AVERAGE",
+ 		"DEF:mdmarcfail=$rrd:dmarcfail:MAX",
+ 		"CDEF:rdmarcfail=dmarcfail,60,*",
+ 		"CDEF:ddmarcfail=dmarcfail,UN,0,dmarcfail,IF,$step,*",
+ 		"CDEF:sdmarcfail=PREV,UN,ddmarcfail,PREV,IF,ddmarcfail,+",
+ 		"CDEF:rmdmarcfail=mdmarcfail,60,*",
+ 		"LINE2:rdmarcfail#$color{dmarcfail}:DMARC fail",
+ 		'GPRINT:sdmarcfail:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdmarcfail:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdmarcfail:MAX:max\: %4.0lf msgs/min\l',
+ 	);
+ }
+
+ sub graph_dkim($)
+ {
+ 	my ($range, $file) = @_;
+ 	my $step = $range*$points_per_sample/$xpoints;
+ 	rrd_graph($range, $file, $ypoints_dkim,
+ 		"DEF:dkimpass=$rrd:dkimpass:AVERAGE",
+ 		"DEF:mdkimpass=$rrd:dkimpass:MAX",
+ 		"CDEF:rdkimpass=dkimpass,60,*",
+ 		"CDEF:ddkimpass=dkimpass,UN,0,dkimpass,IF,$step,*",
+ 		"CDEF:sdkimpass=PREV,UN,ddkimpass,PREV,IF,ddkimpass,+",
+ 		"CDEF:rmdkimpass=mdkimpass,60,*",
+ 		"AREA:rdkimpass#$color{dkimpass}:DKIM pass",
+ 		'GPRINT:sdkimpass:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdkimpass:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdkimpass:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:dkimnone=$rrd:dkimnone:AVERAGE",
+ 		"DEF:mdkimnone=$rrd:dkimnone:MAX",
+ 		"CDEF:rdkimnone=dkimnone,60,*",
+ 		"CDEF:ddkimnone=dkimnone,UN,0,dkimnone,IF,$step,*",
+ 		"CDEF:sdkimnone=PREV,UN,ddkimnone,PREV,IF,ddkimnone,+",
+ 		"CDEF:rmdkimnone=mdkimnone,60,*",
+ 		"STACK:rdkimnone#$color{dkimnone}:DKIM none",
+ 		'GPRINT:sdkimnone:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdkimnone:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdkimnone:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:dkimfail=$rrd:dkimfail:AVERAGE",
+ 		"DEF:mdkimfail=$rrd:dkimfail:MAX",
+ 		"CDEF:rdkimfail=dkimfail,60,*",
+ 		"CDEF:ddkimfail=dkimfail,UN,0,dkimfail,IF,$step,*",
+ 		"CDEF:sdkimfail=PREV,UN,ddkimfail,PREV,IF,ddkimfail,+",
+ 		"CDEF:rmdkimfail=mdkimfail,60,*",
+ 		"LINE2:rdkimfail#$color{dkimfail}:DKIM fail",
+ 		'GPRINT:sdkimfail:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdkimfail:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdkimfail:MAX:max\: %4.0lf msgs/min\l',
+ 	);
+ }
+
  [...]

+ 		print "<img src=\"$scriptname?${n}-s\" alt=\"mailgraph\"/></p>\n";
+ 		print "<img src=\"$scriptname?${n}-d\" alt=\"mailgraph\"/></p>\n";
+ 		print "<img src=\"$scriptname?${n}-k\" alt=\"mailgraph\"/></p>\n";

  [...]

+ 		elsif($img =~ /^(\d+)-s$/) {
+ 			my $file = "$tmp_dir/$uri/mailgraph_$1_spf.png";
+ 			graph_spf($graphs[$1]{seconds}, $file);
+ 			send_image($file);
+ 		}
+ 		elsif($img =~ /^(\d+)-d$/) {
+ 			my $file = "$tmp_dir/$uri/mailgraph_$1_dmarc.png";
+ 			graph_dmarc($graphs[$1]{seconds}, $file);
+ 			send_image($file);
+ 		}
+ 		elsif($img =~ /^(\d+)-k$/) {
+ 			my $file = "$tmp_dir/$uri/mailgraph_$1_dkim.png";
+ 			graph_dkim($graphs[$1]{seconds}, $file);
+ 			send_image($file);
+ 		}

Was die Patches tun

Der Daemon-Patch erweitert die RRD-Datenbank um neun Datasources und erkennt im Syslog die Ausgaben von drei Diensten:

DienstLog-PatternErgebnis
policy-spfReceived-SPF: pass/none/*spfpass, spfnone, spffail
opendmarcpass/none/faildmarcpass, dmarcnone, dmarcfail
opendkimDKIM verification successful / no signature data / bad signature datadkimpass, dkimnone, dkimfail

Der CGI-Patch erzeugt drei neue Graphen unterhalb der bestehenden. Jeder zeigt pass als Fläche, none als Stack darüber und fail als rote Linie. Die Zeiträume (Tag, Woche, Monat, Jahr) werden wie bei den Standard-Graphen automatisch erzeugt.

Hinweise

Die Patches sind für mailgraph 1.14 geschrieben. Weil mailgraph die RRD-Datasources beim ersten Start festlegt, muss nach dem Patchen die bestehende mailgraph.rrd gelöscht werden. Die alten Daten gehen dabei verloren, die neuen Graphen fangen bei Null an.

Es gibt auch einen separaten Patch für DANE-Graphen in mailgraph.

Wer heute mit dem Monitoring anfängt: rspamd bringt eine eigene Weboberfläche mit und prüft SPF, DKIM und DMARC in einem Durchgang. Zusammen mit Prometheus und Grafana bekommt man deutlich flexiblere Dashboards als mit RRDtool. Mailgraph bleibt trotzdem nützlich wenn man schnell was Schlankes braucht, das ohne externe Abhängigkeiten läuft.

Fragen oder Verbesserungsvorschläge? Einfach melden.

DMARC-Prüfung in Postfix: OpenDMARC und rspamd im Vergleich

Eine DMARC-Policy zu veröffentlichen ist die eine Seite. Die andere ist, eingehende Mails gegen die DMARC-Policy des Absenders zu prüfen. Postfix kann das nicht selbst. Dafür gibt es zwei Wege: OpenDMARC als eigenständigen Milter oder rspamd, der DMARC als eines von vielen Modulen mitbringt.

Variante 1: OpenDMARC

OpenDMARC ist ein dedizierter DMARC-Milter. Er liest die Ergebnisse von SPF und DKIM aus den Mailheadern und prüft ob die DMARC-Policy des Absenders erfüllt ist.

# Debian/Ubuntu
apt install opendmarc

# FreeBSD
pkg install opendmarc

Die Konfiguration in /etc/opendmarc.conf:

AuthservID mail.example.de
Socket inet:8893@localhost
UserID opendmarc
SoftwareHeader true
Syslog true
RejectFailures false
HistoryFile /var/run/opendmarc/opendmarc.dat

RejectFailures false ist ein guter Startwert. Damit werden Mails die den DMARC-Check nicht bestehen nicht sofort abgelehnt, sondern nur markiert. So kann man erst beobachten bevor man scharf schaltet. AuthservID muss zum Hostnamen des Mailservers passen.

Wichtig: OpenDMARC verlässt sich darauf, dass SPF und DKIM bereits geprüft wurden und die Ergebnisse in den Authentication-Results-Headern stehen. Die Reihenfolge der Milter in Postfix ist daher entscheidend: SPF und DKIM müssen vor OpenDMARC laufen.

Postfix-Integration

In der main.cf den Milter registrieren. OpenDMARC muss nach dem DKIM-Milter stehen:

# main.cf
smtpd_milters = inet:localhost:8891, inet:localhost:8893
non_smtpd_milters = inet:localhost:8891, inet:localhost:8893

Port 8891 ist hier OpenDKIM, Port 8893 OpenDMARC. Nach einem postfix reload prüft OpenDMARC jede eingehende Mail und schreibt einen Authentication-Results-Header mit dem DMARC-Ergebnis.

Variante 2: rspamd

Wer rspamd als Spamfilter einsetzt, braucht keinen separaten DMARC-Milter. rspamd prüft SPF, DKIM und DMARC in einem Durchgang und verrechnet das Ergebnis mit dem Gesamtscore. Das DMARC-Modul ist standardmäßig aktiv und unterstützt auch DMARC-Reporting (rua/ruf).

Der Vorteil von rspamd: Statt einer harten Ablehnung bei DMARC-Fail fließt das Ergebnis in die Gesamtbewertung ein. Eine Mail die DMARC nicht besteht aber sonst sauber aussieht, wird nicht sofort abgelehnt. Umgekehrt kann eine Mail die DMARC besteht trotzdem als Spam markiert werden wenn andere Indikatoren dagegen sprechen.

Was passt besser?

OpenDMARCrspamd
AufwandEigener Dienst, eigene ConfigBereits als Milter integriert
VerhaltenHarte Entscheidung (reject/accept)Score-basiert, flexibel
ReportingEigenes Reporting-Tool nötigrua-Reports eingebaut
AbhängigkeitenBraucht SPF/DKIM in den HeadernPrüft SPF/DKIM selbst

Für neue Setups ist rspamd meist die bessere Wahl, weil es SPF, DKIM, DMARC, ARC und Spam-Filterung in einem Paket liefert. OpenDMARC ist sinnvoll wenn man einen minimalen Stack ohne Content-Filter will oder bereits einen anderen Spamfilter einsetzt.

Übrigens: DMARC und Mailinglisten vertragen sich nicht ohne Weiteres. ARC löst das Problem. Fragen? Einfach melden.

Yahoo macht Mailinglisten mit DMARC kaputt?

Na was lese ich denn da wieder bei golem.de? Yahoo soll Mailinglisten kaputt machen weil DMARC eingesetzt wird?

Über DMARC habe ich ja schon etwas geschrieben, auch dass man mit Mailinglisten Probleme bekommen kann, wenn man eine Policy veröffentlicht. Denn die meisten Mailinglisten arbeiten leider nicht DKIM konform. Denn DKIM signiert den Body und ebenfalls den Betreff einer E-Mail. Wenn die Mailingliste nun den Betreff ändert um ein [Mailinglistenname] in den Betreff zu schreiben (damit die Anwender darüber besser „filtern“ können und wegen Übersicht bla bla). Sowie einen kleinen Footer mit Infos zur Liste in den Body hängen, ja dann ist die DKIM Signatur ungültig. Soll ja so sein! Mailinglisten lassen sich prima über den Mail-Header: List-id Filtern, die Infos zur Mailingliste im Footer liest eh keiner und es produziert nur unnötig viele Daten, da sie an jede E-Mail gehangen werden. Übersicht? Filtert man seine E-Mail sauber und sortiert sie in die passenden Ordner, dann ist die Übersicht gegeben.

Wie auch immer… DKIM Signaturen werden durch Mailinglisten schon seit vielen Jahren gebrochen. Einige Mailinglisten werfen sogar die DKIM Signaturen aus den E-Mails bevor sie „weitergeleitet“ werden. Damit kann es zwar keine ungültige/gebrochene Signatur mehr geben, in Kombination mit einer DMARC Policy gibt es denn noch ein Problem. Denn wenn in der Policy steht: E-Mails sind immer DKIM und SPF signiert und wenn sie es nicht sind oder wenn die Signatur ungültig ist, dann weise die E-Mail zurück. Dann passiert das so!

All dieses ist schon seit längerem fest in ein RFC gegossen. Eingesetzt wird DMARC bereits von vielen der großen Anbieter… Bisher war nur noch keiner cool genug es „scharf“ zu schalten. Ist völlig legitim, denn auch die Mailinglisten Admins sollen eine gewisse „Übergangszeit“ haben. Wie so oft hat nur kaum einer reagiert. Ist ja nur mal wieder Security oder Features welche die User nicht direkt betreffen. Ja, es ist eine gewisse Art ~Vergewaltigung~!

Der DMARC-RECORD von yahoo.com gibt eine klare Policy vor:

$ dig IN TXT _dmarc.yahoo.com +short
"v=DMARC1\; p=reject\; sp=none\; pct=100\; rua=mailto:dmarc-yahoo-rua@yahoo-inc.com, mailto:dmarc_y_rua@yahoo.com\;"

Als Beispiel: Der DMARC-RECORD von gmail.com dagegen noch nicht:

$ dig IN TXT _dmarc.gmail.com +short
"v=DMARC1\; p=none\; rua=mailto:mailauth-reports@google.com"

Siehe auch: DMARC einrichten

Fragen? Einfach melden.

DMARC und Mailinglisten: Warum Signaturen brechen und wie ARC das löst

Wer eine strenge DMARC-Policy veröffentlicht und gleichzeitig Mailinglisten nutzt, wird früher oder später feststellen: Die eigenen Mails werden auf der Liste zugestellt, aber beim Empfänger abgelehnt. Das liegt nicht an einem Konfigurationsfehler, sondern an der Art wie Mailinglisten funktionieren.

Das Problem

DKIM signiert nicht nur den Body einer E-Mail, sondern auch bestimmte Header wie den Betreff. Mailinglisten-Manager wie Mailman verändern die Mail aber auf dem Weg zum Empfänger. Sie hängen einen Identifier an den Betreff ([liste-name]), fügen einen Footer mit Abmelde-Link an den Body an und setzen eigene Header. Jede dieser Änderungen bricht die DKIM-Signatur.

SPF schlägt ebenfalls fehl, weil die Mail jetzt vom Listserver kommt und nicht mehr vom Mailserver des ursprünglichen Absenders. Die IP des Listservers steht nicht im SPF-Record der Absender-Domain.

DMARC prüft ob mindestens DKIM oder SPF bestehen und das Alignment stimmt. Beides schlägt fehl. Bei einer Policy von p=reject wird die Mail vom empfangenden Server abgelehnt. Der Absender bekommt einen Bounce, der Empfänger sieht die Mail nie.

Workarounds der Listenbetreiber

Mailinglisten-Manager haben verschiedene Strategien entwickelt um das Problem zu umgehen:

From-RewritingDer Absender wird auf die Listenadresse umgeschrieben. DMARC-Alignment passt dann zur Listendomain. Nachteil: Man sieht nicht mehr wer die Mail geschrieben hat.
Subject-Tag weglassenKein [liste] im Betreff. Schont die DKIM-Signatur, aber der Betreff allein reicht nicht, die Signatur kann trotzdem brechen wenn der Body verändert wird.
Footer weglassenKein Abmelde-Link im Body. Schont DKIM, aber Listenbetreiber brauchen den Footer oft aus rechtlichen Gründen.
DKIM re-signingDie Liste signiert die Mail neu mit ihrer eigenen Domain. Funktioniert, aber die originale Signatur des Absenders geht verloren.

Keiner dieser Workarounds ist sauber. Entweder geht Information verloren oder die Authentizität leidet.

ARC: Die eigentliche Lösung

Authenticated Received Chain (RFC 8617) wurde genau für dieses Problem entwickelt. Die Idee: Jeder Zwischenserver (wie ein Listserver) speichert die Authentifizierungsergebnisse der eingehenden Mail in einem ARC-Header und signiert diesen. Der empfangende Server kann dann die Kette zurückverfolgen und sehen, dass die Mail beim Eintritt in die Liste noch gültig war.

Drei Header bilden die Kette:

ARC-Authentication-Results: i=1; lists.example.org;
  dkim=pass header.d=kernel-error.de;
  spf=pass smtp.mailfrom=kernel-error@kernel-error.com;
  dmarc=pass header.from=kernel-error.de

ARC-Message-Signature: i=1; a=rsa-sha256; d=lists.example.org; ...
ARC-Seal: i=1; a=rsa-sha256; d=lists.example.org; cv=none; ...

ARC-Authentication-Results enthält die Ergebnisse zum Zeitpunkt der Annahme. ARC-Message-Signature signiert die Nachricht in ihrem aktuellen Zustand. ARC-Seal signiert die gesamte ARC-Kette und verhindert Manipulation.

Der empfangende Mailserver prüft die ARC-Kette. Wenn er dem Listserver vertraut und die Kette gültig ist, kann er die Mail trotz gebrochener DKIM-Signatur zustellen. Gmail, Microsoft und Yahoo werten ARC bereits aus. rspamd unterstützt ARC-Validierung und ARC-Signing von Haus aus.

In der Praxis

Mailman 3 unterstützt ARC-Signing. Bei Mailman 2 lässt sich ARC über einen vorgeschalteten Milter nachrüsten. Wer rspamd als Milter einsetzt, kann ARC dort aktivieren und braucht keinen separaten Dienst.

Die Empfehlung für Listenbetreiber: ARC-Signing aktivieren und den Betreff nicht verändern (statt [liste] den List-Id-Header nutzen, den alle modernen Mailclients auswerten können). Für Absender mit p=reject: ARC reduziert das Problem erheblich, löst es aber nur wenn die Gegenseite ARC auch prüft. In der Übergangsphase hilft es, bekannte Listserver per DMARC-Whitelist von der Policy auszunehmen.

Fragen? Einfach melden.

« Ältere Beiträge

© 2026 -=Kernel-Error=- — RSS

Theme von Anders Norén — Hoch ↑