Skip to content

fix: reliably check DNSSEC AD flag with dedicated resolver (micro-fix)#7139

Open
omichauhan-lgtm wants to merge 2 commits into
aden-hive:mainfrom
omichauhan-lgtm:fix-5112-dnssec-validation
Open

fix: reliably check DNSSEC AD flag with dedicated resolver (micro-fix)#7139
omichauhan-lgtm wants to merge 2 commits into
aden-hive:mainfrom
omichauhan-lgtm:fix-5112-dnssec-validation

Conversation

@omichauhan-lgtm
Copy link
Copy Markdown

@omichauhan-lgtm omichauhan-lgtm commented Apr 26, 2026

Fixes #5112.

The _check_dnssec\ tool was returning false positives because the system resolver doesn't always validate DNSSEC, or strips the AD flag. This PR ensures we:

  1. Initialize an isolated \dns.resolver.Resolver()\
  2. Target public resolvers known to perform validation (8.8.8.8, 1.1.1.1)
  3. Request validation using EDNS DO bit
  4. Check the AD flag on the returned SOA records

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced DNSSEC validation in the DNS security scanner. The scanner now confirms DNSSEC enablement only when cryptographic validation succeeds, provides clearer "enabled"/"disabled" reporting, and gives more informative issue messages when DNSSEC is absent or cannot be validated. Improvements reduce false positives and improve the accuracy of validation status shown to users.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

DNSSEC validation now uses an isolated resolver with explicit public nameservers and EDNS (DO bit), queries the SOA record, and determines DNSSEC enabled only if the SOA response includes the Authenticated Data (AD) flag; function signature/invocation updated accordingly.

Changes

Cohort / File(s) Summary
DNSSEC Validation Logic
tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py
Reworked _check_dnssec and its caller to create an isolated dns.resolver.Resolver with explicit public nameservers, enable EDNS with the DO bit and larger buffer, query SOA (not DNSKEY), and mark enabled only when the response has the AD flag; updated issue messages for validation failures.

Sequence Diagram(s)

sequenceDiagram
    participant Scanner
    participant IsolatedResolver
    participant PublicNameserver
    participant DNSZone

    Scanner->>IsolatedResolver: build resolver (nameservers + EDNS DO)
    IsolatedResolver->>PublicNameserver: DNS query: SOA for domain
    PublicNameserver->>DNSZone: resolve SOA (validate chain)
    DNSZone-->>PublicNameserver: SOA response (may include AD)
    PublicNameserver-->>IsolatedResolver: return response
    IsolatedResolver-->>Scanner: response with flags (check AD)
    Scanner->>Scanner: set enabled=true if AD else enabled=false + issue
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped from cache to a nameserver bright,
Built a little resolver that queries just right,
With DO and SOA I listen for AD's chime,
No more guessing — the verdict is now on time,
A hop, a sniff, DNS truth in rhyme.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing DNSSEC validation by using a dedicated resolver to check the AD flag instead of relying on DNSKEY presence.
Linked Issues check ✅ Passed The PR successfully implements all core requirements from issue #5112: uses isolated resolver with validating nameservers (8.8.8.8, 1.1.1.1), enables EDNS DO bit, queries SOA records, and checks AD flag for validation status.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the DNSSEC validation issue described in #5112; no extraneous or unrelated modifications are present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix-5112-dnssec-validation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py (1)

201-223: Minor cleanups: unused resolver arg and duplicate except arms.

  • The resolver parameter is no longer used inside _check_dnssec. Either consume it (e.g., as the timeout source / fallback per the comment above) or drop it from the signature for clarity.
  • Lines 220–223 split NoAnswer from (NXDOMAIN, DNSException) but both arms do pass; collapse into a single except clause.
♻️ Proposed cleanup
-    except dns.resolver.NoAnswer:
-        pass
-    except (dns.resolver.NXDOMAIN, dns.exception.DNSException):
-        pass
+    except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.DNSException):
+        pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py`
around lines 201 - 223, The _check_dnssec function currently ignores its
resolver parameter and has two separate except blocks that both pass; either use
the passed-in resolver (e.g., use resolver.timeout, resolver.nameservers as a
fallback, or call resolver.resolve when appropriate) or remove the resolver
argument from the function signature to avoid confusion, and collapse the
exception handling into a single except that catches dns.resolver.NoAnswer,
dns.resolver.NXDOMAIN, and dns.exception.DNSException (or a common base) and
handles them consistently (currently a pass) so the function is concise and the
intent is clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py`:
- Around line 207-217: The current check in dns_security_scanner.py that
inspects answers.response.flags & dns.flags.AD on sec_resolver (which uses plain
UDP nameservers ['8.8.8.8','1.1.1.1']) is only a best-effort signal and not
cryptographic proof; update the dnssec_enabled() implementation and its
docstring to (1) clearly state that the AD flag over plain UDP is not
channel-authenticated and is only advisory, and (2) provide two stronger
options: a) perform DNS-over-TLS/HTTPS queries using dns.query.tls or
dns.query.https (or dns.nameserver.DoTNameserver/DoHNameserver) to ensure
channel integrity, or b) perform local DNSSEC validation of RRsets via
dns.dnssec.validate and document that full chain-of-trust requires managing
trust anchors; modify the code paths around sec_resolver.resolve(domain, "SOA")
and the AD-flag check to either use DoT/DoH resolver or call dns.dnssec.validate
on received RRsets when opting for local validation, and ensure the docstring
describes the trade-offs and recommended default.
- Around line 204-214: The code creates a new sec_resolver and hardcodes public
nameservers without honoring the caller's timeout/lifetime and without a
fallback, causing false negatives; update dns_security_scanner.py to (1) set
sec_resolver.timeout and sec_resolver.lifetime from the passed-in resolver (or
accept caller-configured values) before calling sec_resolver.resolve(domain,
"SOA"), (2) avoid blindly overwriting the provided resolver—use the passed-in
resolver as the primary and only create sec_resolver when needed, (3) implement
a fallback attempt to the system/default resolver if the public validators
(sec_resolver.nameservers = ['8.8.8.8','1.1.1.1']) time out or raise
dns.exception.DNSException, and (4) treat inability to reach any validating
resolver as a distinct error/issue (e.g., return or raise a specific
"validation_unavailable" result) instead of marking DNSSEC as disabled; also
remove the redundant except dns.resolver.NoAnswer handler since NoAnswer is
under dns.exception.DNSException.

---

Nitpick comments:
In `@tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py`:
- Around line 201-223: The _check_dnssec function currently ignores its resolver
parameter and has two separate except blocks that both pass; either use the
passed-in resolver (e.g., use resolver.timeout, resolver.nameservers as a
fallback, or call resolver.resolve when appropriate) or remove the resolver
argument from the function signature to avoid confusion, and collapse the
exception handling into a single except that catches dns.resolver.NoAnswer,
dns.resolver.NXDOMAIN, and dns.exception.DNSException (or a common base) and
handles them consistently (currently a pass) so the function is concise and the
intent is clear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb87686c-84e2-4c63-974a-5cbc75166795

📥 Commits

Reviewing files that changed from the base of the PR and between 445c960 and a0832e0.

📒 Files selected for processing (1)
  • tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py

Comment on lines +204 to +214
# Create an isolated resolver to avoid polluting the default one
sec_resolver = dns.resolver.Resolver()

# Use known validating nameservers (Google, Cloudflare)
sec_resolver.nameservers = ['8.8.8.8', '1.1.1.1']

# Use EDNS to request DNSSEC validation (DO flag) and increase packet size
sec_resolver.use_edns(0, dns.flags.DO, 4096)

# Query for SOA records (mandatory for all domains)
answers = sec_resolver.resolve(domain, "SOA")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

dnspython Resolver default timeout and lifetime values

💡 Result:

The dnspython dns.resolver.Resolver class has default values of timeout=2.0 seconds (time to wait for a response from a server) and lifetime=5.0 seconds (total time to spend trying to get an answer to the question).

Citations:


🏁 Script executed:

head -230 tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py | tail -35

Repository: aden-hive/hive

Length of output: 1301


🏁 Script executed:

python3 << 'EOF'
# Check if dns.resolver.NoAnswer is a subclass of dns.exception.DNSException
# by inspecting dnspython's exception hierarchy

import dns.resolver
import dns.exception

# Check NoAnswer's MRO (method resolution order)
print("dns.resolver.NoAnswer MRO:")
print(dns.resolver.NoAnswer.__mro__)
print()

# Check if NoAnswer is a subclass of DNSException
is_subclass = issubclass(dns.resolver.NoAnswer, dns.exception.DNSException)
print(f"Is NoAnswer a subclass of DNSException? {is_subclass}")
print()

# Check other exceptions
print("dns.resolver.NXDOMAIN MRO:")
print(dns.resolver.NXDOMAIN.__mro__)
EOF

Repository: aden-hive/hive

Length of output: 174


🏁 Script executed:

# Find requirements or dependencies file
find . -name "requirements*.txt" -o -name "pyproject.toml" -o -name "setup.py" | head -5

Repository: aden-hive/hive

Length of output: 185


🏁 Script executed:

# Check the main pyproject.toml and tools pyproject.toml for dnspython version
grep -i "dnspython" ./pyproject.toml ./tools/pyproject.toml ./tools/src/pyproject.toml 2>/dev/null

Repository: aden-hive/hive

Length of output: 105


🌐 Web query:

dnspython 2.4.0 exception hierarchy NoAnswer NXDOMAIN DNSException

💡 Result:

In dnspython 2.4.0, all exceptions subclass dns.exception.DNSException, the abstract base class for dnspython exceptions. dns.resolver.NoAnswer and dns.resolver.NXDOMAIN are both subclasses of DNSException, defined in the dns.resolver module. NoAnswer is raised when the DNS response indicates the query name exists but has no RRset of the requested type/class (if raise_on_no_answer=True). NXDOMAIN is raised when the query name does not exist. This hierarchy has been consistent since at least version 1.14.0 and remains the same in later versions like 2.7+ where source code explicitly shows class NoAnswer(dns.exception.DNSException):.

Citations:


Hardcoded public resolvers + missing timeout can re-introduce false negatives.

In egress-restricted environments (corporate networks, air-gapped hosts, sandboxes) outbound UDP/53 to 8.8.8.8/1.1.1.1 is frequently blocked. When that happens, the SOA query will hit dnspython's default lifetime (~5s) and raise a Timeout/LifetimeTimeout, which is swallowed by the DNSException handler and reported as enabled: False — the same false-negative class issue #5112 set out to fix, just for a different population of users.

Two concrete gaps:

  • sec_resolver.timeout / sec_resolver.lifetime are not set, so this check no longer respects the caller-configured timeout on resolver.
  • There is no fallback path when both public validators are unreachable; the scanner cannot distinguish "DNSSEC not enabled" from "could not reach a validating resolver".

Consider: (1) propagating the caller's timeout/lifetime, (2) attempting the system resolver as a fallback (it's already configured by the caller and may itself be a validating resolver), and (3) surfacing a distinct issue when validation could not be performed at all so the risk grader doesn't penalize unrelated network failures.

Also minor: the resolver parameter is passed but never used (a new sec_resolver is created instead), and the first except dns.resolver.NoAnswer block is redundant since NoAnswer is a subclass of dns.exception.DNSException.

🛠️ Suggested adjustment
-        # Create an isolated resolver to avoid polluting the default one
-        sec_resolver = dns.resolver.Resolver()
-        
-        # Use known validating nameservers (Google, Cloudflare)
-        sec_resolver.nameservers = ['8.8.8.8', '1.1.1.1']
-        
-        # Use EDNS to request DNSSEC validation (DO flag) and increase packet size
-        sec_resolver.use_edns(0, dns.flags.DO, 4096)
-        
-        # Query for SOA records (mandatory for all domains)
-        answers = sec_resolver.resolve(domain, "SOA")
-        
-        # Verify the Authenticated Data (AD) flag is present
-        if answers.response.flags & dns.flags.AD:
-            return {"enabled": True, "issues": []}
+        # Isolated resolver pinned to known validating nameservers, with the
+        # caller's timeout budget preserved.
+        sec_resolver = dns.resolver.Resolver()
+        sec_resolver.nameservers = ["8.8.8.8", "1.1.1.1"]
+        sec_resolver.timeout = resolver.timeout
+        sec_resolver.lifetime = resolver.lifetime
+        sec_resolver.use_edns(0, dns.flags.DO, 4096)
+
+        answers = sec_resolver.resolve(domain, "SOA")
+        if answers.response.flags & dns.flags.AD:
+            return {"enabled": True, "issues": []}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py`
around lines 204 - 214, The code creates a new sec_resolver and hardcodes public
nameservers without honoring the caller's timeout/lifetime and without a
fallback, causing false negatives; update dns_security_scanner.py to (1) set
sec_resolver.timeout and sec_resolver.lifetime from the passed-in resolver (or
accept caller-configured values) before calling sec_resolver.resolve(domain,
"SOA"), (2) avoid blindly overwriting the provided resolver—use the passed-in
resolver as the primary and only create sec_resolver when needed, (3) implement
a fallback attempt to the system/default resolver if the public validators
(sec_resolver.nameservers = ['8.8.8.8','1.1.1.1']) time out or raise
dns.exception.DNSException, and (4) treat inability to reach any validating
resolver as a distinct error/issue (e.g., return or raise a specific
"validation_unavailable" result) instead of marking DNSSEC as disabled; also
remove the redundant except dns.resolver.NoAnswer handler since NoAnswer is
under dns.exception.DNSException.

Comment on lines +207 to +217
# Use known validating nameservers (Google, Cloudflare)
sec_resolver.nameservers = ['8.8.8.8', '1.1.1.1']

# Use EDNS to request DNSSEC validation (DO flag) and increase packet size
sec_resolver.use_edns(0, dns.flags.DO, 4096)

# Query for SOA records (mandatory for all domains)
answers = sec_resolver.resolve(domain, "SOA")

# Verify the Authenticated Data (AD) flag is present
if answers.response.flags & dns.flags.AD:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

dnspython AD flag DNSSEC validation trust channel DoT DoH

💡 Result:

dnspython provides DNSSEC signature validation for individual RRsets via dns.dnssec.validate and validate_rrsig, requiring the cryptography library (pip install dnspython[dnssec]). This validates RRSIGs against DNSKEYs but does not perform full chain-of-trust validation from root trust anchors; users must fetch and provide keys manually or implement recursive validation. The stub resolver (dns.resolver.Resolver) supports DNSSEC-aware queries by setting use_edns(0, dns.flags.DO) to request DNSSEC data and checking response.flags & dns.flags.AD to verify if the upstream resolver performed validation (AD flag indicates authenticated data). Example from Mailu project: import dns.resolver import dns.flags resolver = dns.resolver.Resolver resolver.use_edns(0, dns.flags.DO, 1232) resolver.nameservers = ['8.8.8.8'] answer = resolver.resolve('example.com', 'A') if answer.response.flags & dns.flags.AD: print("Upstream validated DNSSEC") The resolver does not perform validation itself; AD flag presence confirms the recursive resolver (e.g., 8.8.8.8) did. dnspython supports secure channels: DNS-over-TLS (DoT) via dns.query.tls or dns.nameserver.DoTNameserver, DNS-over-HTTPS (DoH) via dns.query.https or dns.nameserver.DoHNameserver (requires pip install dnspython[doh] for requests/httpx). Resolver can use these by setting nameservers to DoT/DoH objects (recent versions). These protect query privacy/integrity; DNSSEC validation occurs at the upstream resolver if supported. To check DNSSEC protection programmatically: Query with DO bit to authoritative NS, validate DNSKEY self-signature using dns.dnssec.validate, and check AD from validating resolver. No built-in full trust chain validation; see open issue #414.

Citations:


Use DoT/DoH or local validation for cryptographic DNSSEC assurance, not the AD flag over plain UDP.

The AD flag from an upstream resolver (8.8.8.8/1.1.1.1) only indicates the resolver performed DNSSEC validation; it provides no protection against on-path modification of that flag. Plain UDP/53 cannot guarantee channel integrity, making this check best-effort rather than cryptographic proof.

For stronger validation:

  • Use DNS-over-TLS (dns.query.tls or dns.nameserver.DoTNameserver) or DNS-over-HTTPS (dns.query.https or dns.nameserver.DoHNameserver) for channel security
  • Perform local DNSSEC validation using dns.dnssec.validate on responses; however, this validates individual RRsets but does not automatically perform full chain-of-trust validation from root trust anchors

At minimum, document in the dnssec_enabled() docstring that the AD flag check is only a best-effort signal and does not provide cryptographic assurance without a trusted channel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py`
around lines 207 - 217, The current check in dns_security_scanner.py that
inspects answers.response.flags & dns.flags.AD on sec_resolver (which uses plain
UDP nameservers ['8.8.8.8','1.1.1.1']) is only a best-effort signal and not
cryptographic proof; update the dnssec_enabled() implementation and its
docstring to (1) clearly state that the AD flag over plain UDP is not
channel-authenticated and is only advisory, and (2) provide two stronger
options: a) perform DNS-over-TLS/HTTPS queries using dns.query.tls or
dns.query.https (or dns.nameserver.DoTNameserver/DoHNameserver) to ensure
channel integrity, or b) perform local DNSSEC validation of RRsets via
dns.dnssec.validate and document that full chain-of-trust requires managing
trust anchors; modify the code paths around sec_resolver.resolve(domain, "SOA")
and the AD-flag check to either use DoT/DoH resolver or call dns.dnssec.validate
on received RRsets when opting for local validation, and ensure the docstring
describes the trade-offs and recommended default.

@omichauhan-lgtm
Copy link
Copy Markdown
Author

Thanks for the review @coderabbitai! I've removed the unused
esolver argument from _check_dnssec and merged the exception blocks as suggested.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Only users with a collaborator, contributor, member, or owner role can interact with CodeRabbit.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py (1)

220-220: Remove redundant exception classes from except tuple.

Both dns.resolver.NoAnswer and dns.resolver.NXDOMAIN are subclasses of dns.exception.DNSException. This pattern appears 7 times throughout the file (lines 130, 171, 192, 220, 236, 245, 253) and can be simplified to catch just dns.exception.DNSException.

♻️ Optional cleanup
-    except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.DNSException):
+    except dns.exception.DNSException:
         pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py` at
line 220, Replace the redundant exception tuple "except (dns.resolver.NoAnswer,
dns.resolver.NXDOMAIN, dns.exception.DNSException):" with a single "except
dns.exception.DNSException:" wherever it appears (the occurrences that reference
dns.resolver.NoAnswer and dns.resolver.NXDOMAIN alongside
dns.exception.DNSException); this removes duplicate subclass catches and leaves
the broad DNSException catch in functions/blocks that currently use that except
clause.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py`:
- Line 220: Replace the redundant exception tuple "except
(dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.DNSException):"
with a single "except dns.exception.DNSException:" wherever it appears (the
occurrences that reference dns.resolver.NoAnswer and dns.resolver.NXDOMAIN
alongside dns.exception.DNSException); this removes duplicate subclass catches
and leaves the broad DNSException catch in functions/blocks that currently use
that except clause.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f14e14d-3012-4afd-afc8-4ad1ab970a59

📥 Commits

Reviewing files that changed from the base of the PR and between a0832e0 and 1dfe5d4.

📒 Files selected for processing (1)
  • tools/src/aden_tools/tools/dns_security_scanner/dns_security_scanner.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: dns_security_scanner tool relies on the system DNS resolver which may strip DNSSEC data

1 participant