Installiere Postfix aus deiner Distribution und übernimm die Voreinstellungen, und du hast einen Mailserver, der deine Mail im Klartext an jeden Server schickt, der kein TLS anbietet, und Mail von jedem annimmt, der behauptet, irgendwer zu sein. Debians ausgeliefertes main.cf setzt beide TLS-Stufen auf may. Nichts prüft SPF. Nichts prüft DKIM. Eine Absenderdomain, die nicht existiert, wird angenommen.

Das ist kein Fehler. Es ist das, was die Standards vor zwanzig Jahren verlangt haben, und so laufen noch heute sehr viele Mailserver im Internet.

Dieser Beitrag baut die andere Art. Er nimmt die Hausregeln aus dem Leitbeitrag dieser Reihe und setzt jede einzelne in ein funktionierendes Postfix um, mit der Antwort, die jede erzeugt, wenn sie etwas verweigert. Die Begründung der Regeln steht in jenem Beitrag, und ich mache sie nicht zweimal. Dieser hier ist der Aufbau.

RegelDebians Postfix ab WerkDieser AufbauWas ein Durchbruch bekommt
Kein Klartext reinTLS angeboten, nie verlangtencrypt auf jedem Listener530 5.7.0 Must issue a STARTTLS command first
Kein Klartext rausTLS wenn angeboten, sonst Klartextencrypt, DANE oder MTA-STS wo veröffentlichtzurückgestellt, dsn=4.7.4 oder dsn=4.7.5
IPv6 auf alleminet_protocols = alldasselbe, plus AAAA auf jedem MXjeder Test unten lief auf beiden Familien
Relay nur mit Loginrelayed für mynetworksPort 25 relayed nie, 587 und 465 brauchen Login554 5.7.1 … Relay access denied
Ein Login sendet als sich selbstnicht geprüftEnvelope und From: beide an den Login gebunden553 5.7.1 … not owned by user
Absenderdomain muss existierennicht geprüftreject_unknown_sender_domain, Code 550550 5.1.8 … Domain not found
SPF-Fehler, DKIM-Fehlernicht geprüftRspamd, mit erzwungenen Rejects554 5.7.1 SPF hard fail / DKIM signature does not verify

Jede Antwort in diesem Beitrag wurde von einem laufenden Server mitgeschnitten, über IPv4 und IPv6. Von mehreren Einstellungen stellte sich heraus, dass sie nichts bewirken, solange nicht auch etwas anderes gesetzt ist. Jede davon habe ich gefunden, indem ich ihr beim Nichtstun zugesehen habe. Sie sind unten benannt, denn sie sind die, die dich beißen werden.

Das Labor

Ein isoliertes Netz, jeder Host auf beiden AdressfamilienDNS UND POLICYNetzwerk maillab: 10.89.50.0/24 und fd89:50::/64, internlab-nsBIND 9.20.30, signiert test..10 und ::10lab-resolverunbound 1.26.1, validiert.11 und ::11lab-tlspolpostfix-tlspol 1.14.4.25 und ::25lab-alphaPostfix 3.10.13alpha.test.20 und ::20lab-bravoStalwart 0.16.27bravo.test.21 und ::21lab-plainPostfix, kein TLSplain.test.22 und ::22lab-clientswaks, Testsendersender.test.50 und ::50MAILSERVERWegwerf-CA für die Zertifikate. Jeder Test in diesen Beiträgen lief über beide Familien.
Alles im Beitrag wurde zuerst hier ausgeführt. Nichts darauf kann das Internet erreichen, also ist auch nichts darauf eine Vermutung über das Internet
TeilVersionAufgabe
Postfix3.10.13, Debian trixieder MX und Submission-Server für alpha.test
Dovecot2.4.1Passwörter für Logins, jedes Mailprotokoll aus
Rspamd, Redis3.12.1, 8.0.2SPF, DKIM, DMARC, DKIM-Signierung
postfix-tlspol1.14.4ausgehende Policy aus TLSA und MTA-STS
BIND, unbound9.20.30, 1.26.1eine signierte private test.-Zone und ein Resolver, der sie validiert
DomainWofür sie da ist
alpha.testder Server, den dieser Beitrag baut
bravo.testStalwart, das seinen eigenen Beitrag bekommt
plain.testder schlechte Nachbar: unsignierte Zone, kein STARTTLS
sender.testSPF -all, DKIM, DMARC p=reject: zum Fälschen, Manipulieren und Weiterleiten

Jeder Listener bindet beide Familien, und jeder MX hat einen A- und einen AAAA-Eintrag. IPv6 ist hier kein Extra. Ein Mailserver ohne IPv6 ist ein Mangel, und zu beweisen, dass dieser es hat, ist einfach: führe jede Prüfung zweimal aus.

Stufe für Stufe

Eine SMTP-Sitzung ist eine feste Abfolge, und jede Regel in diesem Aufbau hat darin ihren natürlichen Platz. Die Reihenfolge zählt aus einem Grund: verweigere so früh, wie es die Beweislage erlaubt. Eine Absenderdomain, die nicht existiert, ist schon bei MAIL FROM bekannt, also gibt es keinen Grund, den Nachrichtentext anzunehmen, bevor man sie verweigert. Ein manipulierter Nachrichtentext ist erst nach DATA bekannt, also wird er dort verweigert.

Wo jede Regel greift, und was der Server sagteSTUFEDIE REGEL UND DIE IM LABOR MITGESCHNITTENE ANTWORTEHLOSTARTTLSAUTHMAIL FROMRCPT TODATAkein AUTH angeboten, bis TLS stehtsmtpd_tls_auth_only = yesüberspring es, und nichts sonst geht durch530 5.7.0 Must issue a STARTTLS command firstnur 587 und 465; Port 25 hat keinen Login554 5.7.1 Client host rejected: Access deniedAbsenderdomain muss existieren550 5.1.8 Sender address rejected: Domain not foundeigene Domain braucht Login554 5.7.1 Our own domain: send through submissionLogin muss die Adresse besitzen553 5.7.1 not owned by user alice@alpha.testPort 25 relayed nie554 5.7.1 Relay access deniedRspamd: SPF, DKIM, DMARC554 5.7.1 SPF hard fail / DKIM does not verifyFrom:-Header muss der Login sein554 5.7.1 You may only send as your own addressPostfix hält MAIL-FROM-Ablehnungen bis RCPT TO zurück, daher kommen diese drei Antworten einen Schritt später.
Jede Regel hat eine Stufe. Weise so früh zurück, wie es die Beweislage erlaubt

Der Rest des Beitrags geht diese Stufen der Reihe nach durch: TLS rein, TLS raus, Logins und Relaying, Absenderadressen, dann was Rspamd mit dem Nachrichtentext macht.

Kein Klartext herein

Eine Zeile:

smtpd_tls_security_level = encrypt

Ein Client, der versucht, STARTTLS zu überspringen, bekommt nichts als die Zurückweisung:

<-  220 mail.alpha.test ESMTP Postfix
 -> MAIL FROM:<carol@sender.test>
<** 530 5.7.0 Must issue a STARTTLS command first

Lies nun, was Postfix’ eigenes Handbuch zu dieser Einstellung sagt:

According to RFC 2487 this MUST NOT be applied in case of a publicly-referenced SMTP server. Instead, this should be used on dedicated servers, for example submission (port 587).1

RFC 2487 ist die alte Nummer für RFC 3207, und diese Regel ist der Grund, warum die meisten MX-Hosts im Internet Mail noch im Klartext annehmen. Ich wende sie trotzdem auf einen öffentlich referenzierten MX an. Mit Absicht. Der Leitbeitrag erklärt, warum ich die Regel für falsch halte, und der Preis ist echt: Ein Absender, dessen Server kein TLS kann, erreicht dich nicht. 2026 ist das ein Server, von dem du keine Mail willst.

Kein Klartext hinaus

Das ist die Hälfte, die die meisten Anleitungen falsch machen, weil die naheliegende Einstellung nicht das tut, was ihr Name nahelegt.

Postfix hat eine Sicherheitsstufe dane. Setzt du sie, erwartest du, dass Mail authentifiziert hinausgeht. Tut sie nicht. Hier ist, was postconf(5) sagt, dass sie tut, wenn die Gegenseite nichts veröffentlicht:

When DNSSEC-validated TLSA records are not found the effective tls security level is “may”.2

may ist opportunistisches TLS, was das TLS_README schlicht beschreibt: „Otherwise, messages are sent in the clear.“3 Der Großteil des Internets veröffentlicht keine TLSA-Einträge. Also bedeutet smtp_tls_security_level = dane für sich allein DANE für die Handvoll Domains, die es tun, und Klartext für alle anderen, deren Server kein STARTTLS anbietet. Opportunistisch, nur mit besserem Namen.

Die Lösung ist, encrypt zum Boden zu machen und jedes Ziel darüber zu heben, wenn es etwas Besseres veröffentlicht. postfix-tlspol übernimmt das Heben. Es ist ein kleiner Policy-Server, MIT-lizenziert, den Postfix über eine Socketmap zu jeder Empfängerdomain befragt. Es prüft zuerst auf brauchbare TLSA-Einträge in einer signierten Zone und dann auf MTA-STS, und antwortet mit der Policy, die Postfix nutzen soll:

smtp_dns_support_level = dnssec
smtp_tls_security_level = encrypt
smtp_tls_mandatory_protocols = >=TLSv1.2
smtp_tls_policy_maps = socketmap:inet:[fd89:50::25]:8642:QUERYwithTLSRPT
Drei Policies, und keine davon ist KlartextFRAGEWAS DIE DOMAIN VERÖFFENTLICHTPOLICY, DIE POSTFIX NUTZTpostfix-tlspolpro Empfängerdomain,über DNSSECBrauchbares TLSAin signierter ZoneMTA-STS, enforcePolicy über HTTPS geholtWeder nochder Großteil des Internetsdane-onlyder Schlüssel des Servers muss passensecure match=…CA-Zertifikat, nur gelisteter MXencryptTLS verlangt, nicht authentifiziertUND WENN SIE NICHT ERFÜLLBAR IST, WARTET DIE MAILkein STARTTLS angeboten: dsn=4.7.4Schlüssel passt nicht: dsn=4.7.5
Die ausgelieferte Stufe dane würde eine vierte Zeile hinzufügen: kein TLSA, im Klartext senden. Dieser Aufbau lässt sie weg

Sein README ist genau darin, was es zurückgibt: Es wird „Verify TLSA records for correctness and supported parameters, only then the dane-only policy (Mandatory DANE) will be returned.“4 Zu den drei Labordomains befragt, sagte es genau das:

level=INFO msg="Evaluated policy" origin=network domain=alpha.test policy=dane-only ttl=214
level=INFO msg="Evaluated policy" origin=network domain=bravo.test policy=dane-only ttl=198
level=INFO msg="No policy found" origin=network domain=plain.test ttl=1800

Keine Policy für plain.test bedeutet, Postfix fällt auf den encrypt-Boden zurück, und plain.test bietet kein STARTTLS an. Die Mail wartet:

postfix/smtp[194]: 78598130DA54: to=<someone@plain.test>, relay=mail.plain.test[fd89:50::22]:25,
  delay=0.02, delays=0.01/0.01/0.01/0, dsn=4.7.4,
  status=deferred (TLS is required, but was not offered by host mail.plain.test[fd89:50::22])

Und der Fall, der beweist, dass DANE etwas tut. Früh im Aufbau lieferte Stalwart auf bravo.test noch sein eigenes Bootstrap-Zertifikat aus, während die Zone den TLSA-Eintrag für das echte veröffentlichte. Ein Server mit einem völlig gültigen Zertifikat, nur eben nicht dem, für das der Domaininhaber einstand:

postfix/smtp[196]: server certificate verification failed for mail.bravo.test[fd89:50::21]:25:
  num=65:no matching DANE TLSA records
postfix/smtp[196]: 79160130DA56: to=<someone@bravo.test>, relay=mail.bravo.test[10.89.50.21]:25,
  delay=5.9, delays=0/0.01/5.9/0, dsn=4.7.5, status=deferred (Server certificate not verified)

Zurückgestellt ist in beiden Fällen die richtige Antwort. Eine zurückgestellte Nachricht wird tagelang erneut versucht, was der Gegenseite Zeit lässt, ihr Ende zu reparieren, und nichts geht verloren. Eine im Klartext gesendete Nachricht lässt sich nicht zurückholen. Insofern ist Warten immer der billigere Fehler.

Der Resolver ist Teil der Vertrauenskette

smtp_dns_support_level = dnssec bringt Postfix dazu, seinen Resolver nach validierten Antworten zu fragen, und dann dem AD-Bit des Resolvers zu glauben, dem Flag, das sagt „ich habe die Signaturen geprüft“. Postfix validiert selbst nichts. Es vertraut. Welcher Resolver auch immer in /etc/resolv.conf steht, entscheidet jetzt, ob deine Mail authentifiziert ist.

Du findest Ratschläge, die sagen, glibc werfe dieses Bit weg, wenn resolv.conf nicht options trust-ad trägt. Ich habe es getestet, und auf diesem Postfix macht es keinen Unterschied: DANE passte mit der Option und ohne sie. Der Quelltext erklärt, warum. Postfix setzt das Flag selbst, wann immer es nach DNSSEC fragt:

    if (DNS_WANT_DNSSEC_VALIDATION(flags))
	flags |= (RES_USE_EDNS0 | RES_TRUSTAD);

Das ist aus src/dns/dns_lookup.c.5 Was also zählt, ist keine Resolver-Option. Es ist, auf welchen Resolver du zeigst. Er muss validieren, und der Weg zu ihm muss einer sein, dem du vertraust, was in der Praxis heißt, er läuft auf dem Mailserver selbst oder in einem Netz, das sonst niemand erreichen kann. Ein öffentlicher Resolver quer durchs Internet setzt dir das AD-Bit bereitwillig. Und jeder, der zwischen dir und ihm sitzt, auch.

DANE: Veröffentliche den Schlüssel, nicht das Zertifikat

Ein TLSA-Eintrag legt fest, was ein sendender Server sehen soll, wenn er sich verbindet. Der, den man nimmt, ist 3 1 1, und jede der drei Zahlen ist eine Entscheidung:

FeldWertBedeutungWarum dieser
Usage3, DANE-EEpasst zum eigenen Zertifikat des Serverskeine CA in der Kette, der man vertrauen muss oder die versagen kann
Selector1, SPKInur den öffentlichen Schlüssel hashenübersteht jede Erneuerung auf demselben Schlüssel
Matching1, SHA-256ein Digest, nicht der ganze Schlüsseljeder DANE-Client muss es unterstützen

RFC 7672 gibt den Grund für diese Kombination in einer Zeile:

this record type works for all clients and need not change across certificate renewals with the same key.6

Der Hash ist eine einzige Pipeline, und hier ist sie gegen das Laborzertifikat und den Eintrag, den die signierte Zone ausliefert:

$ openssl x509 -in mail.alpha.test.crt -noout -pubkey \
    | openssl pkey -pubin -outform DER | sha256sum
093a80cac907a52eb41d17df20163c95f2e15d6702c76f65174d7fc32460c69f  -

$ dig +dnssec _25._tcp.mail.alpha.test TLSA
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
_25._tcp.mail.alpha.test. 165 IN TLSA 3 1 1 093A80CAC907A52EB41D17DF20163C95F2E15D6702C76F65174D7FC3 2460C69F

Das Labor bewies den Teil „need not change“ aus Versehen. Mittendrin wiesen Python 3.13s strengere Zertifikatsprüfungen die Laborzertifikate zurück, weil ihnen ein Authority Key Identifier fehlte, also wurde jedes einzelne neu ausgestellt. Dieselben privaten Schlüssel, neue Zertifikate. Nicht ein TLSA-Eintrag änderte sich, und nicht ein DANE-Absender bemerkte es.

Jetzt die Falle. Die meisten erneuern ihre Zertifikate mit certbot, automatisch, alle paar Wochen. Aus certbots eigener Dokumentation:

Not reusing private keys is the default behavior of Certbot.7

Also macht die Standard-Erneuerung jedes Mal einen neuen Schlüssel. Der TLSA-Eintrag passt nicht mehr, und jeder Absender, der DANE prüft, stellt deine Mail am Erneuerungstag zurück. Auf deiner Seite bricht nichts. Nichts sagt dir Bescheid. Führe certbot mit --reuse-key aus, und der Eintrag überlebt jedes Zertifikat, das du dahinter setzt. Wenn der Schlüssel doch wechseln muss, gibt es dafür eine Reihenfolge:

Gleicher Schlüssel: nichts zu tun. Neuer Schlüssel: vier Schritte, der Reihe nachERNEUERUNG AUF DEM GLEICHEN SCHLÜSSELZertifikat erneuerngleicher privater SchlüsselTLSA unverändert093a80… vorher/nachherNichts zu publizierenniemand bemerkt esDEN SCHLÜSSEL WECHSELN1. Neues TLSA setzenneben dem alten2. Zwei TTLs wartenCaches halten das Alte3. Schlüssel wechselnServer zeigt den neuen4. Das alte entfernenerst jetzt, nie vorherSchritt 2 überspringenund jeder DANE-Absender, der den alten Eintrag gecacht hat, stellt deine Mail zurück, bis er abläuft.
Verwende den Schlüssel bei der Erneuerung wieder, und der TLSA-Eintrag überlebt jedes Zertifikat, das du dahinter setzt

Schritt zwei ist der, den man überspringt. Das Ergebnis ist zurückgestellte Mail, bis der alte Eintrag aus jedem Cache herausgealtert ist, und sie kommt von genau den Absendern, die Sicherheit richtig machen.

Niemand relayed ohne Login

Relaying heißt, Mail an eine Domain weiterzuschicken, für die du nicht das Ziel bist. Ein offenes Relay ist, wie Spam durch den Ruf eines anderen gewaschen wird. Der alte Weg, eins zu schließen, war, einem Adressbereich zu vertrauen, sodass alles aus dem Büronetz relayen durfte. Das ist Vertrauen nach Standort. In einem Netz mit Gäste-WLAN, VPN-Nutzern und dem einen oder anderen kompromittierten Laptop ist Standort sehr wenig wert.

Also wird keiner Adresse vertraut außer der Maschine selbst:

mynetworks = 127.0.0.0/8 [::1]/128
smtpd_relay_restrictions = reject_unauth_destination

Port 25 ist für Mail, die für diese Domain ankommt, und nichts sonst. Er relayed nie, für niemanden:

<** 554 5.7.1 <someone@plain.test>: Relay access denied

Nutzer senden über Submission: Port 587 mit STARTTLS oder 465 mit TLS ab dem ersten Byte. Beide wollen einen Login, bevor sie irgendetwas tun. Dovecot prüft die Passwörter, mit jedem Mailprotokoll abgeschaltet, sodass es nichts anderes tut, und Postfix fragt es über einen Socket. Der Submission-Listener in master.cf:

submission inet n       -       y       -       -       smtpd
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_sender_restrictions=reject_sender_login_mismatch
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject

smtpd_tls_auth_only bedeutet, der Server bietet AUTH gar nicht erst an, bevor TLS steht, sodass ein Passwort nie im Klartext über die Leitung geht. AUTH-Zeilen in der Antwort auf ein Klartext-EHLO: null. Ein Client, der sich mit 587 verbindet und ohne Login zu senden versucht, kommt nirgendwo hin:

<** 554 5.7.1 <unknown[fd89:50::50]>: Client host rejected: Access denied

Ein Login besitzt seine Adressen

Ein Login beweist, wer du bist. Es sagt nichts darüber, als welche Adressen du senden darfst. Ohne diese Verbindung kann jeder Nutzer auf dem Server als der Finanzchef senden, und die Mail wird signiert und zugestellt, als wäre sie echt. Postfix verknüpft die beiden mit einer Zuordnung von Adresse zu Eigentümer:

alice@alpha.test       alice@alpha.test
bob@alpha.test         bob@alpha.test
postmaster@alpha.test  alice@alpha.test

reject_sender_login_mismatch auf den Submission-Ports erzwingt das gegen den Envelope-Absender. Als alice eingeloggt, versucht als bob zu senden:

<** 553 5.7.1 <bob@alpha.test>: Sender address rejected: not owned by user alice@alpha.test

Das deckt den Envelope ab. Es deckt nicht den From:-Header ab, der die Adresse ist, die der Empfänger tatsächlich sieht, und postconf(5) sagt das in seinen Anmerkungen zu genau dieser Restriction:

to enforce that the From: header address matches the envelope sender (MAIL FROM) address, use an external filter such as a Milter8

Rspamd ist hier ohnehin der Milter, also ist die Header-Prüfung eine kurze Lua-Regel, die den eingeloggten Nutzer mit der From:-Adresse vergleicht, plus ein erzwungener Reject auf das Symbol, das sie auslöst:

rspamd_config.AUTH_FROM_NOT_LOGIN = {
  callback = function(task)
    local user = task:get_user()
    if not user then return false end
    local from = task:get_from('mime')
    if not from or not from[1] or not from[1].addr then return true, 'no From address' end
    if string.lower(from[1].addr) ~= string.lower(user) then
      return true, from[1].addr .. ' is not ' .. user
    end
    return false
  end,
  score = 0.0,
  description = 'Authenticated user sent with a From: address that is not their own',
  group = 'headers',
}

Alice sendet mit MAIL FROM:<alice@alpha.test> und From: bob@alpha.test:

<** 554 5.7.1 You may only send as your own address

Ein Anzeigename ist in Ordnung, und ein Wechsel der Groß- und Kleinschreibung auch. Alice <ALICE@alpha.test> geht durch, weil der Vergleich auf der Adresse liegt und Groß- und Kleinschreibung ignoriert.

Die Prüfung, die sich selbst abschaltet

Hier ist die erste der Einstellungen, die stillschweigend nichts tun. Der naheliegende Weg, Außenstehende davon abzuhalten, deine eigene Domain auf Port 25 zu fälschen, ist, die Login-Mismatch-Prüfungen auch dort hinzusetzen. Postfix akzeptiert die Konfiguration, startet und loggt das:

warning: restriction `reject_authenticated_sender_login_mismatch' ignored: no SASL support
warning: restriction `reject_unauthenticated_sender_login_mismatch' ignored: no SASL support

Port 25 hat SASL abgeschaltet, ganz zu Recht, weil sich dort niemand einloggt. Also wird die Prüfung übersprungen. Die Mail wird angenommen. Der einzige Beleg ist eine Warnung in einem Log, das niemand liest, und mit weggefallener Prüfung könnte jeder im Internet MAIL FROM:<alice@alpha.test> an diesen Server senden und sie an alices Kollegen zustellen lassen, als hätte sie sie geschrieben.

Die Lösung ist, unsere eigenen Domains auf Port 25 namentlich abzulehnen, weil Mail von uns dort nie rechtmäßig ankommt:

smtpd_sender_restrictions = reject_non_fqdn_sender, reject_unknown_sender_domain,
    check_sender_access texthash:/etc/postfix/own_domains

wobei own_domains eine Zeile enthält, alpha.test REJECT Our own domain: send through submission with a login. Von außen getestet:

<** 554 5.7.1 <alice@alpha.test>: Sender address rejected: Our own domain: send through submission with a login

Absenderdomains, die nicht existieren

Wenn die Domain in MAIL FROM nicht existiert, kann nie etwas an sie zurückgesendet werden, und es kam nie etwas Rechtmäßiges von ihr. reject_unknown_sender_domain verweigert Mail, wenn diese Domain „no DNS MX and no DNS A record“ hat.9 Postfix’ Standard-Antwortcode dafür ist 450, ein vorübergehender Fehler, der den Fälscher einlädt, es später erneut zu versuchen. Ich habe ihn auf 550 geändert.

postconf(5) ist unverblümt zu dieser Einstellung: „Do not change this unless you have a complete understanding of RFC 5321.“10 Na gut. Hier die Begründung. Ein 4xx sagt dem Absender, in die Warteschlange zu gehen und erneut zu versuchen, was richtig ist, wenn sich die Antwort noch ändern kann. Eine Domain ohne Einträge jetzt wird sich in den nächsten fünf Minuten keine zulegen. Postfix trennt das von dem Fall, in dem die Antwort noch nicht bekannt ist: Ein DNS-Fehler wird von unknown_address_tempfail_action behandelt, das weiterhin zurückstellt. Den Code zu ändern ändert nur den Fall, der eine Antwort hat.

Das Labor testete alle drei, plus den einen, der der IPv6-Regel wichtig ist. Die postconf-Formulierung sagt „A record“, und eine Domain, deren einzige Adresse ein AAAA-Eintrag ist, sollte nicht als fehlend behandelt werden:

AbsenderdomainDNS sagtAntwort
nosuch-lab.co.ukNXDOMAIN550 5.1.8 <someone@nosuch-lab.co.uk>: Sender address rejected: Domain not found
v6only-lab.co.uknur AAAA, kein A, kein MXangenommen
dnsfail-lab.co.ukSERVFAIL450 4.1.8 … Sender address rejected: Domain not found, später erneut versucht
sender.testMX, A, AAAAangenommen

In der Praxis deckt „A record“ auch AAAA ab, und eine reine IPv6-Domain ist sicher. Die Verweigerung kommt bei MAIL FROM, bevor der Absender ein einziges Byte des Nachrichtentexts gesendet hat. Postfix hält eine MAIL FROM-Ablehnung standardmäßig bis zum ersten RCPT TO zurück, sodass die Antwort einen Schritt später ankommt, als du vielleicht erwartest, weshalb das Stufendiagramm sie dort zeigt.

SPF, DKIM und DMARC, mit Zähnen

Diese drei sind im Leitbeitrag richtig behandelt. In je einer Zeile: SPF sagt, welche Server für eine Domain senden dürfen, DKIM signiert die Nachricht, sodass Änderungen sichtbar werden, und DMARC sagt, was zu tun ist, wenn sie fehlschlagen, und ob sie zur sichtbaren From:-Domain passen. Die Hausregel ist, bei einem harten SPF-Fehler und bei einer DKIM-Signatur zurückzuweisen, die vorhanden ist und nicht verifiziert. Unsignierte Mail ist kein DKIM-Fehler. DMARC p=reject auf der Domain des Absenders ist, was das abdeckt.

Rspamd macht alle drei in einem Milter, plus DKIM-Signierung für ausgehende Mail. Postfix übergibt ihm jede Nachricht, und wenn Rspamd nicht antwortet, wartet die Mail, statt ungeprüft durchzugehen:

smtpd_milters = inet:127.0.0.1:11332
non_smtpd_milters = $smtpd_milters
milter_default_action = tempfail

Rspamd arbeitet mit Scores, und das ist das Erste, was man darüber verstehen muss. Ab Werk wird nichts zurückgewiesen, weil SPF fehlschlug. Nichts. Jedes Ergebnis addiert oder subtrahiert ein paar Punkte, und die Nachricht wird zurückgewiesen, wenn die Summe 15 übersteigt. Das Modul force_actions ist, was aus einem einzelnen Ergebnis eine Entscheidung macht:

rules {
  SPF_FAIL_REJECT {
    action = "reject";
    expression = "R_SPF_FAIL";
    message = "SPF hard fail: this host is not allowed to send for that domain";
  }
  DKIM_FAIL_REJECT {
    action = "reject";
    expression = "R_DKIM_REJECT";
    message = "DKIM signature does not verify: the message was changed or forged";
  }
  DMARC_REJECT {
    action = "reject";
    expression = "DMARC_POLICY_REJECT";
    message = "DMARC policy of the sender's domain says reject";
  }
}

Fünf Nachrichten von sender.test, gesendet vom Client im Labor. Der SPF-Eintrag erlaubt ip6:fd89:50::50 und nichts sonst, also ist derselbe Client, der über IPv4 sendet, ein nicht autorisierter Server für diese Domain. Genau so sieht auch eine weitergeleitete Nachricht aus:

FallGesendet alsSPFDKIMDMARCRspamd-ScoreAntwort
Asigniert, von der gelisteten IPv6-Adressepasspasspass3.59 von 15angenommen
Bsigniert, dann die Bankdaten geändertpassfailpass2.30 von 15554 5.7.1 DKIM signature does not verify
Cunsigniert, von IPv4failnonereject5.00 von 15554 5.7.1 SPF hard fail
Dsigniert und unversehrt, von IPv4failpasspass2.30 von 15554 5.7.1 SPF hard fail
Eunsigniert, von der gelisteten IPv6-Adressepassnonepass3.80 von 15angenommen

Schau dir Fall B an. Es ist eine Rechnung mit Bankleitzahl und Kontonummer, geändert, nachdem sie signiert war. Rechnungsbetrug, mit anderen Worten. SPF bestand, weil sie vom richtigen Server kam. DMARC bestand, weil SPF bestand und zur From:-Domain passte. Nach seinen Scores gab Rspamd ihr 2.30 von 15 und hätte sie zugestellt. Das Einzige, was sie aufhielt, war der erzwungene Reject auf die kaputte Signatur. DMARC für sich allein lässt eine manipulierte Nachricht durch, solange eine der beiden Prüfungen überlebt, und das ist gut zu wissen, bevor du eine Domain für geschützt hältst, weil ihr DMARC reject sagt.

Fall D ist der Preis, und er sollte klar gesagt werden. Eine korrekt signierte, unangetastete Nachricht wird abgelehnt, weil sie von einem Server kam, der nicht im SPF-Eintrag steht. Genau das tut Weiterleitung: Der Weiterleiter schickt deine Nachricht von seiner eigenen Adresse weiter. Allein auf SPF zurückzuweisen bedeutet, dass weitergeleitete Mail von Domains mit -all abgewiesen wird. SRS auf dem Weiterleiter behebt das, und DKIM über SPF zu vertrauen würde es auch, aber die Hausregel ist die Hausregel, und der Preis wird schriftlich festgehalten.

Die Voreinstellung, die jedem privaten Netz vertraut

Die zweite Einstellung, die stillschweigend nichts tut. Rspamd überspringt SPF und einige andere Prüfungen für Adressen, die es als lokal ansieht, und die ausgelieferte Liste in options.inc ist diese:

local_addrs = [192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, fd00::/8, 169.254.0.0/16, fe80::/10];

Jeder private Bereich, beide Familien. Wenn dein MX hinter einem Load Balancer, einem NAT oder einem Proxy auf einer privaten Adresse sitzt, wird SPF dafür stillschweigend übersprungen. Im Labor, das durchweg private Adressierung nutzt, schlug SPF nie fehl, bis das geändert wurde.

Die naheliegende Lösung funktioniert nicht. Rspamd hat zwei Verzeichnisse für lokale Einstellungen, und sein FAQ legt den Unterschied dar: local.d „Merges with defaults. Collections and lists are combined“, während override.d „Replaces defaults.“11 Eine engere Liste in local.d/options.inc wird mit der ausgelieferten kombiniert, was heißt, sie engt nichts ein. Sie muss in override.d/options.inc:

local_addrs = [127.0.0.0/8, ::1];

und rspamadm configdump options ist, wie du prüfst, was bei Rspamd tatsächlich herauskam, statt was du zu schreiben glaubst.

Greylisting ist in diesem Aufbau ebenfalls abgeschaltet. Es schickt unbekannten Absendern eine vorübergehende Ablehnung und nimmt sie beim erneuten Versuch an, was zwar etwas Spam stoppt, aber Rspamds Greylisting antwortet, bevor SPF und DMARC zu Wort gekommen sind, sodass du beim Testen eine Zurückstellung siehst, wo du ein Urteil erwartet hast.12 Die Tests oben sind nur mit abgeschaltetem Greylisting aussagekräftig.

Deine eigenen Bounces, abgelehnt von deinem eigenen DMARC

Die letzte Falle tauchte auf, als Stalwart im Labor einen Bounce von diesem Server ablehnte. Postfix sendet Bounces mit leerem Envelope-Absender und From: MAILER-DAEMON@mail.alpha.test. Ohne Envelope-Absender bleibt die einzige SPF-Prüfung auf dem Namen, den der Server im HELO angab, mail.alpha.test, der keinen SPF-Eintrag hatte. Der Bounce war auch nicht signiert. Also fiel DMARC auf die Policy für alpha.test zurück, fand p=reject, und der empfangende Server tat genau das, was die Domain ihm sagte:

smtp.dmarc-fail … domain = "alpha.test", policy = "reject", result = DMARC check failed
  (dmarc.fail) { causedBy = Policy not aligned }

Die eigene Policy der Domain warf ihre eigenen Zustellfehler weg. Jeder MX-Hostname bekommt seinen eigenen SPF-Eintrag, was das behebt:

mail IN TXT "v=spf1 a -all"

Sechs Einstellungen, die fertig aussehen

Die Fallen oben teilen eine Form. Jede von ihnen ist eine Einstellung, die fertig aussieht, keinen Fehler erzeugt und rein gar nichts tut.

Sieht fertig ausWas tatsächlich passiertGefunden durch
smtp_tls_security_level = daneKlartext überall, wo kein TLSA veröffentlicht istLesen von postconf(5)
Login-Mismatch-Prüfungen auf Port 25übersprungen, eine Warnung im LogSenden einer Fälschung
local_addrs in local.d eingeengtmit der Voreinstellung kombiniert, nichts eingeengtrspamadm configdump
DMARC p=reject beim Absendermanipulierte Mail geht durch, wenn SPF bestehtFall B
certbot-Erneuerungenneuer Schlüssel, TLSA passt nicht mehrLesen von certbots Doku
DMARC p=reject auf deiner eigenen Domaindeine eigenen Bounces abgelehntein abgelehnter Bounce

Zwei davon stehen in der Dokumentation, wenn du den richtigen Absatz der richtigen Seite liest. Die anderen vier tauchten erst auf, als ich die Nachricht sendete, die die Regel aufhalten sollte, und ansah, was zurückkam. Die Konfiguration zurückzulesen und ihr zuzustimmen fand keine von ihnen. Wenn du eine Gewohnheit aus diesem Beitrag mitnimmst, nimm diese, denn jede Zeile hier lässt sich in einen Server kopieren und trotzdem nicht funktionieren.

Die Voreinstellungen sind die Entscheidung von jemand anderem

Jede Verweigerung in diesem Beitrag ist eine Einstellung, die Postfix seit Jahren hat. Sogar DANE, das Neueste hier, ist seit 2.11 drin.13 Nichts davon ist neu, nichts davon ist teuer, und der Server darunter ist dieselbe freie Software, die er immer war. Der Abstand zwischen einem Mailserver, der eine veränderte Rechnung durchlässt, und einem, der sie bei DATA verweigert, ist eine Seite Konfiguration und ein Nachmittag Testen.

Warum also läuft so wenig des Internets so? Weil die Voreinstellungen entscheiden, und die Voreinstellungen wurden gesetzt, um sicherzustellen, dass Mail durchkam, in einem Internet, in dem TLS die Ausnahme war. Damals waren sie eine vernünftige Entscheidung. Lass sie heute in Ruhe, und du hast deine Sicherheitsentscheidungen an eine Wahl abgegeben, die jemand 2002 traf, für ein Netz, das nicht mehr existiert, über Mail, die nicht seine war.

Einen Mailserver zu betreiben ist eine Entscheidung, für das verantwortlich zu sein, was durch ihn läuft. Diese Einstellungen sind die niedergeschriebene Entscheidung. Die Voreinstellungen sind, was du bekommst, wenn niemand sie getroffen hat.


  1. postconf(5): smtpd_tls_security_level — die Stufe encrypt: „According to RFC 2487 this MUST NOT be applied in case of a publicly-referenced SMTP server.“ ↩︎

  2. postconf(5): smtp_tls_security_level — die Stufe dane: „When DNSSEC-validated TLSA records are not found the effective tls security level is ‚may’.“ ↩︎

  3. Postfix TLS_README: Opportunistic TLS — „The SMTP transaction is encrypted if the STARTTLS ESMTP feature is supported by the server. Otherwise, messages are sent in the clear.“ ↩︎

  4. postfix-tlspol on GitHub — MIT-lizenzierter MTA-STS- und DANE-Policy-Server für Postfix: „Verify TLSA records for correctness and supported parameters, only then the dane-only policy (Mandatory DANE) will be returned.“ Version 1.14.4 hier verwendet. ↩︎

  5. Postfix source, src/dns/dns_lookup.c — „Automatically turns on the RES_TRUSTAD flag on systems that support this flag“, und der Code, der das tut, wenn DNSSEC-Validierung angefordert wird. ↩︎

  6. RFC 7672 §3.1.1 — SMTP-Sicherheit über opportunistisches DANE TLS. TLSA-Einträge für SMTP-Server „should, in most cases, be ‚DANE-EE(3) SPKI(1) SHA2-256(1)’ records“. ↩︎

  7. Certbot user guide, command-line options — --reuse-key: „When renewing, use the same private key as the existing certificate. (default: False)“, und unter --no-reuse-key: „Not reusing private keys is the default behavior of Certbot.“ ↩︎

  8. postconf(5): reject_known_sender_login_mismatch — „to enforce that the From: header address matches the envelope sender (MAIL FROM) address, use an external filter such as a Milter“. ↩︎

  9. postconf(5): reject_unknown_sender_domain — weist zurück, wenn die MAIL-FROM-Domain „no DNS MX and no DNS A record“ hat, oder einen fehlerhaften MX. ↩︎

  10. postconf(5): unknown_address_reject_code — Standard 450: „Do not change this unless you have a complete understanding of RFC 5321.“ ↩︎

  11. Rspamd FAQ: der Unterschied zwischen local.d und override.d — local.d „Merges with defaults. Collections and lists are combined“; override.d „Replaces defaults. Entire sections are overwritten.“ ↩︎

  12. Rspamd greylisting module — „The greylisting module triggers a soft reject action, which is intended to be interpreted by the MTA as a temporary rejection“. ↩︎

  13. Postfix TLS_README: DANE — „Postfix ≥ 2.11 supports the new dane and dane-only security levels“. ↩︎