MX lookup fails with "DNSSEC validation failed" for an insecure third-level delegation under an NSEC3 opt-out TLD (v0.16.23)

Issue Description

Outbound delivery to port.venice.it fails at the MX lookup stage with a DNSSEC validation error, even though the zone is legitimately unsigned (insecure) and validates correctly with standard tools.

The failure persists with DANE disabled globally, so the validation seems to be applied to the MX lookup itself.

The bounce received by the sender reads:

(failed to lookup 'port.venice.it': DNS resolution error: DNS error: DNSSEC validation failed)

Other .it second-level domains are delivered without problems.

  • Stalwart version: v0.16.23
  • Deployment: Docker
  • DNS resolver: Cloudflare DNS with TLS (also reproduced with the system resolver)
  • EDNS enabled, Try TCP on Error enabled

Workaround

A static relay route for port.venice.it pointing to port-venice-it.mail.protection.outlook.com:25 (STARTTLS) bypasses the MX lookup and delivery works.

Expected Behavior

The MX lookup should succeed with an insecure (unsigned) result, and the message should be delivered to port-venice-it.mail.protection.outlook.com.

Actual Behavior

Outbound delivery to port.venice.it fails at the MX lookup stage with a DNSSEC validation error, even though the zone is legitimately unsigned (insecure) and validates correctly with standard tools.

Reproduction Steps

  1. Configure any resolver type (e.g. Cloudflare with TLS).
  2. Send a message to any address at port.venice.it.
  3. Delivery fails with delivery.mx-lookup-failed / DNSSEC validation failed.

Relevant Log Output

INFO MX record lookup failed (delivery.mx-lookup-failed) queueName = "remote", from = "[email protected]", to = ["[email protected]"], 
domain = "port.venice.it", causedBy = DNS error (mail-auth.dns-error) { details = "DNS error: DNSSEC validation failed" }, elapsed = 168ms

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

Validation with standard tools

BIND’s delv (full validation from the root) correctly reports the answer as insecure, both from the host and from a container on the same Docker network querying Cloudflare:

$ delv @1.1.1.1 port.venice.it MX
; unsigned answer
port.venice.it.  3600  IN  MX  0 port-venice-it.mail.protection.outlook.com.

Google Public DNS (validating) returns NOERROR without the ad flag, as expected for an insecure zone.

DNS structure (likely root cause)

venice.it is not a separate zone: port.venice.it is delegated directly from the .it registry to its authoritative servers (ns033.name.cloud.ibm.com, ns176.name.cloud.ibm.com).

The DS query for port.venice.it is answered by the .it servers with an NSEC3 closest encloser proof where:

  • the closest encloser is the TLD apex it itself;
  • the next closer name (venice.it) is covered by an NSEC3 record with the opt-out flag set.
$ dig +dnssec DS port.venice.it @1.1.1.1
;; ->>HEADER<<- opcode: QUERY, status: NOERROR
;; AUTHORITY SECTION:
it.  3600  IN  SOA  dns.nic.it. hostmaster.nic.it. 2026092414 10800 900 604800 3600
it.  3600  IN  RRSIG  SOA 13 1 3600 20261024110329 20260924110329 53706 it. ...
6EQIAO6UF3HPKVP4A8704T8QV4NV63RL.it. 3600 IN NSEC3 1 1 0 - 6ERRV534H8JG5VFJ7B5IG4SMVMTKVGOA NS DS RRSIG
6EQIAO6UF3HPKVP4A8704T8QV4NV63RL.it. 3600 IN RRSIG NSEC3 13 2 3600 20261024110329 20260924110329 53706 it. ...
RS1N3N7M54PDEM5EUNV9NPKH3B6CGPJC.it. 3600 IN NSEC3 1 1 0 - RS1PG43S35R5V1KF1V10FT5SP409IUBV NS SOA RRSIG DNSKEY NSEC3PARAM
RS1N3N7M54PDEM5EUNV9NPKH3B6CGPJC.it. 3600 IN RRSIG NSEC3 13 2 3600 20261024110329 20260924110329 53706 it. ...

Per RFC 5155 (section 8.6), this is a valid proof of an insecure delegation. My guess is that the validator used by Stalwart (hickory-dns) does not handle the case where the closest encloser is two labels above the queried name within an opt-out span, and returns “bogus” instead of “insecure”.

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

This isn’t covered in the current docs. A community member or staff member will follow up.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Please track these two hickory bugs:

Same issue here on v0.16.24 (binary, Debian 12), System resolver, DANE disabled on the default TLS strategy (also after “Reload server settings” + “Invalidate all caches”).

It affects Italian municipalities using the standard comune...it naming, e.g. comune.casalattico.fr.it: “it.” uses NSEC3 opt-out, fr.it and casalattico.fr.it are empty non-terminals in the “it.” zone, and comune.casalattico.fr.it is an unsigned delegation directly from “it.”. delv reports “unsigned answer”; a manual SMTP session to the MX is accepted up to RCPT TO.

Packet capture during a retry: MX? → NS? → DS? (NOERROR, NSEC3 proof) → no A/AAAA query, attempt aborted.

Since thousands of Italian municipalities use this naming scheme, outbound mail to a large share of Italian public administrations is stuck. Any ETA for the hickory fix, or a way to skip DNSSEC validation on MX/IP lookups when DANE is disabled? Thanks!

Disabling DANE validation from settings is the only solution until hickory releases the fix.

Hi everyone,

This is an update on the DNSSEC and DANE delivery failures reported across several threads here. They come from bugs in hickory, the DNS library Stalwart uses to validate DNSSEC, and each one now has a fix submitted upstream:

  • Google Workspace MX hosts (aspmx.l.google.com and the altN hosts) failing with “exceeded max validation depth”: hickory#3974, fixed in #3976 and #4007. Both are merged but not yet in a hickory release.
  • Domains delegated two or more labels below an NSEC3 opt-out zone, such as port.venice.it and the comune.*.fr.it municipality domains: hickory#3985, fix in #4001.
  • Unsigned zones without an NS record at their apex, for example akokan.pl: hickory#4027, fix in #4028.
  • Microsoft 365 hosts (*.mail.protection.outlook.com) failing when the upstream resolver answers SERVFAIL for them, which Google Public DNS and many local resolvers do: hickory#4063, fix in #4064.
  • Truncated UDP responses never retried over TCP when several name servers are queried at the same time: hickory#3963, fix in #4062.
  • Valid answers rejected because the response also carries an unrelated NSEC record, plus some correctly signed wildcard answers rejected: hickory#4047, fix in #4055.
  • Validating queries sent without the CD bit, which makes a validating upstream resolver answer SERVFAIL: hickory#3966, fix in #4014 and #4036.

DANE delivery has been broken for some of you for weeks now, and the hickory maintainers seem to be busy at the moment, so most of these PRs are still waiting for review. We have forked hickory at stalwartlabs/hickory-dns with all of the fixes above applied, and Stalwart will build against that fork until hickory publishes a release that contains all of them. We ran the patched resolver against every domain reported in these threads, with Cloudflare, Google and Quad9 as upstream resolvers, and they all validate correctly now.

v0.16.26 ships with the patched resolver and will be out today or tomorrow. After upgrading you can remove any TLS strategy you added to turn DANE off for specific domains. If a domain still fails after the upgrade, post it in this thread together with the DNS resolver you use and we’ll look into it.

Still happening on v0.16.26 for unsigned domains under .aero (an NSEC3 opt-out TLD).

Setup: v0.16.26 in Docker, DNS resolver set to "System".

Outbound delivery to an unsigned .aero domain fails at the MX lookup on every retry:

  delivery.mx-lookup-failed ... causedBy = DNS error (mail-auth.dns-error) { details = "DNS error: DNSSEC validation failed" }

The domain is legitimately unsigned (no DS in aero.; aero. uses NSEC3 with opt-out, NSEC3PARAM 1 0 0 73).
delv returns "; unsigned answer" with the correct MX, and public resolvers (1.1.1.1, 8.8.8.8) answer normally.
In the same instance, TXT lookups for that domain (SPF/DKIM/DMARC) succeed; only the outbound MX lookup fails.
Delivery to .com domains works.

This looks like the hickory NSEC3 opt-out issue (hickory-dns #3985 / PR #4001) is not yet covered for .aero
in the resolver shipped with 0.16.26.

Workaround: a static Relay route per affected domain. I can share the domain privately if needed for testing.

@velidtf I ran the patched resolver from 0.16.26 against sita.aero and emirates.aero, both unsigned delegations straight from aero., with Cloudflare, Google and Quad9 upstream. All three come back insecure and the MX lookup succeeds, so .aero opt-out by itself isn’t the problem any more.

Since TXT works for your domain and MX doesn’t, I suspect something specific to its MX answer, so yes, please send me the domain in a private message (or to [email protected]). Please also include the contents of /etc/resolv.conf inside the container and what the host itself resolves through (systemd-resolved, the router, Pi-hole…). With “System” in Docker, queries go to Docker’s embedded DNS at 127.0.0.11, which forwards to whatever the host uses, and some of those don’t handle DNSSEC well. Switching the resolver to Cloudflare or Quad9 for a test would tell us quickly whether it’s the upstream.

A TLS strategy with DANE disabled for those domains gets the mail out with less setup than a relay route per domain, because the MX lookup is only validated when DANE is on.