Install Postfix from your distribution and accept the defaults, and you have a mail server that sends your mail in the clear to any server that does not offer TLS, and takes mail from anybody claiming to be anybody. Debian’s shipped main.cf sets both TLS levels to may. Nothing checks SPF. Nothing checks DKIM. A sender domain that does not exist is accepted.

That is not a bug. It is what the standards asked for twenty years ago, and it is how a great many mail servers on the internet still run today.

This post builds the other kind. It takes the house rules from the lead post in this series and puts every one of them into a working Postfix, with the reply each one produces when it refuses something. The argument for the rules lives in that post and I will not make it twice. This one is the build.

RuleDebian’s Postfix as shippedThis buildWhat a breach gets
No plaintext inTLS offered, never requiredencrypt on every listener530 5.7.0 Must issue a STARTTLS command first
No plaintext outTLS if offered, clear text if notencrypt, DANE or MTA-STS where publisheddeferred, dsn=4.7.4 or dsn=4.7.5
IPv6 on everythinginet_protocols = allthe same, plus AAAA on every MXevery test below ran on both families
Relay only with a loginrelays for mynetworksport 25 never relays, 587 and 465 need a login554 5.7.1 … Relay access denied
A login sends as itselfnot checkedenvelope and From: both tied to the login553 5.7.1 … not owned by user
Sender domain must existnot checkedreject_unknown_sender_domain, code 550550 5.1.8 … Domain not found
SPF fail, DKIM failnot checkedRspamd, with forced rejects554 5.7.1 SPF hard fail / DKIM signature does not verify

Every reply in this post was captured from a running server, over IPv4 and IPv6. Several settings turned out to do nothing until something else was also set. I found each of those by watching it do nothing. They are called out below, because they are the ones that will bite you.

The Lab

One isolated network, every host on both address familiesDNS AND POLICYnetwork maillab: 10.89.50.0/24 and fd89:50::/64, internallab-nsBIND 9.20.30, signs test..10 and ::10lab-resolverunbound 1.26.1, validates.11 and ::11lab-tlspolpostfix-tlspol 1.14.4.25 and ::25lab-alphaPostfix 3.10.13alpha.test.20 and ::20lab-bravoStalwart 0.16.27bravo.test.21 and ::21lab-plainPostfix, no TLSplain.test.22 and ::22lab-clientswaks, test sendersender.test.50 and ::50MAIL SERVERSThrowaway CA for the certificates. Every test in these posts ran over both families.
Everything in the post was run here first. Nothing on it can reach the internet, so nothing on it is a guess about the internet either
PartVersionJob
Postfix3.10.13, Debian trixiethe MX and submission server for alpha.test
Dovecot2.4.1passwords for logins, every mail protocol off
Rspamd, Redis3.12.1, 8.0.2SPF, DKIM, DMARC, DKIM signing
postfix-tlspol1.14.4outbound policy from TLSA and MTA-STS
BIND, unbound9.20.30, 1.26.1a signed private test. zone, and a resolver that validates it
DomainWhat it is for
alpha.testthe server this post builds
bravo.testStalwart, which gets its own post
plain.testthe bad neighbour: unsigned zone, no STARTTLS
sender.testSPF -all, DKIM, DMARC p=reject: for forging, tampering and forwarding

Every listener binds both families, and every MX has an A and an AAAA record. IPv6 is not an extra here. A mail server without it is a defect, and proving this one has it is simple: run every check twice.

Stage By Stage

An SMTP session is a fixed sequence, and every rule in this build has a natural place in it. The order matters for one reason: refuse as early as the evidence allows. A sender domain that does not exist is known at MAIL FROM, so there is no reason to take the body before refusing it. A tampered body is only known after DATA, so that is where it gets refused.

Where each rule bites, and what the server saidSTAGETHE RULE, AND THE REPLY CAPTURED IN THE LABEHLOSTARTTLSAUTHMAIL FROMRCPT TODATAno AUTH offered until TLS is upsmtpd_tls_auth_only = yesskip it and nothing else is accepted530 5.7.0 Must issue a STARTTLS command first587 and 465 only; port 25 has no login554 5.7.1 Client host rejected: Access deniedsender domain must exist550 5.1.8 Sender address rejected: Domain not foundour own domain needs a login554 5.7.1 Our own domain: send through submissionthe login must own the address553 5.7.1 not owned by user alice@alpha.testport 25 never relays554 5.7.1 Relay access deniedRspamd: SPF, DKIM, DMARC554 5.7.1 SPF hard fail / DKIM does not verifyFrom: header must be the login554 5.7.1 You may only send as your own addressPostfix holds MAIL FROM rejections until RCPT TO, so those three replies arrive one step later.
Every rule has a stage. Reject as early as the evidence allows

The rest of the post goes through those stages in order: TLS in, TLS out, logins and relaying, sender addresses, then what Rspamd does with the body.

No Plaintext Coming In

One line:

smtpd_tls_security_level = encrypt

A client that tries to skip STARTTLS gets nothing but the refusal:

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

Now read what Postfix’s own manual says about that setting:

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 is the old number for RFC 3207, and that rule is why most MX hosts on the internet still accept mail in the clear. I am applying it to a publicly referenced MX anyway. On purpose. The lead post explains why I think the rule is wrong, and the cost is real: a sender whose server cannot do TLS cannot reach you. In 2026 that is a server you do not want mail from.

No Plaintext Going Out

This is the half most guides get wrong, because the obvious setting does not do what its name suggests.

Postfix has a dane security level. Set it and you would expect mail to go out authenticated. It does not. Here is what postconf(5) says it does when the far end publishes nothing:

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

may is opportunistic TLS, which the TLS_README describes plainly: “Otherwise, messages are sent in the clear.”3 Most of the internet publishes no TLSA records. So smtp_tls_security_level = dane on its own means DANE for the handful of domains that do it, and clear text for everybody else whose server does not offer STARTTLS. Opportunistic, with a better name.

The fix is to make encrypt the floor and lift each destination above it when it publishes something better. postfix-tlspol does the lifting. It is a small policy server, MIT licensed, that Postfix asks about every recipient domain over a socketmap. It checks for usable TLSA records in a signed zone first and MTA-STS second, and answers with the policy Postfix should use:

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
Three policies, and none of them is plaintextASKWHAT THE DOMAIN PUBLISHESPOLICY POSTFIX USESpostfix-tlspolper recipient domain,over DNSSECUsable TLSAin a signed zoneMTA-STS, enforcepolicy fetched over HTTPSNeithermost of the internetdane-onlythe server's key must matchsecure match=…CA certificate, listed MX onlyencryptTLS required, not authenticatedAND WHEN IT CANNOT BE MET, THE MAIL WAITSno STARTTLS offered: dsn=4.7.4key does not match: dsn=4.7.5
The shipped dane level would add a fourth row: no TLSA, send in the clear. This setup leaves it out

Its README is specific about what it returns: it will “Verify TLSA records for correctness and supported parameters, only then the dane-only policy (Mandatory DANE) will be returned.”4 Asked about the three lab domains, it said exactly that:

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

No policy for plain.test means Postfix falls back to the encrypt floor, and plain.test offers no STARTTLS. The mail waits:

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])

And the case that proves DANE is doing something. Early in the build, Stalwart on bravo.test was still serving its own bootstrap certificate while the zone published the TLSA record for the real one. A server with a perfectly good certificate, just not the one the domain owner vouched for:

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)

Deferred is the right answer in both cases. A deferred message is retried for days, which leaves the other side time to fix their end, and nothing is lost. A message sent in the clear cannot be unsent. As such, waiting is always the cheaper failure.

The resolver is part of the trust chain

smtp_dns_support_level = dnssec makes Postfix ask its resolver for validated answers, and then believe the resolver’s AD bit, the flag that says “I checked the signatures”. Postfix validates nothing itself. It trusts. Whatever resolver is named in /etc/resolv.conf is now deciding whether your mail is authenticated.

You will find advice saying glibc throws that bit away unless resolv.conf carries options trust-ad. I tested it, and on this Postfix it makes no difference: DANE matched with the option and without it. The source explains why. Postfix sets the flag itself whenever it asks for DNSSEC:

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

That is from src/dns/dns_lookup.c.5 So what matters is not a resolver option. It is which resolver you point at. It must validate, and the path to it must be one you trust, which in practice means it runs on the mail server itself or on a network nobody else can reach. A public resolver across the internet will happily set AD for you. So will anybody sitting between you and it.

DANE: Publish The Key, Not The Certificate

A TLSA record pins what a sending server should see when it connects. The one to use is 3 1 1, and each of the three numbers is a choice:

FieldValueMeaningWhy this one
Usage3, DANE-EEmatch the server’s own certificateno CA in the chain to trust or to fail
Selector1, SPKIhash the public key onlysurvives every renewal on the same key
Matching1, SHA-256a digest, not the full keyevery DANE client must support it

RFC 7672 gives the reason for that combination in one line:

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

The hash is one pipeline, and here it is against the lab certificate and the record the signed zone serves:

$ 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

The lab proved the “need not change” part by accident. Halfway through, Python 3.13’s stricter certificate checks refused the lab certificates for missing an authority key identifier, so every one of them was reissued. Same private keys, new certificates. Not one TLSA record changed, and not one DANE sender noticed.

Now the trap. Most people renew their certificates with certbot, automatically, every few weeks. From certbot’s own documentation:

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

So the default renewal makes a new key every time. The TLSA record stops matching, and every sender that checks DANE starts deferring your mail on renewal day. Nothing on your side breaks. Nothing tells you. Run certbot with --reuse-key and the record outlives every certificate you put behind it. When the key does have to change, there is an order to it:

Same key: nothing to do. New key: four steps, in orderRENEWING ON THE SAME KEYRenew the certificatesame private keyTLSA unchanged093a80… before and afterNothing to publishDANE senders never noticeCHANGING THE KEY1. Publish new TLSAbeside the old one2. Wait two TTLscaches hold the old set3. Switch the keyserver presents the new one4. Remove the oldonly now, never beforeSkip step 2and every DANE sender that cached the old record defers your mail until it expires.
Reuse the key on renewal and the TLSA record outlives every certificate you put behind it

Step two is the one people skip. The result is deferred mail until the old record ages out of every cache, and it comes from exactly the senders who are doing security properly.

Nobody Relays Without A Login

Relaying is sending mail onwards to a domain you are not the destination for. An open relay is how spam gets laundered through somebody else’s reputation. The old way of closing one was to trust an address range, so anything from the office network could relay. That is trust by location. On a network with guest WiFi, VPN users and the odd compromised laptop, location is worth very little.

So no address is trusted except the machine itself:

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

Port 25 is for mail arriving for this domain and nothing else. It never relays, for anybody:

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

Users send through submission: port 587 with STARTTLS, or 465 with TLS from the first byte. Both want a login before they will do anything. Dovecot checks the passwords, with every mail protocol switched off so it does nothing else, and Postfix asks it through a socket. The 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 means the server does not even offer AUTH until TLS is up, so a password never crosses the wire in the clear. AUTH lines in the reply to a plaintext EHLO: zero. A client that connects to 587 and tries to send without logging in gets nowhere:

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

A Login Owns Its Addresses

A login proves who you are. It says nothing about which addresses you may send as. Without that link, any user on the server can send as the finance director, and the mail will be signed and delivered as if it were real. Postfix ties the two together with a map of address to owner:

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

reject_sender_login_mismatch on the submission ports enforces it against the envelope sender. Logged in as alice, trying to send as bob:

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

That covers the envelope. It does not cover the From: header, which is the address the recipient actually sees, and postconf(5) says so in its notes on this very restriction:

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

Rspamd is already the milter here, so the header check is a short Lua rule that compares the logged-in user with the From: address, plus a forced reject on the symbol it raises:

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 sending with MAIL FROM:<alice@alpha.test> and From: bob@alpha.test:

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

A display name is fine and so is a change of case. Alice <ALICE@alpha.test> goes through, because the comparison is on the address and ignores case.

The check that switches itself off

Here is the first of the settings that silently do nothing. The obvious way to stop outsiders forging your own domain on port 25 is to put the login-mismatch checks there too. Postfix accepts the configuration, starts, and logs this:

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

Port 25 has SASL switched off, quite rightly, because nobody logs in there. So the check is skipped. The mail is accepted. The only evidence is a warning in a log nobody reads, and with the check gone, anybody on the internet could send MAIL FROM:<alice@alpha.test> to this server and have it delivered to alice’s colleagues as if she had written it.

The fix is to refuse our own domains by name on port 25, because mail from us never legitimately arrives there:

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

with own_domains holding one line, alpha.test REJECT Our own domain: send through submission with a login. Tested from outside:

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

Sender Domains That Do Not Exist

If the domain in MAIL FROM does not exist, nothing can ever be sent back to it, and nothing legitimate ever came from it. reject_unknown_sender_domain refuses mail when that domain has “no DNS MX and no DNS A record”.9 Postfix’s default reply code for it is 450, a temporary failure, which invites the forger to try again later. I changed it to 550.

postconf(5) is blunt about that setting: “Do not change this unless you have a complete understanding of RFC 5321.”10 Fair enough. Here is the reasoning. A 4xx tells the sender to queue and retry, which is right when the answer might change. A domain with no records now is not going to grow some in the next five minutes. Postfix separates that from the case where the answer is not known yet: a DNS failure is handled by unknown_address_tempfail_action, which still defers. Changing the code changes only the case that has an answer.

The lab tested all three, plus the one the IPv6 rule cares about. The postconf wording says “A record”, and a domain whose only address is an AAAA record should not be treated as missing:

Sender domainDNS saysReply
nosuch-lab.co.ukNXDOMAIN550 5.1.8 <someone@nosuch-lab.co.uk>: Sender address rejected: Domain not found
v6only-lab.co.ukAAAA only, no A, no MXaccepted
dnsfail-lab.co.ukSERVFAIL450 4.1.8 … Sender address rejected: Domain not found, retried later
sender.testMX, A, AAAAaccepted

In practice “A record” covers AAAA, and an IPv6-only domain is safe. The refusal comes at MAIL FROM, before the sender has sent a single byte of the body. Postfix holds a MAIL FROM rejection until the first RCPT TO by default, so the reply arrives one step later than you might expect, which is why the stage diagram shows it there.

SPF, DKIM And DMARC, With Teeth

These three are covered properly in the lead post. In one line each: SPF says which servers may send for a domain, DKIM signs the message so changes show, and DMARC says what to do when they fail and whether they match the visible From: domain. The house rule is to reject on an SPF hard fail and on a DKIM signature that is present and does not verify. Unsigned mail is not a DKIM failure. DMARC p=reject on the sender’s domain is what handles that.

Rspamd does all three in one milter, plus DKIM signing for mail going out. Postfix hands it every message, and if Rspamd is not answering, mail waits rather than going through unchecked:

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

Rspamd works on scores, and that is the first thing to understand about it. Out of the box, nothing is rejected because SPF failed. Nothing. Each result adds or subtracts a few points, and the message is rejected when the total passes 15. The force_actions module is what turns a single result into a decision:

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";
  }
}

Five messages from sender.test, sent from the client in the lab. The SPF record allows ip6:fd89:50::50 and nothing else, so the same client sending over IPv4 is an unauthorised server for that domain. That is also exactly what a forwarded message looks like:

CaseSent asSPFDKIMDMARCRspamd scoreReply
Asigned, from the listed IPv6 addresspasspasspass3.59 of 15accepted
Bsigned, then the bank details changedpassfailpass2.30 of 15554 5.7.1 DKIM signature does not verify
Cunsigned, from IPv4failnonereject5.00 of 15554 5.7.1 SPF hard fail
Dsigned and intact, from IPv4failpasspass2.30 of 15554 5.7.1 SPF hard fail
Eunsigned, from the listed IPv6 addresspassnonepass3.80 of 15accepted

Look at case B. It is an invoice with the sort code and account number changed after it was signed. Invoice fraud, in other words. SPF passed, because it came from the right server. DMARC passed, because SPF passed and lined up with the From: domain. On its scores Rspamd gave it 2.30 out of 15 and would have delivered it. The only thing that stopped it was the forced reject on the broken signature. DMARC on its own lets a tampered message through as long as one of the two checks survives, and that is worth knowing before you call a domain protected because its DMARC says reject.

Case D is the cost, and it should be said plainly. A correctly signed, untouched message is refused because it came from a server not in the SPF record. That is what forwarding does: the forwarder sends your message on from its own address. Rejecting on SPF alone means forwarded mail from domains with -all gets bounced. SRS on the forwarder fixes it, and so would trusting DKIM over SPF, but the house rule is the house rule, and the cost goes in writing.

The default that trusts every private network

The second setting that silently does nothing. Rspamd skips SPF and some other checks for addresses it considers local, and the shipped list in options.inc is this:

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];

Every private range, both families. If your MX sits behind a load balancer, a NAT or a proxy on a private address, SPF is quietly skipped for all of it. In the lab, which is all private addressing, SPF never failed until this was changed.

The obvious fix does not work. Rspamd has two directories for local settings, and its FAQ spells out the difference: local.d “Merges with defaults. Collections and lists are combined”, while override.d “Replaces defaults.”11 A narrower list in local.d/options.inc is combined with the shipped one, which means it narrows nothing. It has to go in override.d/options.inc:

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

and rspamadm configdump options is how you check what Rspamd actually ended up with, rather than what you think you wrote.

Greylisting is also switched off in this build. It sends a temporary rejection to unknown senders and accepts them on the retry, which does stop some spam, but Rspamd’s greylisting answers before SPF and DMARC have had their say, so while testing you see a deferral where you expected a verdict.12 The tests above are only meaningful with it off.

Your own bounces, refused by your own DMARC

The last trap turned up when Stalwart in the lab refused a bounce from this server. Postfix sends bounces with an empty envelope sender and From: MAILER-DAEMON@mail.alpha.test. With no envelope sender, the only SPF check left is on the name the server gave in HELO, mail.alpha.test, which had no SPF record. The bounce was not signed either. So DMARC fell back to the policy for alpha.test, found p=reject, and the receiving server did exactly what the domain told it to:

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

The domain’s own policy was throwing away its own delivery failures. Every MX host name gets its own SPF record, which fixes it:

mail IN TXT "v=spf1 a -all"

Six Settings That Look Done

The traps above share a shape. Every one of them is a setting that looks done, produces no error, and does nowt.

Looks doneWhat actually happensFound by
smtp_tls_security_level = daneclear text wherever no TLSA is publishedreading postconf(5)
login-mismatch checks on port 25skipped, one warning in the logsending a forgery
local_addrs narrowed in local.dmerged with the default, nothing narrowedrspamadm configdump
DMARC p=reject on the sendertampered mail passes if SPF passescase B
certbot renewalsnew key, TLSA stops matchingreading certbot’s docs
DMARC p=reject on your own domainyour own bounces refuseda refused bounce

Two of them are in the documentation, if you read the right paragraph of the right page. The other four turned up only by sending the message the rule was supposed to stop and looking at what came back. Reading the configuration back and agreeing with it found none of them. If you take one habit from this post, take that one, because every line here can be copied into a server and still not be working.

The Defaults Are Somebody Else’s Decision

Every refusal in this post is a setting Postfix has had for years. Even DANE, the newest thing here, has been in it since 2.11.13 None of it is new, none of it is expensive, and the server underneath is the same free software it always was. The gap between a mail server that lets an altered invoice through and one that refuses it at DATA is a page of configuration and an afternoon of testing.

So why does so little of the internet run like this? Because the defaults decide, and the defaults were set to make sure mail got through on an internet where TLS was the exception. They were a reasonable call then. Leave them alone now and you have handed your security decisions to a choice somebody made in 2002, for a network that no longer exists, about mail that was not yours.

Running a mail server is a decision to be responsible for what passes through it. Those settings are the decision written down. The defaults are what you get when nobody made it.


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

  2. postconf(5): smtp_tls_security_level — the dane level: “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 licensed MTA-STS and DANE policy server for Postfix: “Verify TLSA records for correctness and supported parameters, only then the dane-only policy (Mandatory DANE) will be returned.” Version 1.14.4 used here. ↩︎

  5. Postfix source, src/dns/dns_lookup.c — “Automatically turns on the RES_TRUSTAD flag on systems that support this flag”, and the code that does it when DNSSEC validation is requested. ↩︎

  6. RFC 7672 §3.1.1 — SMTP security via opportunistic DANE TLS. TLSA records for SMTP servers “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)”, and under --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 — rejects when the MAIL FROM domain has “no DNS MX and no DNS A record”, or a malformed MX. ↩︎

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

  11. Rspamd FAQ: the difference between local.d and 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”. ↩︎