dnsworkshop.de
22 Dec 2013

a local, augmented root-zone with DNSSEC

Sunday Dec 22, 2013

Table of Contents

Why a local root zone

Sometimes I get this question in my DNSSEC training classes: "now DNSSEC seems to be a good technology, but the root-zone is controlled by the US government. Because of that, can we trust DNSSEC?".

My answer is not to mix technology (DNSSEC) with implementation (the DNS system of the Internet).

Both are separate. While I can understand that some people do not trust the organisations in control of the Internet DNS root-zone, I see no flaw in DNSSEC (at this moment).

One way to solve the trust issue with the Internet root-zone is to host your own root-zone for the Internet. Then you are in full control of that zone. Have that zone in your own network, or on your Laptop computer und use it for the starting.point of all DNSSEC validation (the trust anchor) for DNS.

Besides the trust question, there might be another reason to operate a local root-zone: some organisations have created an internal, private top-level domain (TLD)1. A local, dnssec-signed root-zone enables the operator to remove or add any delegations, while still being able to validate all DNSSEC signed data in the Internet, as well as data that is stored in their own private DNS namespace.

The following tutorial explains the steps required to generate a local augmented and DNSSEC signed root-zone. The tutorial requires some understanding of DNS concepts and basic knowledge on DNSSEC. A good starting point to learn about DNSSEC (besides a training) is the book DNSSEC mastery by Michael W. Lucas.

All the tools used are part of the BIND DNS Server for ISC (Internet Systems Consortium). This tutorial has been tested using BIND 9.9.4-P1. Older or newer versions of BIND might require a different setup.

The setup

We need one or more authoritative servers to host the augmented root-zone. For production deployments in an internal network, at least two authoritative servers are required. For a local root-zone on a mobile device (Laptop etc.), one single authoritative server might be good enough.

We also need at least one caching DNS server (smart resolver). This cannot be the same DNS server instance as the authoritative server, however it is possible to run both (caching and authoritative server) on the same machine, but have them listen to different IP addresses. In this tutorial, I will use two separate machines:

  • authoritative server: a.myroot-server.loc:192.0.2.53
  • caching server: cache.loc:192.0.2.153

On both servers, the BIND configuration file will be in /etc/named.conf and the BIND "data" directory will be /var/named.

The operating-system used is Debian 7.x.

Authoritative Server (DNS root-server)

Root-Zone

The starting point is the official root-zone. The "F" root-server allows zone transfer of the full root-zone (alternatively, the root-zone can be downloaded by ftp from ftp.internic.net).

shell> mkdir -p /var/named/root
shell> cd /var/named/root
shell> dig @f.root-servers.net . axfr +onesoa | grep -v DNSKEY > root.zone
shell> named-checkzone . root.zone

The grep command will remove the public DNSSEC keys (the DNSKEY records) from the zone, as we will use our own DNSSEC keys for signing the root zone.

The command named-checkzone will test if the zone is completely transferred and will load. Expect to see a couple of "glue record" warnings from the tool, we can ignore them.

Next, we create a new Zone-Signing-Key (ZSK) and a Key-Signing-Key (KSK) for the root-zone:

shell> dnssec-keygen -K /var/named/keys/ -a RSASHA256 -b 2048 -n ZONE .
shell> dnssec-keygen -K /var/named/keys/ -a RSASHA256 -b 4096 -f KSK  -n ZONE .

Now we copy the new DNSKEY records into the root-zone file2:

shell> cat ../keys/K.+008+*.key >> root.zone

The official Internet root-zone authoritative servers have names in the root-servers.net domain. As we do not have the private key for that name, and we cannot enter a DS record (delegation signer) for our internal root-server into the .net zone, we need to change the NS records and the SOA record. In my example, the hostname of the local root-server is in the myroot-servers.loc domain. That name is stored inside the loc. TLD, which we will also host on our authoritative server.

With an text editor, we change the SOA record and the NS record(s) to point to our own authoritative servers. We also need to add proper glue records for every hostname used in the NS records:

.                       86400   IN      SOA     a.myroot-servers.loc. hostmaster.loc. 2013122200 1800 900 604800 86400
.                       518400  IN      NS      a.myroot-servers.loc.
loc.                    86400   IN      NS      a.myroot-servers.loc.
a.myroot-servers.loc.   86400   IN      A       192.0.2.53

Make sure that all other NS records for the root zone "." have been removed.

Now we can sign our zone:

shell> dnssec-signzone -o . -t -R -S -K /var/named/keys/ root.zone
Fetching ZSK 15795/RSASHA256 from key repository.
Fetching KSK 50434/RSASHA256 from key repository.
Verifying the zone using the following algorithms: RSASHA256.
Zone fully signed:
Algorithm: RSASHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                      ZSKs: 1 active, 0 stand-by, 0 revoked
root.zone.signed
Signatures generated:                      528
Signatures retained:                         0
Signatures dropped:                          3
Signatures successfully verified:            0
Signatures unsuccessfully verified:          0
Signing time in seconds:                85.686
Signatures per second:                   6.161
Runtime in seconds:                     89.025

The -R switch removed all signatures created by the DNSSEC keys of the real root zone. The -t switch prints out some benchmark information. Your signing process is probably faster, as I tested this on a Rasberry Pi.

the augmented ".loc" TLD-Zone

Below is the content of the zone-file for the .loc TLD zone. It contains the same NS records as we have seen in the root-zone for the delegation of the .loc zone:

$TTL 86400
loc.                    86400   IN      SOA     a.myroot-servers.loc. (
                                                hostmaster 1 1d 2h 41d 1h )
loc.                    86400   IN      NS      a.myroot-servers.loc.
a.myroot-servers.loc.   86400   IN      A       192.0.2.53

The .loc TLD should also be DNSSEC secured, so we create a set of keys for this zone as well:

shell> dnssec-keygen -K /var/named/keys/ -a rsasha256 -b 2048 \
              -n ZONE loc
shell> dnssec-keygen -K /var/named/keys/ -a rsasha256 -b 4096 \
              -f KSK -n ZONE loc

The BIND configuration file

This is the BIND configuration file named.conf for the authoritative server. It loads both the root-zone and the .loc private TLD. Both zones are configured as dynamic zones. After loading the zones into the BIND DNS Server, you cannot change the zone content with an text editor anymore. You need to use nsupdate instead. I highly recommend nsupdate, as it catches a number of errors that can occur when editing zone file manually:

options {
        directory "/var/named";
        key-directory "keys";
        dnssec-enable yes;
        dnssec-validation auto;
        dnssec-lookaside auto;
        recursion no;
};

zone "." {
        type master;
        file "root/root.zone.signed";
        update-policy local;
        auto-dnssec maintain;
};

zone "loc" {
        type master;
        file "master/loc.zone";
        update-policy local;
        auto-dnssec maintain;
};

This is the time to check the BIND configuration and all zone-files:

shell> named-checkconf -z
zone ./IN: loaded serial 2013122201 (DNSSEC signed)
zone loc/IN: loaded serial 1 

If named-checkconf does not report any errors, we start our DNS Server (from the command-line or using the start-script):

shell> named
shell> tailf /var/log/syslog
22-Dec-2013 14:39:27.405 starting BIND 9.9.4-P1 -g
22-Dec-2013 14:39:27.407 built with '--libdir=/usr/local/lib'
22-Dec-2013 14:39:27.409 ----------------------------------------------------
22-Dec-2013 14:39:27.411 BIND 9 is maintained by Internet Systems Consortium,
22-Dec-2013 14:39:27.412 Inc. (ISC), a non-profit 501(c)(3) public-benefit
22-Dec-2013 14:39:27.412 corporation.  Support and training for BIND 9 are
22-Dec-2013 14:39:27.412 available at https://www.isc.org/support
22-Dec-2013 14:39:27.412 ----------------------------------------------------
22-Dec-2013 14:39:27.414 using 1 UDP listener per interface
22-Dec-2013 14:39:27.418 using up to 4096 sockets
22-Dec-2013 14:39:27.471 loading configuration from '/etc/named.conf'
[...]
22-Dec-2013 14:39:28.143 zone ./IN: loaded serial 2013122200 (DNSSEC signed)
22-Dec-2013 14:39:28.148 all zones loaded
22-Dec-2013 14:39:28.150 running
22-Dec-2013 14:39:28.152 zone ./IN: sending notifies (serial 2013122200)
22-Dec-2013 14:39:28.159 zone ./IN: reconfiguring zone keys
22-Dec-2013 14:39:28.172 zone ./IN: next key event: 22-Dec-2013 15:39:28.159

The .loc TLD needs to be signed, we do that using rndc:

shell> rndc sign loc

As rndc sign does not report some types of errors, we check if the .log zone is now really signed:

shell> dig -t soa loc +dnssec +m @localhost
; <<>> DiG 9.9.4-P1 <<>> -t soa loc +dnssec +m @localhost
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31525
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 3
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 4096
;; QUESTION SECTION:
;loc.                   IN SOA

;; ANSWER SECTION:
loc.                    86400 IN SOA a.myroot-servers.loc. hostmaster.loc. (
                                3          ; serial
                                86400      ; refresh (1 day)
                                7200       ; retry (2 hours)
                                3542400    ; expire (5 weeks 6 days)
                                3600       ; minimum (1 hour)
                                )
loc.                    86400 IN RRSIG SOA 8 1 86400 (
                                20140121142448 20131222132448 38865 loc.
                                cDBUly3QhZ0dm6HJboA/UQPYkMLFK3pWGOt8x98pzY+W
                                oD3cBult7trdxAqNgMuyl5nTZbciEU0o0HNKZlr9cJ75
                                sxqY2PbKjPyZ63t9837LRIoWQtce62M+uP9KLnbaBXrw
                                x9eeyUofy8hNFRstGwoTuDqT5s0GgcUBGjpOWS3vyQB2
                                zAj1cNvy8Mr9sTdxw84VdM7np30dn6z/9IrGurVD3Etz
                                YXo94QgZrNMExqV2u9vYE+tJWldeo+swbwctWH6f/oqC
                                jvOArSiR378h0XiHq11IzM4cjx0IwW0rsVYmD+dhwsEm
                                svLsY66mZRKqyskaXXbbIOUoXWrydwiv+Q== )

;; AUTHORITY SECTION:
[...]

Yes, we see a RRSIG signature for the SOA record, so the signing process was successful.

To create a full DNSSEC chain-of-trust from the .loc TLD to the local root-zone, we need to add the DS record (delegation signer) into out private root-zone. We create the DS-Record from the KSK of .loc

shell> dnssec-dsfromkey /var/named/keys/Kloc.+008+38611.key
loc. IN DS 38611 8 1 FBBBF41938C6CA0E286675F9BBD38C7719217B89
loc. IN DS 38611 8 2 52F2B009BCD019B9AE2470EAE321DAE74AE77B58F094778BCCD9A53AF38A30F8

… and use nsupdate to add the DS-Records to the root-zone:

# nsupdate -l
> ttl 86400
> add loc. IN DS 38611 8 1 FBBBF41938C6CA0E286675F9BBD38C7719217B89
> add loc. IN DS 38611 8 2 52F2B009BCD019B9AE2470EAE321DAE74AE77B58F094778BCCD9A53AF38A30F8
> send
> quit

Our authoritative server is now ready. Next is the caching server(s).

the Caching Server configuration

We start with a basic BIND named.conf configuration file for a caching, validating DNS Server:

acl myclients { 192.0.2.0/24; localhost; };

options {
        directory "/var/named";
        allow-recursion { myclients; };
        dnssec-enable yes;
        dnssec-validation yes;
        dnssec-lookaside auto;
};

logging {
    channel syslog { syslog daemon; severity info; };
    channel security { file "security.log" versions 10 size 50M; print-time yes; };
    channel query_log {
      file "query.log" versions 10 size 50M; severity debug; print-time yes;
    };
    category general       { syslog; };
    category security      { security; };
    category queries       { query_log; };
    category dnssec        { security; };
    category default       { syslog; };
    category resolver      { syslog; };
    category client        { syslog; };
    category query-errors  { query_log; };
    category edns-disabled { syslog; };
};

managed-keys {
  "." initial-key 257 3 8      "AwEAAcFN/moqnq1SxdGnZW9JigGYgmFx5WN68RKJ90Je
                                61LJVXi8pKFRz+rajcAu7g7hb0o56RGShWkIWJAosOGr
                                O4onzJ5t+h+rwRNe7EX++KI9XJobzHp+LQiOi/eo2cze
                                91oik9+9Tr+NzyJsssOEq9X/mm9hP44a8YgqSzR+rwCb
                                8jeB9WPhQk25Sp7qN3o8WLCfDxFy6ioOFa7MFirUa1dK
                                30B92X10JN0KA3d0UmOJ3GU50T0WSbWq15k28qX0et/M
                                W3Wl3T5CclG0goM19ET0iQPJh4mN3Gdw9bHqDyiPsqeP
                                MRJNzjb+EgK+MOp3eAqbQ1hlnr/ruwOMKz6oEIYviec8
                                QWvJShmeY4+rRCL5lzZuQ7AYOQq8QO4jUnqTe2/t/Oqy
                                o6yqOtiwsLjmgnsK11qZojJ9RSzjb1r5D5Icl5UHGNxG
                                O6/CQYJMHFsyksYzc1Brtg1PdyHc1zy/GFNI6QEQzlhQ
                                H1nTa+F2MnoZp3k1Z6PTh+GU1796jNtUhMgy4pN7dOqC
                                35hP3GzaP3/XkXtzWbpZRgcXNahbiKso8eqt3r45MCsK
                                AsW3r3hg+CzujTOjVBveKhGfb3nQvx702IdW6Jy62HWX
                                TvipTuG1Kqw+zWHofR2P0ugAszYQBL7G3Zo45VtEScwP
                                XaWavSbGNnHJH5OQQT6HnDvF8hZP";
};

zone "." {
        type hint;
        file "root.hint";
};

The managed-key is the DNSKEY record of our private root-zone. We get that record with the query:

shell> dig @192.0.2.53 . DNSKEY | grep 257

This will request the DNSKEY record set from the private root-server, the grep will filter out the key-signing-key (KSK) with the flag field of "257"3.

The file root.hint will contain the root-hints (NS records and A/AAAA records of our authoritative DNS servers for the private root-zone):

shell> dig @192.0.2.53 ns .

; <<>> DiG 9.9.4-P1 <<>> @192.0.2.53 ns .
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3958
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 2
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;.                              IN      NS

;; ANSWER SECTION:
.                       518400  IN      NS      a.myroot-servers.loc.

;; ADDITIONAL SECTION:
a.myroot-servers.loc.   86400   IN      A       192.0.2.53

;; Query time: 15 msec
;; SERVER: 192.0.2.53#53(192.0.2.53)
;; WHEN: Sun Dec 22 15:06:50 CET 2013
;; MSG SIZE  rcvd: 77

This data can be redirected into the file:

shell> dig @192.0.2.53 ns . > root.hint

Next, we check that the BIND configuration is error free …

shell> named-checkconf -z

and if no error is shown, we start the BIND DNS Server:

shell> named
shell> tail /var/log/syslog
Dec 22 18:19:13 raspidev named[2384]: automatic empty zone: 9.E.F.IP6.ARPA
Dec 22 18:19:13 raspidev named[2384]: automatic empty zone: A.E.F.IP6.ARPA
Dec 22 18:19:13 raspidev named[2384]: automatic empty zone: B.E.F.IP6.ARPA
Dec 22 18:19:13 raspidev named[2384]: automatic empty zone: 8.B.D.0.1.0.0.2.IP6.ARPA
Dec 22 18:19:13 raspidev named[2384]: command channel listening on 127.0.0.1#953
Dec 22 18:19:13 raspidev named[2384]: command channel listening on ::1#953
Dec 22 18:19:13 raspidev named[2384]: managed-keys-zone: loaded serial 9
Dec 22 18:19:13 raspidev named[2384]: all zones loaded
Dec 22 18:19:13 raspidev named[2384]: running

Now, on the caching server, we should be able to validate out own root zone (watch for the AD-Flag):

; <<>> DiG 9.9.4-P1 <<>> @localhost soa . +adflag +m
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13316
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;.                      IN SOA

;; ANSWER SECTION:
.                       86327 IN SOA a.myroot-servers.loc. nstld.verisign-grs.com. (
                                2013122200 ; serial
                                1800       ; refresh (30 minutes)
                                900        ; retry (15 minutes)
                                604800     ; expire (1 week)
                                86400      ; minimum (1 day)
;; AUTHORITY SECTION:
.                       518274 IN NS a.myroot-servers.loc.

;; Query time: 3 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Dec 22 15:13:02 CET 2013
;; MSG SIZE  rcvd: 118

And we should also be able to validate other data out in the Internet, like the domain name for this blog (dnsworkshop.de or dnsworkshop.org):

; <<>> DiG 9.9.4-P1 <<>> dnsworkshop.de @localhost
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60851
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;dnsworkshop.de.                        IN      A

;; ANSWER SECTION:
dnsworkshop.de.         7200    IN      A       91.190.147.212

;; AUTHORITY SECTION:
dnsworkshop.de.         7200    IN      NS      ns2.myinfrastructure.org.
dnsworkshop.de.         7200    IN      NS      ns1.myinfrastructure.org.

;; Query time: 1230 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Dec 22 15:14:16 CET 2013
;; MSG SIZE  rcvd: 115

And we can also validate our augmented .loc TLD that does not exist in the public Internet:

; <<>> DiG 9.9.4-P1 <<>> @localhost a.myroot-servers.loc +adflag
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15859
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;a.myroot-servers.loc.          IN      A

;; ANSWER SECTION:
a.myroot-servers.loc.   86400   IN      A       192.0.2.53

;; AUTHORITY SECTION:
loc.                    86399   IN      NS      a.myroot-servers.loc.

;; Query time: 94 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Dec 22 18:22:33 CET 2013
;; MSG SIZE  rcvd: 79

Voila, a DNSSEC signed local root-zone.

The root-zone is a busy place right now. New TLDs are added all the time. Make sure you follow these changes. A good way to get notice of updates in the root zone is to follow @diffroot on Twitter. Please remember, if you followed this tutorial, you need to add the changes using nsupdate, do not use a text editor on the zone files!

Footnotes:

1] I do not recomment hosting a private TLD, it is much easier and less error-prone to run a private DNS delegation on the second or third level of the DNS hierachy, such as private.example.com.

2] Be sure to use the double >> to append to the file, a single > will override the zonefile with the keys. Not what we want.

3] Always double check the key data, there is a slight chance that the sequence "257" will appear in the key material of the ZSK as well!

Other posts
Creative Commons License
strotmann.de by Carsten Strotmann is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License .