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.

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.

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.

You will use this output to populate the DS records for your domain at your registrar. At Namecheap, this is located
under Advanced DNS→DNSSEC. 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).

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.

As always, contact me via email or ping me on Twitter
with any questions or issues you have.