Skip to content

Encryption and Certificates

chrisholloway5 edited this page Sep 8, 2026 · 3 revisions

Encryption and Certificates

This page began as a chapter of the 6.2.10 manual and has been corrected against the code of 6.2.28. The Control Panel's pages are grouped differently now, so the paths below use today's groups; the TLS ports 465, 993 and 995 exist only after you create them on the TCP/IP ports page (a fresh install seeds 25, 587, 110 and 143); and everything added since 6.2.10 is in Changes-Since-6210. Where a value here disagrees with the Settings Reference, which is generated from the code, the reference is right.

Mail must be encrypted in transit. You need a TLS certificate for mail.yourcompany.com, you need at least one listener configured to use it, and — for mail you send — you need the three outbound policies in §9.6, all of which are already on.

A fresh install offers no TLS at all. The four seeded listeners (25, 587, 110, 143) are created with connection security None and no certificate (DBScripts/CreateTables*.sql, the hm_tcpipports seed rows), and STARTTLS is advertised only on a port whose security is STARTTLS-optional or STARTTLS-required (SMTPConnection.cpp, the 250-STARTTLS line). Getting a certificate is step one; §9.5 is step two, and without it nothing changes.

Start with the summary if the server is already running. TLS & certificates → Transport encryption overview is a read-only page that answers "is anything on this server carrying a password in the clear, and will it still serve a valid certificate next month". It exists because the interesting states only appear in the combination of four editors: a port set to use TLS with no certificate assigned does not fall back to plaintext, it fails to start; the AEAD-ONLY cipher preset leaves TLS 1.0 and 1.1 advertised with no suite they can use; a certificate that expired last week is listed exactly like one that has not (Tools/ControlPanel/Views/TlsOverviewView.cs).


9.1 The easy way: automatic Let's Encrypt certificates

hMailServer has an ACME v2 client built in (RFC 8555, http-01 challenge). It obtains a free certificate, installs it, assigns it to your TLS ports, renews it before expiry and reloads it — with no restart and no scheduled task.

Open TLS & certificates → Certificates (ACME):

The Automatic certificates page, showing the ACME enable switch, contact address, the comma-separated host name list, directory URL, challenge port, output folder and the key-reuse switch

Or in hMailServer.INI:

[Settings]
AcmeEnabled=1
AcmeContactEmail=you@yourcompany.com
AcmeDomains=mail.yourcompany.com,mta-sts.yourcompany.com,autoconfig.yourcompany.com
AcmeHttpPort=80
AcmeReuseKey=1
Setting Default Notes
AcmeEnabled 0 Off. With it off, AcmeRenewalTask::DoWork returns immediately and nothing on this page can fire
AcmeDirectoryUrl https://acme-v02.api.letsencrypt.org/directory Any RFC 8555 CA. Point it at the staging directory while you are testing
AcmeContactEmail empty Where the CA sends expiry notices
AcmeDomains empty Comma separated. Every name becomes a SAN on one certificate
AcmeHttpPort 80 Where the http-01 challenge is answered
AcmeCertificateDirectory empty Falls back to <data directory>\ACME
AcmeReuseKey 1 Keep the same private key across renewals

Requirements: port 80 must be reachable from the internet — that is how the CA verifies you own the name — and each name in AcmeDomains must already resolve to this server.

AcmeReuseKey=1 matters if you use DANE. Keeping the same private key across renewals keeps your published 3 1 1 TLSA records valid. Leave it on. When it is on and a privkey.pem already exists, the log says ACME: Reusing the existing certificate key (keeps published TLSA records valid). Otherwise a fresh 2048-bit RSA key is generated.

What the client actually does

sequenceDiagram
    participant S as hMailServer
    participant CA as ACME directory
    participant L as Challenge listener on port 80
    S->>CA: GET /directory
    CA-->>S: newNonce, newAccount, newOrder, renewalInfo
    S->>S: Load or create the ACME account key
    S->>CA: POST newAccount, JWS signed with the account key
    CA-->>S: 200 or 201 with the account URL
    S->>CA: POST newOrder with every configured name
    CA-->>S: 201, order URL, finalize URL, authorization URLs
    loop one per name
        S->>CA: POST-as-GET the authorization
        CA-->>S: challenges, including http-01 with a token
        S->>L: publish token and key authorization
        S->>CA: POST the challenge, empty JSON object
        loop up to 30 times, 2 seconds apart
            S->>CA: POST-as-GET the authorization
            CA-->>S: pending, valid or invalid
        end
    end
    S->>CA: POST finalize with a CSR carrying every name as a SAN
    loop up to 30 times, 2 seconds apart
        S->>CA: POST-as-GET the order
        CA-->>S: processing, valid or invalid
    end
    S->>CA: POST-as-GET the certificate URL
    CA-->>S: the PEM chain
    S->>S: Write privkey.pem.new and fullchain.pem.new,<br/>then rename key first, certificate second
    S->>S: Create or update the certificate record,<br/>assign it to TLS ports that have none,<br/>reinitialise the servers
Loading

The challenge is answered by whichever listener owns port 80. If the built-in web services server is already listening there, it serves /.well-known/acme-challenge/<token> from a process-wide store and no second listener is started; otherwise a transient AcmeChallengeServer binds the port for the duration of the validation and closes it again.

When it renews

The renewal task runs once an hour, plus once at startup (Application.cpp, where AcmeRenewalTask is scheduled). Each run asks one question: should this certificate be renewed now?

flowchart TD
    A["Hourly renewal task"] --> B{"AcmeEnabled?"}
    B -- no --> STOP["Nothing happens"]
    B -- yes --> C{"Can fullchain.pem<br/>be read for its dates?"}
    C -- no --> REQ["Request a certificate"]
    C -- yes --> D{"Does the CA publish<br/>renewalInfo, ARI?"}
    D -- yes --> E["Ask the CA for a suggested window,<br/>pick a stable point inside it"]
    E --> F["Clamp to at most<br/>24 hours before expiry"]
    F --> G{"Is that moment past?"}
    D -- no --> H["renewAt = notBefore + two thirds<br/>of the certificate lifetime"]
    H --> I["Clamp to at most<br/>24 hours before expiry"]
    I --> G
    G -- no --> J["Deploy a certificate that was issued<br/>but never recorded, if there is one"]
    G -- yes --> REQ
    REQ --> K{"Issuance succeeded?"}
    K -- yes --> DONE["Deployed, servers reinitialised"]
    K -- no --> L{"How long until<br/>the current certificate expires?"}
    L -- "more than 7 days" --> LOG["One application-log line"]
    L -- "7 days or fewer" --> ERR["HM5992 reported as an error"]
    L -- "already expired" --> ERR
Loading

The renewal arithmetic is a fraction of the lifetime, not a fixed number of days, and that is the point. Thirty days was right while certificates lasted ninety; Let's Encrypt defaults to 64 days from February 2027, the maximum falls to 100 days in March 2027 and to 47 in March 2029 — and a fixed 30-day window against a 47-day certificate starts renewing seventeen days after issuance and never stops, which is how an operator gets rate-limited by their own CA.

So, from AcmeClient::GetRenewalTime:

lifetime  = notAfter - notBefore          (90 days assumed if notBefore is unreadable)
renewAt   = notBefore + (lifetime / 3) * 2
renewAt   = min(renewAt, notAfter - 24 hours)     floor, so a failure gets another attempt
renewAt   = max(renewAt, notBefore)               a certificate shorter than a day renews at once

For a 90-day certificate that is 60 days in, 30 days of margin. For a 47-day one it is 31 days in, 16 days of margin. Whatever the lifetime, a third of it remains to notice a failure.

When the CA implements ARI (RFC 9773, a renewalInfo endpoint in its directory) its opinion wins, with two guards: the suggested moment is never later than 24 hours before expiry, and the point chosen within the CA's window is derived from the certificate's own ARI identifier rather than from a random number — so this server picks the same moment every hour for this certificate, and a different one from the server next door, which is what §4.2 of the RFC is actually asking for.

What ends up on disk

<data directory>\ACME\
├── account.key      the ACME account key; created on first use, never rotated
├── privkey.pem      the certificate private key; reused across renewals by default
├── fullchain.pem    the leaf certificate followed by the issuer chain
├── privkey.pem.new      transient, during a renewal only
└── fullchain.pem.new    transient, during a renewal only

The two .new files exist because the order of the writes matters. The key is written, then the certificate, then the key is renamed into place first and the certificate second. A half-finished renewal therefore never leaves a new key beside an old certificate — a mismatched pair is not a degraded listener, it is a listener that refuses to start, which would be a worse outcome than the expiry it was avoiding. If the second rename fails anyway, that is error HM5993 and the message names both files and says the fix is manual.

What you see in the log

A successful run:

ACME: Certificate is missing or due for renewal. Requesting a new certificate.
ACME: Reusing the existing certificate key (keeps published TLSA records valid).
ACME: Certificate issued successfully: C:\Program Files\hMailServer\Data\ACME\fullchain.pem
ACME: Assigned the certificate to port 587 (no certificate was configured).
ACME: Restarting servers to load the new certificate.
ACME: DANE TLSA record for this certificate: _25._tcp.<mx-host>. IN TLSA 3 1 1 3b1f…

A failed one always names the failing step, then says so plainly:

ACME: Authorization for mail.example.com offers no http-01 challenge. Response: {…}
ACME: Certificate renewal FAILED. The failing step is in the lines above; the next attempt
is in one hour. The certificate currently installed has not been changed.
Symptom in the log Almost always means
Failed to start the http-01 challenge listener. Is the port in use? Something else owns port 80 — IIS, another web server, or a WebServicesHttpPort set to a different port
Challenge validation failed for <name> The name does not resolve to this server, or port 80 is not reachable from the internet
Could not read the authorization (HTTP status 0) Outbound HTTPS to the CA is blocked
No domains configured AcmeDomains is empty
Timed out waiting for challenge validation The CA reached something, but not this server — check for a proxy or a stale DNS record

9.2 The manual way

If you have a certificate from another CA, go to TLS & certificates → SSL certificates, add it, then assign it to your TLS ports under Connections & protocols → TCP/IP ports.

The SSL certificates page, listing each certificate by name with the paths to its certificate and private key files

  • Both files are PEM. The certificate file must contain the leaf and the issuer chain — hMailServer loads it twice, once as the certificate and once as the chain, and reports each failure separately.
  • An encrypted private key is supported: give the certificate its passphrase in the same dialog. Different certificates may have different passphrases, which is why there is no single global one.
  • A certificate is not checked for validity dates when it is served, only when it is verified — so an expired certificate loads without complaint and every client then refuses to connect, with no symptom on the server side. hMailServer therefore checks the dates itself at startup and reports HM5991 for a certificate that has expired or one that is not valid yet (which usually means this server's clock is wrong). It reports and carries on rather than refusing to listen, because a mail server that stops answering the moment a certificate expires is worse than one that answers with a stale certificate.
Code Message Meaning
HM5113 Failed to load certificate file / …certificate chain… / …private key file The path, the format or the passphrase is wrong. The port does not start
HM5113 Error initializing SSL. Certificate not set. A TLS port has no certificate assigned
HM5991 The TLS certificate … expired on … The listener started and is offering an expired certificate
HM5991 The TLS certificate … is not valid until … Future-dated certificate, or the server clock is behind
HM5603 Unable to enable Diffie-Hellman key agreement dh2048.pem is missing from the Bin directory

9.3 TLS versions, ciphers and key exchange

TLS & certificates → SSL/TLS controls what is negotiable, for this server's own listeners and for the connections it makes when delivering.

The SSL/TLS page on its Protocol versions tab, captured on a 6.2.10-era build: the heading "SSL / TLS", the subtitle "Protocol versions, cipher configuration and brute-force protection", three tabs - Protocol versions, Ciphers, Auto-ban - and four ticked checkboxes for TLS 1.0 (legacy), TLS 1.1 (legacy), TLS 1.2 and TLS 1.3

Two things about this 6.2.10-era capture. Its Auto-ban tab is now a page of its own under Access & abuse protection, so today this page carries Protocol versions, Ciphers and the Session resumption tab of the next section. And all four protocol versions are ticked in the shot because that build was configured that way; the shipped default is SslVersions 24 — TLS 1.2 and 1.3 only, as the table below says.

Setting Storage Default What it does
TLS 1.0 / 1.1 / 1.2 / 1.3 SslVersions bitmask 24 = 8 + 16, i.e. TLS 1.2 and TLS 1.3 only SSLv2 and SSLv3 cannot be enabled at all
Cipher list for TLS 1.2 and below SslCipherList a long OpenSSL list ending …:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK; Applied with SSL_CTX_set_cipher_list. Does not touch TLS 1.3
TLS 1.3 cipher suites TlsCipherSuites13 (INI) empty = OpenSSL's defaults RFC 8446 names, colon separated, e.g. TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
Key exchange groups TlsKeyExchangeGroups (INI) X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:secp256r1 Two post-quantum hybrids first, then the classical curves
Prefer server cipher order TlsOptions bit 2 off SSL_OP_CIPHER_SERVER_PREFERENCE
Prioritize ChaCha20-Poly1305 TlsOptions bit 4 off Only effective with server preference on and TLS 1.2 or 1.3 enabled
Verify remote certificates when delivering VerifyRemoteSslCertificate on Applies to outbound SMTP

The defaults — TLS 1.2 and 1.3 only — are correct. Do not re-enable TLS 1.0 or 1.1 unless a specific ancient client forces you to, and then plan to replace that client.

The post-quantum default is real and it applies in both directions. The shipped group list offers X25519MLKEM768 and SecP256r1MLKEM768 — ML-KEM hybrids — ahead of X25519 and the NIST curves, so a modern peer negotiates a hybrid and an older one falls back with no loss. The same list is installed on the client context, so mail this server sends uses it too.

Six failure modes here are handled by falling back loudly rather than quietly, and each has its own error number:

Code Condition What the server does instead
HM5990 No TLS version is enabled at all Falls back to TLS 1.2 + 1.3, because "TLS but no version of it" cannot be meant — turning TLS off is done per port
HM5720 TlsKeyExchangeGroups is rejected by OpenSSL, or would leave no group enabled Falls back to secp384r1:x25519:secp256r1
HM5721 Even that fallback is rejected Reports loudly; OpenSSL's own defaults are in force
HM6120 TlsCipherSuites13 is rejected outright The list is not applied; OpenSSL's default TLS 1.3 suites stand
HM6121 One or more names in TlsCipherSuites13 are unknown to this build The rest of the list is applied — a single typo would otherwise silently halve the suites offered
HM5511 SslCipherList is rejected Reported; the previous list stands

AEAD-ONLY is a named preset, not an OpenSSL string: set SslCipherList to exactly that word and the server applies a vetted forward-secret AEAD-only list (ECDHE/DHE with AES-GCM or ChaCha20-Poly1305). It excludes every CBC-mode suite — the Lucky13 padding-oracle family — and static-RSA key exchange. The cost is stated plainly: TLS 1.2 clients that only speak CBC cannot connect, and TLS 1.0/1.1 are left with no usable cipher at all, so do not enable those protocols alongside it. A misspelled preset name is not silently ignored; it flows through to OpenSSL, which rejects a list containing no known cipher, and HM5511 names the string.

Session resumption

Four settings, all on the SSL/TLS page's Session resumption tab. At their defaults the server makes no OpenSSL call at all for three of them, so resumption behaves exactly as stock OpenSSL does.

Setting Default What a non-default value buys
TlsSessionTicketsEnabled on Off: no tickets on any TLS version. TLS 1.2 clients fall back to the in-process cache, TLS 1.3 clients get no ticket at all
TlsSessionCacheSize 0 = OpenSSL's cap of 20480 A positive value replaces the cap; -1 turns the server-side cache off entirely, leaving tickets unaffected
TlsSessionTimeoutSeconds 0 = OpenSSL's 300 Bounds how long a leaked resumption secret stays useful
TlsTicketKeyRotationSeconds 0 = one non-rotating key for the life of the process With rotation on, a captured ticket is decryptable for at most two intervals. 86400 is a day

Ticket-key rotation is the one worth turning on if you care about forward secrecy: without it, every ticket a listener ever issues is sealed under one key generated at startup, and that key — recovered later from a memory dump or a hibernation file — decrypts every ticket ever recorded. Rotation is process-global, so all listeners rotate together; cross-listener resumption is still refused, because each listener has its own session-ID context.


9.4 Client certificates (mutual TLS)

Per port, not per server, under Connections & protocols → TCP/IP ports:

Policy Value Behaviour
Off 0 Never ask for a client certificate. The default, and what every port did before this existed
Request 1 Ask, verify one if offered, log the outcome, never fail the handshake. Use it to inventory which clients could survive "require"
Require 2 The handshake fails unless the client presents a certificate chaining to the port's configured CA bundle

A verified client certificate that names an address also makes AUTH EXTERNAL (RFC 4422 Appendix A) available on that connection — SMTP, IMAP and POP3 all advertise it exactly when such a certificate exists, and to nobody else.

This is per port on purpose: the population connecting to a partner-relay SMTP port and the population connecting to the public IMAP port are different populations with different certificate authorities, and a single global switch set to "require" would lock every ordinary mail client out of the server.


9.5 Turning TLS on for your listeners

Nothing above changes a single connection until a listener is configured to use it.

The TCP/IP ports page, listing protocol, address, port, connection security and assigned certificate for each listener

Connection security Value What it means Typical ports
None 0 Plaintext only. STARTTLS is not advertised
SSL/TLS 1 Implicit TLS from the first byte 465, 993, 995
STARTTLS (optional) 2 STARTTLS is advertised; a client may or may not use it 25
STARTTLS (required) 3 STARTTLS is advertised and the session is refused until it is used 587, 143, 110

A working modern configuration:

Port Protocol Security Why
25 SMTP STARTTLS optional Other servers must be able to reach you; many still cannot do TLS
587 SMTP STARTTLS required Submission from your own users — never in the clear
465 SMTP SSL/TLS Implicit-TLS submission (RFC 8314) for clients that prefer it
143 IMAP STARTTLS required
993 IMAP SSL/TLS
110 POP3 STARTTLS required
995 POP3 SSL/TLS

Assign the certificate to every one of them. ACME does this for you for any TLS-enabled port that has no certificate configured; a port that already names one is left alone.

A TLS port with no certificate does not fall back to plaintext — it fails to start. That is HM5113, and the only other symptom is a service that came up with one listener missing. See Ports Reference for the full list of what listens where.

Two related controls live elsewhere and belong in the same decision:

  • Require TLS before AUTH is an IP range option, not a port option (Security Hardening §17.1). On a range with it set, AUTH is not advertised on a cleartext connection at all — all four protocols were fixed to stop advertising mechanisms they would refuse, because a client that sends AUTH PLAIN and then gets 530 has already put the password on the wire.
  • DisableAUTHList names local TCP ports where AUTH should not be offered at all, for example a port 25 that only accepts inbound mail from other servers.

9.6 Outbound transport security: MTA-STS, DANE and DNSSEC

These protect mail you send, and all three are on by default. Configure them under TLS & certificates → Transport security.

[Settings]
MtaStsEnabled=1            ; honour recipients' MTA-STS policies
DaneEnforcementEnabled=1   ; honour recipients' DANE/TLSA records
DnssecValidationEnabled=1  ; validate DNSSEC for DANE
DnssecTrustAnchors=        ; empty = the built-in IANA root anchors

The Transport security page, showing the DANE and DNSSEC card, MTA-STS, ARC sealing, DKIM timestamp settings, authentication-results headers and the reporting cards

What MTA-STS actually does

flowchart TD
    A["A message is due for a domain"] --> B{"MtaStsEnabled and this is<br/>an MX delivery, not a fixed relay?"}
    B -- no --> PLAIN["Deliver as usual"]
    B -- yes --> C["Look up the TXT record at<br/>underscore mta-sts, dot, the domain"]
    C -- "no record" --> NEG["Cache no-policy for 30 minutes"]
    NEG --> PLAIN
    C -- "record found" --> D["HTTPS GET the policy from<br/>mta-sts dot the domain,<br/>path /.well-known/mta-sts.txt"]
    D --> E{"mode?"}
    E -- none --> PLAIN
    E -- testing --> LOGONLY["Debug log only, not enforced"]
    LOGONLY --> PLAIN
    E -- enforce --> F["Drop every MX host the<br/>policy does not name"]
    F --> G{"Any host left?"}
    G -- no --> DEFER["Defer the message, 4.7.0"]
    G -- yes --> H["Require TLS with certificate<br/>verification to those hosts"]
Loading

A policy is cached until its own max_age, revalidated by re-reading the DNS record's policy id at most once an hour, and a missing policy is negatively cached for 30 minutes. DNS failure or a failed fetch during revalidation keeps the cached policy — RFC 8461 §5.1. The mx: patterns match either exactly or with a leading *. that matches exactly one label.

What DANE actually does

flowchart TD
    A["A message is due for a domain"] --> B{"DaneEnforcementEnabled and<br/>this is an MX delivery?"}
    B -- no --> PLAIN["Deliver as usual"]
    B -- yes --> C["DNSSEC-validated MX lookup<br/>for the recipient domain"]
    C --> D{"Chain status of the MX RRset"}
    D -- insecure --> PLAIN
    D -- bogus --> SKIP["Skip this host entirely"]
    D -- secure --> E["Per host: look up the TLSA record at<br/>underscore 25, underscore tcp, the MX host"]
    E --> F{"TLSA lookup status"}
    F -- bogus --> SKIP
    F -- "no records" --> PLAIN
    F -- validated --> G["Require TLS and pin the<br/>certificate to the TLSA records"]
    SKIP --> H{"Any host attempted?"}
    H -- no --> DEFER["Defer the message, 4.7.0"]
Loading

Three things are worth stating exactly:

  • The MX RRset must be DNSSEC-validated too, not just the TLSA record (RFC 7672 §2.2). A TLSA record found under a host name learned from a forged MX answer proves nothing — the attacker chose the host, so its own TLSA record validates perfectly. hMailServer therefore validates the MX RRset and checks that the host it is about to contact is one of the names that RRset published.
  • Only DANE-EE, usage 3, is honoured. DaneVerifier skips any record whose usage is not 3, ignores issuer certificates entirely (depth > 0 always passes), and matches the leaf against selector 0 (whole certificate) or 1 (SubjectPublicKeyInfo) with matching type 0 (exact), 1 (SHA-256) or 2 (SHA-512).
  • The whole thing fails open except for one value. Bogus — a published DNSSEC chain that does not validate — is the only status that can stop a delivery. Every other outcome, including anything a caller produces by accident, lands on the ordinary delivery path. That is designed in: the enum's permissive value is zero, and the value that stops mail is the highest.

Setting DnssecValidationEnabled=0 reverts to opportunistic, unvalidated TLSA use.

With DANE enforcement, a recipient whose DNSSEC chain is bogus will not receive your mail rather than receive it unencrypted — which is the point.

What you see in the log

SMTPDeliverer - Message 4711: MTA-STS policy mode 'enforce' active for example.com. TLS with certificate validation is required.
SMTPDeliverer - Message 4711: Skipping MX host mx9.example.com - not permitted by the MTA-STS policy of example.com.
SMTPDeliverer - Message 4711: Delivery to example.com deferred. No MX host matches the domain's MTA-STS policy.
SMTPDeliverer - Message 4712: DNSSEC-validated DANE-EE TLSA records found for mx1.example.com. TLS with TLSA certificate matching is required.
SMTPDeliverer - Message 4712: TLSA records for mx1.example.com failed DNSSEC validation. Skipping this host (RFC 7672).
SMTPDeliverer - Message 4712: Delivery to example.com deferred. TLSA records of all MX hosts failed DNSSEC validation.

And in the TCP/IP log, per handshake:

DANE: Session 812: Certificate matched DANE-EE TLSA record (selector 1, matching type 1).
DANE: Session 813: Certificate did not match any DANE-EE TLSA record. Failing TLS handshake.

Both deferral paths use the enhanced status code 4.7.0 — "other or undefined security status" (RFC 3463). Neither RFC registers a code of its own, and borrowing one that means something else would be worse than saying "refused on security grounds".


9.7 Publishing policies for your own domains

The outbound half above is about other people's policies. Publishing your own is two DNS records and one switch.

MTA-STS. MtaStsHostingEnabled defaults to 1, and the built-in web services server serves /.well-known/mta-sts.txt for any hosted, active domain when the request's Host is mta-sts.<domain>. It generates the body from your live MX records unless MtaStsPolicyMx overrides them:

version: STSv1
mode: enforce
mx: mail.yourcompany.com
max_age: 604800
Setting Default Notes
MtaStsHostingEnabled 1 On, but inert until the HTTPS listener exists
MtaStsPolicyMode enforce enforce, testing or none; anything else is read as enforce
MtaStsPolicyMaxAge 604800 (7 days) Clamped to 86400 … 31557600
MtaStsPolicyMx empty Comma-separated override; empty derives the list from live MX records
WebServicesHttpsPort 0 Must be set. RFC 8461 §3.3 requires the policy to be fetched over HTTPS

If WebServicesHttpsPort is 0 the server logs, in as many words, that the policy cannot be served and sending servers will treat the domain as having no policy. Add mta-sts.yourcompany.com to AcmeDomains so the HTTPS listener has a certificate for the name it will be asked for.

Then publish _mta-sts.yourcompany.com. IN TXT "v=STSv1; id=20260908000000Z" and bump the id whenever you change the policy. See DNS for Your Domain.

DANE. Every successful ACME issuance logs the exact record to publish:

ACME: DANE TLSA record for this certificate: _25._tcp.<mx-host>. IN TLSA 3 1 1 3b1f…

That is 3 1 1 — DANE-EE, SubjectPublicKeyInfo, SHA-256 — which is why AcmeReuseKey matters: the digest is over the key, so reusing it keeps the record valid across renewals. Your zone must be DNSSEC-signed for the record to mean anything.


9.8 Recording what this server concluded

Off by default, and worth turning on the day you start diagnosing anything:

Setting Default What it writes
AuthenticationResultsEnabled off One Authentication-Results header per accepted inbound message (RFC 8601), carrying the SPF, DKIM and DMARC verdicts this server reached. Only checks that actually ran are reported
ReceivedSpfHeaderEnabled off A Received-SPF header (RFC 7208 §9.1), written only when the SPF check ran
AuthenticationResultsIdentity empty = this computer's name The authserv-id: the first token of every header this server writes, always lower-cased

The identity is load-bearing in both directions. An arriving message carrying an Authentication-Results header that claims this identity has it removed before the server writes its own, so a sender cannot have a verdict believed downstream in this server's name (RFC 8601 §5). A header naming any other identity is left completely untouched.


9.9 Reporting

Two aggregate reporters, both scheduled hourly and both inert until a sender address is set. They flush their statistics either way, so switching one on does not start with an empty history.

TLS-RPT (RFC 8460) DMARC aggregate (RFC 7489)
Address setting TlsRptFromAddress DmarcRptFromAddress
Organisation name TlsRptOrganizationName, default hMailServer DmarcRptOrganizationName, default hMailServer
Recorded by ExternalDelivery, per delivery attempt, classified tlsa, sts or no-policy-found SpamTestDMARC, for every message evaluated against a published policy — passes included
Schema DmarcRptSchemaVersion, default 1 = RFC 7489 Appendix C; 2 = RFC 9990
[Settings]
TlsRptFromAddress=tlsrpt@yourcompany.com
DmarcRptFromAddress=dmarc@yourcompany.com

DMARC reports are only sent to an address outside the policy domain when that address's own DNS says it wants them, so a domain cannot use its DMARC record to aim this server's reports at somebody else.


9.10 A checklist

  1. AcmeEnabled=1, AcmeDomains naming mail., mta-sts. and autoconfig. — port 80 reachable.
  2. Create ports 465, 993 and 995; set 587, 143 and 110 to STARTTLS-required and 25 to STARTTLS-optional. Confirm every one has a certificate.
  3. Check Transport encryption overview — it will tell you if anything still carries a password in the clear.
  4. Leave MtaStsEnabled, DaneEnforcementEnabled and DnssecValidationEnabled at 1.
  5. Set WebServicesHttpsPort and publish _mta-sts in DNS.
  6. Publish the TLSA record the ACME log prints, if your zone is signed.
  7. Turn on AuthenticationResultsEnabled, and set TlsRptFromAddress and DmarcRptFromAddress.
  8. Diary the first renewal: it will be two-thirds of the way through the certificate's life, and the log line to look for is ACME: Certificate issued successfully.

See also: Security Hardening · Ports Reference · DNS for Your Domain · Stopping Spam · Settings Reference · Monitoring and Health · Capabilities and Configuration


Clone this wiki locally