2017-02-11

The DIY root server: Be Your Own Dot

Previously, we have worried about setting up a secure mechanism for running a DNSCurve-enabled recursive DNS resolver and a DNSCurve-enabled authoritative DNS server. Both of these are important, since you need to have both (a) software to encrypt your DNS queries and it needs to be compatible with (b) software to encrypt DNS responses.

If you recall from those earlier tutorials, every DNS lookup starts with a query to the root servers. But what are "the root servers" exactly?

To put it simply, the root servers are thirteen machines that know where every top-level domain (TLD) lives. There used to be a few TLDs: the main ones (.com, .net, .org, .gov, et cetera) and then a few dozen more for each country (.us, .uk, .jp, .de, and so on). In the last few years ICANN (the people in charge of that decision) have opened up the TLD namespace to just about every durn thing you can imagine. There are over 1500 TLDs now and more are constantly being added. If you thought the Internet was over-commercialized before, now we have a .mcdonalds and a .americanexpress. There isn't a .popeyes yet, but there's a .church. I don't think that one's for chicken but I could be wrong.

This all came about because Internet domains are, for convenience (if not necessity), hierarchical. When you want to go to www.youtube.com, you want to go to the one true YouTube, not some other random website that might call itself "youtube" and has gamed a search engine to make it the top result. So there needed to be a system that provides this incontrovertible consensus that youtube.com is, for everyone, everywhere, the domain with the cat videos and video game playthroughs you know and love. Hence, there must be a central source of truth, and that's where the root servers come in.

Root servers don't know where www.youtube.com is, but they are the definitive source of truth for how to reach the .com servers. And those .com servers are the definitive source of truth for youtube.com, and so on down the line until you get to where the cat videos are. Your recursive DNS resolver needs to ask these questions and trust the answers it receives. This is a good thing, because you want your cat videos. This is a bad thing because if someone wants to deprive you of cat videos, they can manipulate the domain name responses to keep you from them. Or to give you the wrong cat videos, which is somehow even worse. Unless, of course, you're running DNSCurve end-to-end.

First, the bad news: at present, this isn't possible. If you set up a DNSCurve-enabled resolver and a DNSCurve-enabled authoritative DNS server, you have encrypted the DNS traffic between you and the last set of machines you need to reach. But those darn root servers aren't running DNSCurve.

Now, the good news: you don't need the root servers, not exactly. What you need is the service that the root servers provide, which is that mapping from top level domains to nameservers that can answer questions for those domains. And if you can get encryption and authentication on top of that, it would be a lot better than depending on unencrypted root server queries.

So how do you get TLD data without asking the root servers? Easy! Make a copy of the Internet.

OK, maybe not the whole thing, but at least you can have a copy of the top level domain DNS database. It's freely available, and in one easy location, too:

https://www.internic.net/domain/root.zone.gz

There's an uncompressed version available too, but hey, packets ain't free, kids.

The really useful thing about this zone file is that it's cryptographically signed. This means that if you verify the data against its signature, you can start to have confidence that the root data hasn't been altered by a third party.

With your own (signed) copy of the root zone, you automatically know where all the .com servers are, all the .org servers are, and so on. You just need a way to make this data accessible to your recursive resolver.

We've already developed the skills that we need to do this. We just need to create one additional authoritative DNS server that we can pretend to be a root server to our recursive resolver.

If you use tinydns, this is going to be pretty simple. Set it up on a loopback IP address on the same machine as your recursive resolver. Let's use 127.53.0.1.

  1. Create a new group and users for your new DNS root service and for your root zone fetching function:

    sudo groupadd _dnsroot
    sudo useradd -d /dev/null -s /usr/bin/false -g _dnsroot _dnsroot
    sudo useradd -d /dev/null -s /usr/bin/false -g _dnsroot _dnsrootlog

    sudo groupadd dnsgpg
    sudo useradd -d /home/dnsgpg -m -s /bin/sh -g dnsgpg dnsgpg

    I like to use a dedicated dnsgpg user account to hold the GnuPG keyring.

  2. Configure tinydns. If you installed djbdns-1.05 as per the recursive resolver howto you will already have tinydns-conf on your system:

    sudo tinydns-conf _dnsroot _dnsrootlog /etc/dnsroot 127.53.0.1

    This will make a new service directory for you, so you can switch to the default /etc/dnsroot/root directory and set up your new zone. Note that /etc/dnsroot/root is the directory which is chrooted into when the tinydns service runs and has nothing to do with our using it for the root zone. There are a lot of things we're calling root here, so I want to avoid confusion.

  3. Install GnuPG. Odds are really good that you either have this installed already or you can add it very quickly. This depends on your operating system and may require you to add the necessary package. A by-no-means complete list of commands that may help:

    FreeBSD: sudo pkg install gnupg20

    OpenBSD: doas pkg_add gnupg

    Debian/Ubuntu: sudo apt-get install gnupg2

    (Dear OS maintainers, please standardize on this nonsense. Thanks.)

  4. Get the root zone signing key. This is going to change over time because the people who are in charge of that decision believe in regular key rotation and don't believe in subkeys. There are pros and cons to this, but oh well. The folks who own the root zone have opted to use a 1024-bit DSA key. This isn't considered to be long-term secure anymore, but it's all we've got. We have to roll with the decisions of the key owners.

    Until March 2018, the root zone is signed by the following GnuPG ID: "Registry Administrator <nstld@verisign-grs.com>". This ID can be sought on a keyserver of your choice.

    The fingerprint for this key is: F0CB 1A32 6BDF 3F3E FA3A 01FA 937B B869 E3A2 38C5

    You have to install this cert locally before you can use it:

    sudo su -l dnsgpg
    whoami
    # Confirm you are the dnsgpg user
    pwd
    # Confirm you are in the dnsgpg user's home directory
    gpg2 --keyserver pgpkeys.mit.edu --recv-key E3A238C5
    gpg2 --list-keys --fingerprint
    # Verify the certificate is installed
    exit
    whoami
    # Confirm you are no longer the dnsgpg user

  5. Convert the root zone into tinydns-data format. Dan Bernstein figured out how to do this a long time ago and put his notes together about how he did it with the now-defunct ORSC root database. His method involved doing an AXFR and cleaning up the result. We already have the zone file, since we fetched it over HTTPS and will verify its correctness with GnuPG.

    I made a slightly modified version of Dan Bernstein's cleanup script that works with the InterNIC root.zone file format and I am making it publicly available here for the first time.

    cd
    wget -N https://su.bze.ro/software/dnsroot
    chmod 0755 ./dnsroot
    sudo mv ./dnsroot /etc/dnsroot/root/

    If you look at my dnsroot script, you'll notice one of the first lines looks like this:

    echo .::127.53.0.1

    This line tells tinydns that the "." domain is controlled by 127.53.0.1, which we also specified when we ran tinydns-conf. This IP address must match in both locations, so if you change the IP address on which tinydns will run, make sure this line in the dnsroot script always matches what's in /etc/dnsroot/env/IP.

  6. Modify the permissions of the /etc/dnsroot/root directory to allow the dnsgpg user account to edit files there:

    sudo chown dnsgpg.dnsgpg /etc/dnsroot/root

  7. Fetch the latest root.zone.gz file and its GnuPG signature, and check that they match.

    cd /etc/dnsroot/root
    sudo su dnsgpg --command ' \
      wget -N https://www.internic.net/domain/root.zone.gz && \
      wget -N https://www.internic.net/domain/root.zone.gz.sig && \
      gpg2 --status-fd 1 --verify /etc/dnsroot/root/root.zone.sig \
    '

    Check that the GnuPG signature is valid. The output should contain something along the lines of:

    Good signature from "Registry Administrator <nstld@verisign-grs.com>"

  8. If the signature is good, extract the data from the .gz file and run the dnsroot script:

    cd /etc/dnsroot/root
    sudo su dnsgpg --command ' \
      gzip -d -c < root.zone.gz > root.zone.tmp && \
      mv -f root.zone.tmp root.zone && \
      ./dnsroot < root.zone > ./data.tmp && \
      mv -f ./data.tmp ./data && \
      make
    '

  9. Confirm that you have a valid ./data and ./data.cdb file in /etc/dnsroot/root. Check that ./data starts with your ".::127.53.0.1" line, and check that the data.cdb reports valid data when you query it with tinydns-get.

    cd /etc/dnsroot/root
    head ./data
    # Confirm .::127.53.0.1 and other tinydns-data lines for the TLDs
    tinydns-get ns .
    tinydns-get ns com
    tinydns-get ns net
    tinydns-get ns org
    tinydns-get ns kp
    # Confirm that these domains return NS records

  10. If you grep the root.zone file for the IP addresses you get in the responses, they should match. Because of the way the BIND zone file format works, you'll have to do this twice. For a specific IP address:

    grep ip.ad.dr.ess root.zone
    # Take the nameserver string from these response and grep for it
    grep "nameserverstring" root.zone

    You should get an A record and an NS record out of root.zone. This will map a domain name to a nameserver and a nameserver to an IP address. So, for example:

    $ tinydns-get ns kp
    2 kp:
    74 bytes, 1+0+2+0 records, response, noerror
    query: 2 kp
    authority: kp 259200 NS 175.45.176.16
    authority: kp 259200 NS 175.45.176.15

    Let's use 175.45.176.15 as the IP we want to check.

    $ grep 175.45.176.15 root.zone
    ns1.kptc.kp.            172800  IN      A       175.45.176.15

    Now we look for "ns1.kptc.kp." from this result and check that it maps our original TLD nameserver query to the IP address we picked.

    $ grep ns1.kptc.kp. root.zone
    kp.                     172800  IN      NS      ns1.kptc.kp.
    ns1.kptc.kp.            172800  IN      A       175.45.176.15

    Note that our ./data file eliminates the intermediate nameserver name. It isn't essential for tinydns.

  11. When you're confident that your personal root server is working, you can update your dnscache instance to use it:

    cd
    echo 127.53.0.1 > ./@.new
    chmod 0644 ./@.new
    sudo mv ./@.new /service/dnscache/root/servers/
    cd /service/dnscache/root/servers
    sudo cp -p ./@ ./@.backup
    sudo mv -f ./@.new ./@
    sudo svc -t /service/dnscache

    Check that your dnscache instance now reports that the root server is 127.53.0.1. So for example:

    $ dnsqr ns .
    2 .:
    40 bytes, 1+1+0+0 records, response, noerror
    query: 2
    answer: . 259200 NS 127.53.0.1

    Try using the Internet. Tail your dnscache log file and see if you can resolve the domains you'd normally expect to reach: google.com, bbc.com, internic.net (that's an important one), and so forth.

    If you have trouble with your new root server, restore /service/dnscache/root/servers/@ from the backup you made and restart dnscache.

  12. The root zone changes over time, so perform these "fetch, validate, and convert" steps regularly. The zone file is updated and signed once a day, so anything more frequent than that is unhelpful. Once a week is good enough for my day-to-day network needs, so adjust your fetching frequency accordingly. The dnsgpg user's crontab might be useful here. I have a dnsroot-fetch script I use that does the above fetch-and-run-dnsroot steps:

    cd
    wget -N https://su.bze.ro/software/dnsroot-fetch
    chmod 0755 ./dnsroot-fetch
    sudo mv dnsroot-fetch /etc/dnsroot/root/
    sudo su -l dnsgpg
    crontab -e
    # Add this line (without the leading '#'):
    #0 18 * * * (cd /etc/dnsroot/root/; perl -le 'sleep 300 + int rand 500'; ./dnsroot-fetch)

    The Perl section is optional. I like to add the perl command to sleep for a random amount of time more than 5 minutes and less than 13 minutes. This is just a little window to be a good bot and avoid traffic spikes of other scripts that run at the top of the hour or at a quarter past.

I've been hosting my own copy of the root zone and using it as my private root server for about six years and so far, it's working out pretty well.

"But gee," you're probably wondering. "Where's the encryption you've been going on about?" Fair question. If you're going to run this private root server on the same machine as your recursive DNS resolver, you don't really need to put a CurveDNS proxy in front of it. It's going to be consumed by the same host on a loopback address and so there aren't many places a malicious attacker can interfere with this data in transit unless they've already compromised the machine. That's the point of using the "127.53.0.1" address that keeps popping up in our configs and scripts.

You can host a public copy of the root zone, though. You know how to do this based on everything we've learned thus far. You can: (a) create a private copy of the root zone based on these steps and then (b) put a CurveDNS proxy in front of it to make it publicly available. The only thing that needs to be adjusted is the IP address of that echo .::127.53.0.1 line in your dnsroot script. Since this line is the address that is treated as the one root server in the universe, you need to adjust this to be the public IP address of your public DNSCurve-enabled root server.

You can still host the root on 127.53.0.1 of course. You'd still set "127.53.0.1" to be the value of /service/curvedns/env/FORWARDIP, but for the /etc/dnsroot/root/data file, it needs to report a reachable, public IP address. There may not be a good way for your server to be able to predict its public IP address, especially if it's using a NAT configuration, so adjust your local ./dnsroot script accordingly to update the line for the "." domain. If you want to be thorough, you can also set up an axfrdns service to handle TCP queries. By default, tinydns only supports UDP queries. curvedns will handle both UDP and TCP requests, but axfrdns needs to be set up to receive the TCP queries that curvedns will forward to it. But I'm getting ahead of myself.

Next time: putting all of this together.

No comments: