4 Nov 2017

DNS Hosting Guide: Hidden Master with DNSSEC

Manage your own DNS using BIND in a hidden master configuration.

DNS is one of the few things I don't like to host myself. A DNS server running on a single host will cause slow queries for far-away clients, making your site seem less responsive. In addition, a failed DNS lookup is a much more serious problem than a web service being down. If your mail server isn't responding, most other mail servers will queue all your incoming mail to be retried later. But if the DNS lookup for your MX record fails, they will usually reject the message altogether.

Because of these issues, I have always used third-party DNS providers. Most professional DNS services are inexpensive for personal use, resistant to DDOS attacks and downtime, and support low-latency queries from anywhere in the world using anycast routing. However, using a paid DNS provider usually forces you to manage all your domains and subdomains through a clunky web interface. Until now!

This guide describes how to run the BIND DNS server in a hidden master configuration. The idea is that you run BIND locally, where you can easily edit your plain text zone file or automate your domain configuration, but use a secondary, more resilient DNS provider for all the actual query resolution. Your local BIND server will act as the primary (or master) DNS server, and will automatically notify your secondary DNS servers when any changes are made. The trick is that when setting your primary nameservers in your domain's registrar, you only use the IP addresses of your secondary nameservers. This way, you can easily manage your zone file locally, but none of the actual query traffic goes to your box.

I use DNS Made Easy for my secondary DNS service. Their small business plan is about $30 per year, and that gets you support for 10 domain names, 5 million queries per month, and DNSSEC. I've found it more than adequate for a personal site, but there are other options as well—Dyn comes to mind (but recently acquired by Oracle...buyer beware).

Installing BIND

As always, this guide assumes you're running FreeBSD. The instructions should map pretty closely to your Linux distro of choice—just install bind from your package manager and modify the file paths where appropriate.

On FreeBSD, install bind99 from ports. There are more recent versions available, but in my experience they all had bugs in their rc scripts and weird runtime issues. 9.9 is the extended support release, and it's the only one that I can confirm works 100% on FreeBSD 11. Use other versions at your own risk.

EDIT (26 Nov 2017): These issues were actually caused by a problematic loader.conf optimization I had configured. You can use BIND 9.11 without issue. Just make sure the net.inet.tcp.soreceive_stream tunable is set to 0. See this forum post for more information.

cd /usr/ports/dns/bind911
make install clean

I always make sure to build with the IPV6- and DNSSEC-related options. Then, enable bind to start on boot (the daemon is called named):

/etc/rc.conf
named_enable="YES"

Now you are ready to edit the configuration file. The annotated example below assumes you are hosting a domain called example.com. Most of the IP addresses in the example are fake. You will need to substitute your own domain and relevant IP addresses where appropriate. Your secondary DNS provider should provide you with the IP addresses for the zone transfers, as well as those of your public nameservers.

/usr/local/etc/namedb/named.conf
// IP addresses of your secondary nameservers, where the zone transfers will // take place. Your DNS provider should give you these addresses. // NOTE - these IPs are probably not the same as your DNS provider's public // nameservers (which will go in your zone file's ns1-3 A records). masters secondaries { 198.51.100.51; 198.51.100.52; 198.51.100.53; }; // same as above - repetition needed for BIND's arcane config syntax acl secondaries { 198.51.100.51; 198.51.100.52; 198.51.100.53; }; // localhost, and any other public IPs of your server acl localnetworks { 127.0.0.1; ::1; 203.0.113.41; 203.0.113.42; 2001:db8::2; 2001:db8::3; }; options { directory "/usr/local/etc/namedb/working"; pid-file "/var/run/named/pid"; dump-file "/var/dump/named_dump.db"; statistics-file "/var/stats/named.stats"; key-directory "/usr/local/etc/namedb/keys"; // listen on all interfaces (needed for zone transfers to secondaries) listen-on { any; }; listen-on-v6 { any; }; // IP addresses of your upstream DNS servers. Your hosting provider most // likely provides a DNS server. Alternatively, leave blank to have BIND // recursively resolve all queries (slow). // // Note - below are Google's DNS servers. If you value your privacy, don't // use them. forwarders { 8.8.8.8; 8.8.4.4; 2001:4860:4860::8888; 2001:4860:4860::8844; }; // If a lookup fails on upstream DNS servers, don't try to recursively // resolve (comment out if not using forwarders). forward only; // When a zone is updated, only send NOTIFY to hosts in the zone's // `also-notify` block (defined below). notify explicit; // Set safe default permissions. We will override these for some zones // below. allow-transfer { none; }; allow-update { none; }; allow-recursion { localnetworks; }; allow-query { localnetworks; }; // If your server has multiple IP addresses, uncomment the two lines below // and change the source address to the one you configured your secondary // DNS provider to use. // query-source address 203.0.113.41; // notify-source 203.0.113.41; // Comment out the three lines below if you don't want DNSSEC. dnssec-enable yes; dnssec-validation auto; dnssec-lookaside auto; }; // Don't attempt to resolve any private IPs include "/usr/local/etc/namedb/localzones.conf"; // Your domain name goes here. zone "example.com" in { // We are the "master" (or primary) server for our domain, but it just // happens that we won't be getting any external queries. type master; // Comment out the two lines below if you don't want DNSSEC. auto-dnssec maintain; inline-signing yes; // only allow zone transfers from localhost or our secondary DNS provider allow-transfer { localnetworks; secondaries; }; // only allow DNS queries from localhost or our secondary DNS provider allow-query { localnetworks; secondaries; }; // send NOTIFY messages to secondary DNS provider when the zone changes also-notify { secondaries; }; // your domain's zone file goes here file "/usr/local/etc/namedb/master/example.com.db"; };

Now you need to write a zone file for your domain—this file contains the actual DNS records.

Optional: Configuring DNSSEC

If you want to configure DNSSEC for your domain, you'll need to generate some keys. Make sure your secondary DNS provider supports DNSSEC first (I know that DNS Made Easy does). I use the ECDSA algorithm when generating keys, since they are smaller and more computationally efficient. However, if you're concerned about maximum compatibility with other DNS resolvers, you probably want to stick with RSA—just replace ECDSAP256SHA256 with RSASHA256 below. ECDSA is good enough for Cloudflare though, so I'm sticking with it for this example.

First, generate a Zone Signing Key (ZSK) for your domain by running the following:

dnssec-keygen -a ECDSAP256SHA256 -K /usr/local/etc/namedb/keys -n ZONE example.com
Generating key pair.
Kexample.com.+013+29679

This will generate a keypair for the example.com in BIND's key directory. Next, generate a Key Signing Key (KSK) for the domain in a similar fashion:

dnssec-keygen -f KSK -a ECDSAP256SHA256 -K /usr/local/etc/namedb/keys -n ZONE example.com
Generating key pair.
Kexample.com.+013+15315 

Take note of the KSK's generated filename—you'll need it in a bit. You should now have 4 files for this domain in BIND's key directory: one pair for the ZSK, and another pair for the KSK. We will come back to these at the end of the guide when you configure DNS settings at your registrar.

Writing a Zone File

Now we come to the fun part: writing your zone file! This is a plain text file where you'll specify all the DNS records for your domain. The below example uses fake IP addresses for a domain named example.com. I've included some commentary to help you out, but basically you'll just be translating the existing records you configured in your original DNS provider's web portal.

/usr/local/etc/namedb/master/example.com.db
; The $TTL variable defines a default TTL for all records in this file. ; This value specifies how long other DNS servers should keep a record in ; their cache. Individual records may override this value. $TTL 3h ; your bare domain name $ORIGIN example.com. ; "Start of Authority" record. First line should contain your primary ; nameserver and administrative contact's email (replace '@' with '.') @ IN SOA ns1.example.com. root.example.com. ( ; serial: you *must* increment this number whenever any change is made ; to this file, otherwise updates will not propagate to your ; secondary DNS servers 2017101800 ; refresh: how often your secondary DNS servers should poll your primary ; server for changes 1d ; retry: how long your secondary DNS servers should wait before ; retrying after a failed update 3m ; expire: how long your secondary DNS servers should be considered ; authoritative if your primary nameserver disappears 1w ; minimum: "negative caching TTL," or how long other servers should wait ; before re-querying a record that didn't exist on the previous ; attempt 3h ) ; Your domain's public nameservers go in the NS records here. You'll need ; an A record for each one that points to your secondary DNS provider. IN NS ns1.example.com. IN NS ns2.example.com. IN NS ns3.example.com. ; MX record (if you have a mail server) IN MX 10 mail.example.com. ; server host definitions @ IN A 203.0.113.41 @ IN AAAA 2001:db8::2 mail IN A 203.0.113.42 mail IN AAAA 2001:db8::3 awesomebox IN A 203.0.113.43 awesomebox IN AAAA 2001:db8::4 ; These records should contain the IP addresses of your secondary DNS ; provider's PUBLIC nameservers. Clients will use these DNS servers when ; querying your domain. ns1 IN A 198.51.100.11 ns1 IN AAAA 2001:db8:beef::7 ns2 IN A 198.51.100.12 ns2 IN AAAA 2001:db8:beef::8 ns3 IN A 198.51.100.13 ns3 IN AAAA 2001:db8:beef::9

Configuring Zone Transfers

At this point, we're done configuring BIND. The next step is to ensure your secondary DNS provider can connect to your server to perform zone transfers. You'll need to configure your server as the primary DNS server in your secondary DNS provider's web portal. With DNS Made Easy, this page is found in the Advanced menu under Secondary IP Sets. Specify your server's public IP address here.

If you have a firewall in place, you'll need to allow TCP and UDP traffic over port 53. If you used my FreeBSD Server Guide to configure the PF firewall, you can just add domain to the inbound_tcp_services and inbound_udp_services variables. Be sure to reload PF's ruleset:

pfctl -f /etc/pf.conf

Now, the moment of truth. It's time to start BIND and test your new DNS server!

service named start

Check /var/log/messages to ensure BIND started up properly. Hopefully you didn't make any typos. You can verify BIND is working by doing a simple DNS query:

dig @127.0.0.1 +short google.com
172.217.5.206

Also, make sure BIND is correctly serving the domain you configured in your zone file:

dig @127.0.0.1 +short example.com
203.0.113.41

Now, head to your secondary DNS provider's web portal. When BIND started up, it should have read your zone file and sent a NOTIFY to your secondary DNS servers, informing them to do a zone transfer from your hidden master. If that happened successfully, your provider's web portal should show the DNS servers as in sync, with the current serial numbers matching.

If that didn't happen, or you see a problem, increment the serial number in your zone file and give BIND a reload:

service named reload

This should re-read your zone file and update your secondary servers. Check for any suspicious messages in your log files.

Once you have zone transfers working, and your primary and secondary nameservers are in sync, you're ready to officially change your public nameservers at your domain's registrar.

Notifying Your Registrar

Your registrar has the secret sauce that tells other DNS servers which nameservers to query for information about your domain. Once you've verified everything is working, you can switch your nameservers over to your secondary DNS provider. You probably want to do this late at night, or during a time when you don't expect much traffic to your site. I use Namecheap, but the instructions should carry over to most other registrars.

In your registrar's web portal, your domain's settings page should have an option to set your nameservers. At Namecheap, you want to select Custom DNS. You will then need to provide the fully qualified domain name of your secondary DNS provider's public DNS servers. As DNS Made Easy states at the top of their portal: These are the name servers that you will want to add as NS records in your zone and also assign to your domain at your domain registrar.

Side Note: Vanity Nameservers

If you'd like your public nameservers for example.com to look like ns1.example.com instead of ns1.yourdnsprovider.com, then you can configure "vanity nameservers" for your domain. First, make sure you have A records (and probably AAAA records) for your secondary DNS provider's public name servers in your zone file (I emphasized this in the example zone file above).

I can't speak for other registrars, but at Namecheap, you can go to the Advanced DNS page for your domain and scroll down to Personal DNS Server. Then you can add ns1, ns2... and map them to the IP addresses of your secondary DNS provider's public nameservers.

Then, in the Custom DNS field, you can just put ns1.example.com, ns2.example.com, etc.

Doing this creates a "glue record" at your registrar for your domain's DNS servers. You can Google this if you're interested in the details. Also, the web interface at Namecheap currently only allows IPv4 glue records. I had to open a support ticket to have IPv6 glue records added for DNS Made Easy's public IPv6 servers.

Optional: DNSSEC at the Registrar

If you configured DNSSEC above, there is one last step to complete while you're on your registrar's web portal. You'll need to provide the SHA-1 and SHA-256 digests of the KSK you generated to your registrar. Your registrar will then add some fancy records for you so that other resolvers can cryptographically verify your DNS records. (Sorry for the hand-wavy explanation—it's 1:00 AM on a Friday night and I'm tired.)

Remember the filename I asked you to remember from the dnssec-keygen command above? We'll use it now. You want the file containing the Key Signing Key (not the Zone Signing Key). It's annoying, because the filenames look exactly the same except for a four-digit identifier at the end. It's easy to figure out though—the contents of the .key file will tell you which one it is.

Once you've found the correct file, run the following command:

dnssec-dsfromkey /usr/local/etc/namedb/keys/Kexample.com.+013+15315.key | awk '{print $4, $5, $6, $7}'
15315 13 1 7F5ED13547D9860965437D0F7CB6BFA7C70F1F62
15315 13 2 AD0C012F749F0422E0CC88D11B65248E78945F149572D54C8AE7C5B63C30E621

You will use this output to populate the DS records for your domain at your registrar. At Namecheap, this is located under Advanced DNSDNSSEC. The first column is the key tag, which corresponds to the key's file name. The second column is the encryption type. If you used ECDSA, this will be 13. If you went with RSA, you'll use 8 here. The third column is the digest type: 1 for SHA-1 and 2 for SHA-256. The final column contains the actual digest.

These columns correspond to the four fields for DS records in the Namecheap web portal. Copy and paste the appropriate values there. Other registrars should have a similar process.

It will take an hour or so for your DNS updates to propagate. You can verify DNSSEC is working by by querying a different DNS server (the below example uses Google's public DNS).

dig @8.8.8.8 example.com +dnssec | grep -m1 flags:
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

If you see the ad flag, then your DNSSEC was validated successfully. You can also check your DNSSEC status on Verisign's test page.

Conclusion

Once you've gotten all this working, you might consider setting the default nameserver on your server to your local BIND instance. In addition to using no network overhead, you'll also take advantage of BIND's default query cache. All it takes is a simple edit to your resolv.conf:

/etc/resolv.conf
nameserver 127.0.0.1 nameserver ::1

If you perform DNS updates frequently, I think you'll find the hidden master setup an invaluable productivity boost for any domains you control. Remember: to update your zone, just make the necessary record changes in your zone file, increment the serial, and issue a service named reload.