Issue Description
I was trying to send mails to domains protected by MS Exchange. For some, it works, but for others, it does not.
After investigating the issue, I found a potential culprit in the Server behaviour: if the recipient’s mailserver has a valid DNSSEC configuration, Stalwart expects it to have a TLSA record.
Expected Behavior
If no TLSA record is found and the DANE strategy is set to ‘Optional’, the mail should be sent.
Actual Behavior
INFO Error fetching TLSA record (dane.tlsa-record-fetch-error)
queueName = "remote"
from = "[email protected]"
to = ["[email protected]"]
domain = "targetserver.tld"
hostname = "targetserver-tld.mail.protection.outlook.com"
causedBy = DNS error (mail-auth.dns-error)
details = "DNS error: error response: Server Failure"
strict = false
elapsed = 177ms
Reproduction Steps
- Use the default TLS strategies on the sender’s mailserver
- Configure DNS entries on the recipient’s server so that DNSSEC passes (https://powerdmarc.com/dnssec-checker/), but TLSA does not (https://powerdmarc.com/dane-record-checker/)
Relevant Log Output
When the recipient’s server does not have a valid DNSSEC configuration, everything works:
INFO New delivery attempt for domain (delivery.domain-delivery-start)
INFO TLS-RPT record not found (tls-rpt.record-not-found)
INFO MTA-STS policy not found (mta-sts.policy-not-found)
+ INFO TLSA record not DNSSEC signed (dane.tlsa-record-not-dnssec-signed)
INFO Connecting to remote server (delivery.connect)
INFO SMTP STARTTLS command (delivery.start-tls)
INFO Message delivered (delivery.delivered)
INFO DSN success notification (delivery.dsn-success)
INFO Delivery completed (delivery.completed)
INFO Delivery attempt ended (delivery.attempt-end)
If the server has DNSSEC but not TLSA:
INFO New delivery attempt for domain (delivery.domain-delivery-start)
INFO TLS-RPT record not found (tls-rpt.record-not-found)
INFO MTA-STS policy not found (mta-sts.policy-not-found)
- INFO Error fetching TLSA record (dane.tlsa-record-fetch-error)
INFO Message rescheduled for delivery (queue.rescheduled)
INFO Delivery attempt ended (delivery.attempt-end)
Stalwart Version
v0.16.x
Installation Method
Docker
Database Backend
FoundationDB
Blob Storage
S3-compatible
Search Engine
Elasticsearch
Directory Backend
OIDC
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on
Looking at the outbound delivery code, this matches a genuine gap in the current DANE handling. When Stalwart looks up a TLSA record for a DNSSEC-secure MX host, the resolver’s error is inspected:
- If the DNS error is a plain “record not found” (NXDOMAIN-style), Stalwart correctly treats it as “no TLSA record” and, when the strategy is
optional, falls through to regular STARTTLS.
- If the DNS error is anything else (including a
SERVFAIL, which is what several Microsoft 365/Exchange domains apparently return when there’s no TLSA record but the zone is DNSSEC-signed), Stalwart logs dane.tlsa-record-fetch-error and unconditionally aborts the delivery attempt to that host, regardless of whether the DANE strategy is require or optional. Unlike the other TLSA outcome branches, this error path does not check the strict flag at all before failing.
So the “Optional” DANE strategy is only honored for a missing-record (NXDOMAIN) response, not for a SERVFAIL response while resolving _25._tcp.<host> records, which is why delivery to these particular Exchange-protected domains gets rescheduled/fails even though DANE is set to Optional.
Relevant docs on the intended behavior of the optional/require DANE setting: TLS | Stalwart (DANE section) and DANE | Stalwart
This looks like a real inconsistency in how SERVFAIL-type TLSA lookup failures are handled versus NXDOMAIN ones, worth a maintainer’s review; a human will follow up on this thread.
This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.
The trigger is not the absent TLSA record. A real “no such record” answer is handled and delivery goes ahead under optional; what you are hitting is a TLSA lookup that comes back SERVFAIL, which is a different thing. Once the recipient’s MX RRset has validated under DNSSEC, an inconclusive TLSA answer cannot be told apart from a stripped record, so RFC 7672 section 2.2 requires us to defer rather than downgrade. That is why optional does not help: it governs the missing-record case, not the failed-lookup case.
It’s a good idea to establish where the SERVFAIL comes from, because it may well be yours rather than theirs. Try dig +dnssec _25._tcp.<mx-hostname>. TLSA against your configured resolver and against an independent one. We run a validating resolver on top of whatever forwarder you have configured, and forward-mode validation throws SERVFAIL on awkward delegations often enough to be worth ruling out first.
If the destination cannot be fixed, set dane to disable on a TLS strategy scoped to that domain.
Using the connectivity test from Microsoft itself, it seems that they standardized this non-compliant behavior:
From what I see, the mail is still being delivered, while a SERVFAIL is returned for TLSA records.
You can also try with their own test domain: contoso.com:
~$ dig contoso.com MX
; <<>> DiG 9.18.49 <<>> contoso.com MX
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31188
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;contoso.com. IN MX
;; ANSWER SECTION:
+ contoso.com. 3408 IN MX 10 contoso-com.mail.protection.outlook.com.
;; Query time: 0 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Mon Aug 24 10:53:31 CEST 2026
;; MSG SIZE rcvd: 92
~$ dig _25._tcp.contoso-com.mail.protection.outlook.com. TLSA
; <<>> DiG 9.18.49 <<>> _25._tcp.contoso-com.mail.protection.outlook.com. TLSA
;; global options: +cmd
;; Got answer:
-; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 55521
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;_25._tcp.contoso-com.mail.protection.outlook.com. IN TLSA
;; Query time: 573 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Mon Aug 24 10:53:41 CEST 2026
;; MSG SIZE rcvd: 77
I have also looked up the Microsoft Learn documentation about DANE, and it seems that this error is returned when the recipient’s server is wrongly configured.
Back to the square one :-/
EDIT: I found that the error is different based on which DNS server I use:
- With Cloudflare, I have a NXDOMAIN answer (DANE optional passes)
- With Quad9, I have a SERVFAIL
- With Google, it’s a better SERVFAIL:
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 512
; EDE: 23 (Network Error): ([104.47.72.81] rcode=NOTIMP for _25._tcp.contoso-com.mail.protection.outlook.com/tlsa)
; EDE: 23 (Network Error): ([104.47.34.49] rcode=NOTIMP for _25._tcp.contoso-com.mail.protection.outlook.com/tlsa)
; EDE: 22 (No Reachable Authority): (At delegation mail.protection.outlook.com for _25._tcp.contoso-com.mail.protection.outlook.com/tlsa)
;; QUESTION SECTION:
;_25._tcp.contoso-com.mail.protection.outlook.com. IN TLSA