MartB
August 22, 2026, 9:17am
21
Luckily the ALG 7 support commit fixed it already but theres a general issue with multi RRSIGS when one is invalid. Depending on the order its loaded it might return the wrong thing. Trying to get it merged but mailbox.org is fixed with the changes you pulled already i was just looking at a stale attempt.
I wrote to the hickory developers and they’re underfunded so it might take a while until the release a new version with all those fixes
MartB
September 5, 2026, 3:14pm
23
release/0.26 ← MartB:fix/multi-rrsig-validation-0.26
merged 02:46PM - 05 Sep 26 UTC
See merged main pr #3847 for more infos.
Is merged, so hopefully obscure errors with unsupported algos (dual signing) or key rollover will be less likely in the future.
Stalwart 0.16.21 will include it
I still an issue:
2026-09-09T08:39:30Z INFO IP address lookup failed (delivery.ip-lookup-failed) queueId = 328546478953863168, queueName = "remote", from = "REDACTED", to = ["[email protected] "], size = 1706, total = 1, domain = "pensionboslucht.nl", hostname = "pensionboslucht-nl.mail.protection.outlook.com", details = "Temporary Failure for pensionboslucht-nl.mail.protection.outlook.com: Connection failed: lookup error: DNS resolution error: DNS error: DNSSEC validation failed", elapsed = 311ms
2026-09-09T08:35:19Z INFO IP address lookup failed (delivery.ip-lookup-failed) queueId = 328150846885875712, queueName = "default", from = "[email protected] ", to = ["[email protected] "], size = 2825, total = 1, domain = "watchdog.kevlarr.io", hostname = "alt2.aspmx.l.google.com", details = "Temporary Failure for alt2.aspmx.l.google.com: Connection failed: lookup error: DNS resolution error: DNS error: DNSSEC validation failed", elapsed = 301ms
Are these related or is this a new issue?
New issue, fixed in hickory 0.26.3.
I still see this issue and I have to keep is disabled with 0.26.3.
Please track these two hickory bugs:
opened 12:33PM - 19 Sep 26 UTC
**Describe the bug**
Resolution fails with "DNS error: DNSSEC validation failed"… . An error is also logged from hickory_net::dnssec with "exceeded max validation depth".
**To Reproduce**
Upgrade from 0.26.1 to 0.26.3
**Expected behavior**
DNS resolution continues to work.
**System:**
- OS: Linux
- Architecture: x86_64
- Version
- rustc version: ?
**Version:**
Crate: resolver and dependencies.
Version: 0.26.3
**Additional context**
I am still looking into this issue and will update. I mostly filed this so that others encountering the same issue can help investigate. Quite possibly this is a configuration issue on my end but the configuration wasn't changed as part of the update.
~~So far I am only seeing this error on `.com` domains that **don't** support DNSSEC. (Mostly Google domains)~~ but that may just be based on the distribution of domains that our service resolves.
opened 06:10PM - 24 Sep 26 UTC
**Describe the bug**
A validating lookup of a name in an insecure zone fails wi… th `DnsError::DnssecBogus` when that zone is delegated two or more labels below the closest encloser in an NSEC3 opt-out parent zone. The parent's DS NODATA answer is a valid RFC 5155 section 8.6 proof, but hickory only looks for an opt-out NSEC3 covering H(QNAME) and never for the one covering the next closer name.
A live example is `port.venice.it`, which is delegated directly from `it.`. `venice.it` is not a zone and has no NSEC3 record of its own: it is an empty non-terminal that exists only because of an insecure delegation inside an opt-out span, and RFC 5155 section 7.1 allows it to be left out. The `it.` servers answer `DS port.venice.it` with this authority section (RRSIGs omitted):
```
it. 3600 IN SOA dns.nic.it. hostmaster.nic.it. 2026092417 10800 900 604800 3600
RS1N3N7M54PDEM5EUNV9NPKH3B6CGPJC.it. 3600 IN NSEC3 1 1 0 - RS1PG43S35R5V1KF1V10FT5SP409IUBV NS SOA RRSIG DNSKEY NSEC3PARAM
6EQIAO6UF3HPKVP4A8704T8QV4NV63RL.it. 3600 IN NSEC3 1 1 0 - 6ERRV534H8JG5VFJ7B5IG4SMVMTKVGOA NS DS RRSIG
```
With an empty salt and 0 iterations the hashes are:
| Name | H(name) | In the response |
|---|---|---|
| `it.` (closest encloser) | `RS1N3N7M54PDEM5EUNV9NPKH3B6CGPJC` | matched by the apex NSEC3 |
| `venice.it.` (next closer) | `6ERNUJ3UKNAC1SIO0BB8OPAM3QSHJC7T` | covered by `6EQIAO6U...`, Opt-Out set |
| `port.venice.it.` (QNAME) | `IE297EM4H4IQ0305K7H1ED4AQNHRBJUU` | neither matched nor covered |
That is a closest provable encloser proof plus an opt-out NSEC3 covering the next closer name, which is exactly what section 8.6 requires when no NSEC3 matches QNAME. The user who ran into this confirmed with BIND's `delv @1.1.1.1 port.venice.it MX` that the answer validates as unsigned, and both 1.1.1.1 and 8.8.8.8 return NOERROR without AD for it.
`comune.venezia.it`, an ordinary insecure delegation one label below `it.`, validates fine because its next closer name is QNAME itself.
**To Reproduce**
```toml
[dependencies]
hickory-resolver = { version = "0.26.3", features = ["dnssec-aws-lc-rs"] }
tokio = { version = "1", features = ["full"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
```
```rust
use hickory_resolver::{
TokioResolver,
config::{CLOUDFLARE, ResolverConfig, ResolverOpts},
net::runtime::TokioRuntimeProvider,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter("hickory_net::dnssec=debug")
.without_time()
.with_ansi(false)
.init();
let mut opts = ResolverOpts::default();
opts.validate = true;
opts.num_concurrent_reqs = 1;
let resolver = TokioResolver::builder_with_config(
ResolverConfig::udp_and_tcp(&CLOUDFLARE),
TokioRuntimeProvider::default(),
)
.with_options(opts)
.build()
.unwrap();
for name in ["port.venice.it.", "comune.venezia.it."] {
match resolver.mx_lookup(name).await {
Ok(lookup) => lookup
.answers()
.iter()
.for_each(|r| println!("{name} {} {}", r.data, r.proof)),
Err(err) => println!("{name} error: {err:?}"),
}
}
}
```
`num_concurrent_reqs = 1` only keeps an unrelated failure out of the way: with the default, one of my runs failed earlier on the root DNSKEY fetch with "response was truncated; TCP already tried or no other transport available", which looks like #3963. Relevant output, identical over three runs:
```
DEBUG hickory_net::dnssec: nsec3 proof for port.venice.it., returning Bogus: no valid servicing wildcard proof
DEBUG hickory_net::dnssec: returning Nsec error for port.venice.it. Bogus
DEBUG hickory_net::dnssec: failed to verify: port.venice.it. record_type: MX: communication failure for query: port.venice.it. IN DS: DNS error: DNSSEC Negative Record Response for port.venice.it. IN DS, Bogus
port.venice.it. error: Dns(DnssecBogus)
DEBUG hickory_net::dnssec: nsec3 proof for comune.venezia.it., returning Secure: DS query covered by opt-out proof
comune.venezia.it. 5 venis01.esvacloud.com. Insecure
comune.venezia.it. 5 venis02.esvacloud.com. Insecure
```
**Expected behavior**
The MX lookup for `port.venice.it` succeeds and the records carry `Proof::Insecure`.
**Suggested fix**
Disclaimer: Fix proposed by Claude Opus 5.5, against main at f486d9e:
```diff
@@ -220,7 +220,18 @@ pub(super) fn verify_nsec3_insecure_delegation(zone: &Name, nsec3s: &[(&Name, &N
return info.nsec3_data.opt_out();
}
- false
+ let Some(parent) = records.first().map(|record| &record.zone_name) else {
+ return false;
+ };
+ if records.iter().any(|record| &record.zone_name != parent) {
+ return false;
+ }
+ let Ok(cx) = Context::new(&query, Some(parent), &records) else {
+ return false;
+ };
+ cx.closest_encloser_proof()
+ .next_closer
+ .is_some_and(|(_, record)| record.nsec3_data.opt_out())
}
/// Convert pairs of record names and NSEC3 RDATA to structures containing hashed record names, zone
@@ -425,6 +436,18 @@ fn validate_nodata_response(
return cx.proof(Proof::Secure, "DS query covered by opt-out proof");
}
+ if query_type == RecordType::DS
+ && cx
+ .closest_encloser_proof()
+ .next_closer
+ .is_some_and(|(_, record)| record.nsec3_data.opt_out())
+ {
+ return cx.proof(
+ Proof::Secure,
+ "DS query next closer covered by opt-out proof",
+ );
+ }
+
let (proof, reason) = match wildcard_encloser_num_labels {
// Case 4:
// Name is serviced by wildcard that has a record of this type
```
`verify_nsec3_insecure_delegation` has no SOA to work from, so the patch bounds the encloser walk with the parent of the NSEC3 owner names and gives up if they disagree.
The change does not accept anything RFC 5155 rejects. `closest_encloser_proof()` already refuses an ancestor-delegation NSEC3 as the closest encloser (RFC 5155 section 8.3, RFC 6840 section 4.1). In a correctly signed zone no existing name falls inside an NSEC3 span, and section 7.1 requires the empty non-terminal above a signed delegation to have its own NSEC3, so without forged signatures this proof cannot be produced for a delegation that has a DS.
With the patch applied to main, `cargo test -p hickory-net --features dnssec-aws-lc-rs --lib dnssec::` passes, 31 tests including the new one. The same patch on 0.26.3 makes the program above return `Insecure` for `port.venice.it`, and `comune.venezia.it`, `gmail.com` and `example.com` resolve with the same proofs as before. I can send it as a PR together with the test.
**System:**
- OS: macOS 26.6.2 (also seen on Linux in Docker by the user who reported it)
- Architecture: arm64
- rustc version: 1.98.1
**Version:**
Crate: hickory-net, hickory-resolver
Version: 0.26.3, and main at f486d9e
afink
October 1, 2026, 2:23pm
29
I have the same issue (I believe): [email protected] (failed to lookup ‘sprk.gov.lv’: DNS resolution error: DNS error: DNSSEC validation failed)
sprk.gov.lv is not DNSSec signed according to https://dnssec.health/ .
now I wonder how I can pull in the upgrades of hickory. apt update/apt upgrade seems not enough
sprk.gov.lv isn’t signed, but the chain above it is: .lv is signed and proves that gov.lv has no DS using an NSEC3 opt-out record. Hickory currently rejects that particular kind of proof as Bogus, which is hickory-dns#3985, the second issue linked above and still open. The other one, #3974 , has been fixed upstream but isn’t in a hickory release yet.
Hickory is compiled into the Stalwart binary, so apt has nothing to update; the fix will come with a Stalwart release once hickory ships it. Until then, on 0.16.24 or later you can turn DANE off for that one domain: create an MtaTlsStrategy named no-dane with dane set to disable, and add this rule ahead of the others in the tls expression on MtaOutboundStrategy:
{"if": "rcpt_domain == 'sprk.gov.lv'", "then": "'no-dane'"}
With DANE off for that domain the MX and address lookups skip DNSSEC validation, and every other destination keeps it.
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.