dnssec validation / authoritative server

dnssec validation / authoritative server

  • Written by
    Walter Doekes
  • Published on

The delv(1) tool is the standard way to validate DNSSEC signatures. By default it will validate up to the DNS root zone, for which it knows and trusts the DNSKEY. If you want to validate only a part of a chain, you'll need to know a few things.

Regular DNSSEC validation

Using delv is normally as simple as this:

$ delv -t A @1.1.1.1 dnssec.works.
; fully validated
dnssec.works.   3600  IN  A 5.45.107.88
dnssec.works.   3600  IN  RRSIG A 8 2 3600 20220408113557 20220309112944 63306 dnssec.works. O+...

(1.1.1.1 is the IP of Cloudflare's free recursive resolver. If you don't know the difference between a recursive and authoritative DNS server, you may want to look that up now.)

For unsigned hostnames:

$ delv -t A @1.1.1.1 apple.com.
; unsigned answer
apple.com.    900 IN  A 17.253.144.10

And for badly signed hostnames:

$ delv -t A @1.1.1.1 fail01.dnssec.works.
;; resolution failed: SERVFAIL

(The DNSSEC signature for fail01.dnssec.works. hostname is invalid by design. This aids in testing.)

Sidenote: we add the period (".") to the end of the hostname so additional domain searches are not tried — see domain or search in /etc/resolv.conf. Without it, a system resolver might also try apple.com.yourdomain.tld.

Trace DNS lookups with dig

When using dig with +trace, we can see how a lookup would be performed by a recursing DNS server (like 1.1.1.1).

$ dig -t A fail01.dnssec.works. +trace
...
.                       6875  IN  NS  k.root-servers.net.
...
works.                172800  IN  NS  v0n3.nic.works.
...
dnssec.works.           3600  IN  NS  ns5.myinfrastructure.org.
...
fail01.dnssec.works.    3600  IN  A 5.45.109.212

As you can see, dig will not do DNSSEC validation. The recursor (at 1.1.1.1) does though. It rightly responded with a SERVFAIL because there is something wrong.

In this case, the problem being that a DS record for the hostname exists but the nameserver did not provide an RRSIG at all:

$ dig -t DS @ns5.myinfrastructure.org. fail01.dnssec.works. +short
41779 8 2 A73A4215B94FD90C2E6B94BD0513C7A82C4A1E592FD686420573E611 A1D29DE1
$ dig -t A @ns5.myinfrastructure.org. fail01.dnssec.works. +dnssec |
    awk '{if($4=="RRSIG"&&$5=="A")print}'
(no response)

Trace DNS lookups with delv

So, how does delv do this validation?

For a valid hostname, things look like this:

$ delv -t A @1.1.1.1 dnssec.works. +rtrace
;; fetch: dnssec.works/A
;; fetch: dnssec.works/DNSKEY
;; fetch: dnssec.works/DS
;; fetch: works/DNSKEY
;; fetch: works/DS
;; fetch: ./DNSKEY
; fully validated
dnssec.works.   3600  IN  A 5.45.107.88

delv does not ask other nameservers than the supplied server. But it will ask for all relevant information to be able to verify the hostname signatures.

From the output above, we see that the validation happens bottom-up (contrary to a DNS query which happens top-down): we get a record, look for the DNSKEY, look for the DS, get the next DNSKEY, etc., all the way to the root DNSKEY.

If we try this on an authoritative nameserver — one that explicitly does not recurse — we'll get an error.

$ dig -t NS @1.1.1.1 dnssec.works. +short
ns3.myinfrastructure.org.
ns5.myinfrastructure.org.
$ dig -t A @1.1.1.1 ns3.myinfrastructure.org. +short
5.45.109.212

(We looked up the IP 5.45.109.212 of the authoritative nameserver manually, so as not to clutter the following output.)

$ delv -t A @5.45.109.212 dnssec.works.
;; chase DS servers resolving 'dnssec.works/DS/IN': 5.45.109.212#53
;; REFUSED unexpected RCODE resolving 'works/NS/IN': 5.45.109.212#53
;; REFUSED unexpected RCODE resolving './NS/IN': 5.45.109.212#53
;; REFUSED unexpected RCODE resolving 'works/DS/IN': 5.45.109.212#53
;; no valid DS resolving 'dnssec.works/DNSKEY/IN': 5.45.109.212#53
;; broken trust chain resolving 'dnssec.works/A/IN': 5.45.109.212#53
;; resolution failed: broken trust chain

As promised, an error.

The nameserver at 5.45.109.212 (that knows dnssec.works.) refuses to answer requests for which it is not the authority: in this case the DS record, which is supposed to be in the parent zone. That is correct behaviour. But that is annoying if we want to test the validity of records returned by an authoritative nameserver. Can we work around that?

Creating the delv anchor-file

As we saw above, delv validation starts by looking up the DNSKEY and DS records for the hostname. Your authoritative nameserver will have the DNSKEY, but not the DS record(s):

$ dig -t A @5.45.109.212 dnssec.works. +short
5.45.107.88
$ dig -t DNSKEY @5.45.109.212 dnssec.works. +short
257 3 8 AwEAAePcoDyvYNNO/pM4qLxDQItc...
$ dig -t DS @5.45.109.212 dnssec.works.
...
;; WARNING: recursion requested but not available

The DS record can be found at the nameserver of the parent zone:

$ dig -t NS @1.1.1.1 works. +short
v0n0.nic.works.
$ dig -t DS @v0n0.nic.works. dnssec.works. +short
41779 8 2 A73A4215B94FD90C2E6B94BD0513C7A82C4A1E592FD686420573E611 A1D29DE1

As expected, there it is.

So, in order for us to validate only the behaviour/responses of the 5.45.109.212 nameserver, we have to "pre-load" the DS key. We'll whip up a small shell script for that:

make_trust_anchors() {
    local recursor='1.1.1.1'
    local awk='{printf "  \"%s\" %s %s %s %s \"%s\";\n",D,N,$1,$2,$3,$4}'
    echo "trust-anchors {"
    for name in "$@"; do
        # DNSKEY: Contains the public key that a DNS
        # resolver uses to verify DNSSEC signatures
        # in RRSIG records.
        delv -t DNSKEY @$recursor "${name%.}." +short +split=0 |
          awk -vD="${name%.}." -vN=static-key "/^257 /$awk"
        # DS: Holds the name of a delegated zone.
        # References a DNSKEY record in the sub-delegated
        # zone. The DS record is placed in the parent
        # zone along with the delegating NS records.
        delv -t DS @$recursor "${name%.}." +short +split=0 |
          awk -vD="${name%.}." -vN=static-ds "$awk"
        # (delv requires one of the above)
    done
    echo "};"
}

(By using delv to look up the DNSKEY and DS, we even validate those against our trusted root zone key.)

If we run that snippet, we see this:

$ make_trust_anchors dnssec.works.
trust-anchors {
  "dnssec.works." static-key 257 3 8 "AwEAAa+YwrBlCwfJzwmsSK87hKFAm+yz03z5pZwZWpMRJu33+GQLswgZJJX/iOTcjwHdpQXvbAHwNhLtTJ1Pp46b55Q8+zH7DkvqQAJyDTfjVXEyX/745e/5CCPAkVGnaZihj9jqichokDfWkAOJvGxqg9HdqsLmXH3a2GrxFfvwsdSPuBwQmSVzURIyZMMxRC+GH2B+ADGWxJNvrspS0lf9svfkrdMvG4hjLhwNViDSjdx9yb4yRH/+TgvTAkYS/6iB8FLBKnltYtsXuveovKp9Dwq+xllqvUQTkRK90aUQEQa8G8ukecJbIliCrPJH7JK2IaDX8ezoYZ4QMZPc2y/K8FHK0G7EVDcgwskGj/NdfEHUuBdw+Vr9eHu8x6aoU/tnTRI7qI2HmCUqcVLSEGJAmKu4A7lqVP2Xw6cpROGviS6Z";
  "dnssec.works." static-ds 41779 8 2 "A73A4215B94FD90C2E6B94BD0513C7A82C4A1E592FD686420573E611A1D29DE1";
};

And — using Bash process subtitution — we can feed that output to delv:

$ delv -t A @5.45.109.212 dnssec.works. \
    -a <(make_trust_anchors dnssec.works.) \
    +root=dnssec.works.
; fully validated
dnssec.works.   3600  IN  A 5.45.107.88
dnssec.works.   3600  IN  RRSIG A 8 2 3600 20220408113557 20220309112944 63306 dnssec.works. O++...

Cool. Now we can ask an authoritative server and validate its response.

NOTE: You do need bind9-dnsutils 9.16 or newer for this to work. Otherwise you'll get a unknown option 'trust-anchors'.

Validating authoritative server responses with an anchor-file

Using the make_trust_anchors snippet works for all subdomains served by the same DNS server:

$ delv -t A @5.45.109.212 www.dnssec.works. \
    -a <(make_trust_anchors dnssec.works.) \
    +root=dnssec.works.
; fully validated
www.dnssec.works. 3600  IN  A 5.45.109.212
www.dnssec.works. 3600  IN  RRSIG A 8 3 3600 20220420081240 20220321074251 63306 dnssec.works. 2Pq...

Let's check the invalid one:

$ delv -t A @5.45.109.212 fail01.dnssec.works. \
    -a <(make_trust_anchors dnssec.works.) \
    +root=dnssec.works.
;; insecurity proof failed resolving 'fail01.dnssec.works/A/IN': 5.45.109.212#53
;; resolution failed: insecurity proof failed

Or another invalid one:

$ delv -t A @5.45.109.212 fail02.dnssec.works. \
    -a <(make_trust_anchors dnssec.works.) \
    +root=dnssec.works.
;; validating fail02.dnssec.works/DNSKEY: verify failed due to bad signature (keyid=2536): RRSIG has expired
;; validating fail02.dnssec.works/DNSKEY: no valid signature found (DS)
;; no valid RRSIG resolving 'fail02.dnssec.works/DNSKEY/IN': 5.45.109.212#53
;; broken trust chain resolving 'fail02.dnssec.works/A/IN': 5.45.109.212#53
;; resolution failed: broken trust chain

The purpose

Why would you do this?

For one, out of curiosity. But if you're moving your DNS data to a new authoritative server, it is wise to confirm that the signatures are still correct.


Back to overview Newer post: thunderbird / opening links / ubuntu Older post: nvme drive refusing efi boot