Introduction

This post is about configuring a DNS infrastructure for the internal network using the two daemons nsd(8) and unbound(8). Both daemons are part of the OpenBSD base system, you don’t need to install any packages.

NSD will be the authoritative name server for the internal DNS zone. Unbound will provide name resoultion services for all hosts in the internal network (including caching). If possible answers from external name servers will be validated using DNSSEC. The environment will consist of:

  • One instance of nsd(8) being the master server for the internal zone(s)
  • Two instances of nsd(8) being the slave servers for the internal zone(s)
  • Two instances of unbound(8)

You can distribute these five instances on any number of servers between one (not recommended at all) and five (the easiest thing to do). This post will show you how to do it with three servers. Why three? Because NSD and Unbound both use port 53/udp and 53/tcp, so I need a concept to run both daemons on the same server.

The big picture

Preparation

In this post I presume that three server are used: the NSD master runs on one server while the two other servers run each a pair of NSD and Unbound. To run NSD and Unbound on the same server you either assign NSD an unusual port on the same IP address Unbound is using or you assign a dedicated IP address to NSD. Here I will do it with dedicated IP addresses. Further I presume that all servers run OpenBSD and you have made yourself comfortable on all systems.

This table shows you the assignment of roles, hostnames and IP addresses I’m using in this post:

Role Hostname IP address
Master (NSD) dns.example.com 192.0.2.10
Resolver 1 (Unbound) dns1.example.com 192.0.2.11
Resolver 2 (Unbound) dns2.example.com 192.0.2.12
Slave 1 (NSD) dns3.example.com 192.0.2.13
Slave 2 (NSD) dns4.example.com 192.0.2.14

Master server with NSD

All equired configuration files for NSD are in the folder /var/nsd/etc/. The following subsections show all required files.

ACL key file

First, let’s setup the required keys for the communication with the slave servers. You can use a manual key by issuing the following command:

# echo "secret: $(echo "MyNotSoSecretKey" | sha256 -b)" > /var/nsd/etc/nsd-acl.key

A better way is to generate a long, random key which you never see in clear text. This script will generate such a key:

genkey.sh

#!/bin/sh

s=$(printf \\$(printf '%03o' $(($RANDOM/255))))
i=31
while [ $i -gt 0 ] ; do
        s=$s$(printf \\$(printf '%03o' $(($RANDOM/255))))
        i=$((i-1))
done
echo "secret: $(echo $s | sha256 -b)" > /var/nsd/etc/nsd-acl.key

Configuration file

The man page nsd.conf(5) contains a complete description of the format and settings in the configuration file. Edit the configuration file /var/nsd/etc/nsd.conf:

The first section contains some general settings. The most important one is the IP address NSD will bind to.

server:
        ip-address: 192.0.2.10
        server-count: 1
        hide-version: yes
        zonelistfile: "/var/nsd/db/zone.list"

In order to use nsd-control(8) you have to enable it in the configuration. This is done by the following lines:

remote-control:
        control-enable: yes

The next section contains the ACL key you’ve created before. The key itself is stored in a separate file just in case you want to apply different security policy to the two files.

key:
        name: "dns.example.com"
        algorithm: hmac-sha256
        include: "/var/nsd/etc/nsd-acl.key"

NSD comes with a handy feature called patterns. This allows you to summarise common settings for zones, give them a name and refer to this name in the zone sections of the configuration file. The following pattern summarises the required settings for the slave servers.

pattern:
        name: slaves
        outgoing-interface: 192.0.2.10
        notify: 192.0.2.13 dns.example.com
        notify: 192.0.2.14 dns.example.com
        provide-xfr: 192.0.2.13 dns.example.com
        provide-xfr: 192.0.2.14 dns.example.com

Finally, it is time to define the zones NSD will serve as master server. The example shows the definition of both a forward and a reverse zone. The %s in the zonefile: clause resolves to the name of the zone.

zone:
        name: "example.com"
        zonefile: "/var/nsd/zones/master/%s.dns"
        include-pattern: slaves

zone:
        name: "2.0.192.in-addr.arpa"
        zonefile: "/var/nsd/zones/master/%s.dns"
        include-pattern: slaves

You can download the complete example file here: nsd-master.conf

Zone files

Before you start NSD for the first time you have to create the zone files for the DNS zones mentioned in the configuration file. Each file is named <zonename>.dns and is stored in the folder /var/nsd/zones/master.

First the zone file for the forward zone example.com:

example.com.dns

$ORIGIN .
$TTL 86400      ; 1 day
example.com             IN SOA  dns.example.com. admin.example.com. (
                                1          ; serial
                                86400      ; refresh (1 day)
                                3600       ; retry (1 hour)
                                604800     ; expire (1 week)
                                3600       ; minimum (1 hour)
                                )
                        NS      dns3.example.com.
                        NS      dns4.example.com.
$ORIGIN example.com.
desktop                 A       192.0.2.33
dns                     A       192.0.2.10
dns1                    A       192.0.2.11
dns2                    A       192.0.2.12
dns3                    A       192.0.2.13
dns4                    A       192.0.2.14
router                  A       192.0.2.1
switch                  A       192.0.2.91
server                  A       192.0.2.101

Second the zone file for the reverse zone 2.0.192.in-addr.arpa:

2.0.192.in-addr.arpa.dns

$ORIGIN .
$TTL 86400      ; 1 day
2.0.192.in-addr.arpa    IN SOA  dns.example.com. admin.example.com. (
                                1          ; serial
                                86400      ; refresh (1 day)
                                3600       ; retry (1 hour)
                                604800     ; expire (1 week)
                                3600       ; minimum (1 hour)
                                )
                        NS      dns3.example.com.
                        NS      dns4.example.com.
$ORIGIN 2.0.192.in-addr.arpa.
33                      PTR     desktop.example.com.
10                      PTR     dns.example.com.
11                      PTR     dns1.example.com.
12                      PTR     dns2.example.com.
13                      PTR     dns3.example.com.
14                      PTR     dns4.example.com.
1                       PTR     router.example.com.
91                      PTR     switch.example.com.
101                     PTR     server.example.com.

Starting NSD

The nsd-control utility only works if you setup the required certificates and private keys. These secure the communication between nsd-control and the NSD daemon. Fortunately NSD comes with a script that does the job for you: nsd-control-setup(8):

# nsd-control-setup

Before you run NSD it is advisable to check the configuration. NSD comes with a handy tool called nsd-checkconf(8) for this task:

# nsd-checkconf

If the tool complains about errors fire up your editor of choice and correct them. Now that everything is in place you can enable and start NSD with the following commands:

# rcctl enable nsd

# rcctl start nsd

Slave servers with NSD

The first part in the configuration of the slave servers is to copy over the ACL key and zone files from the master server. The ACL key file goes into the same folder as on the master server. The two zone files are stored in the folder /var/nsd/zones/slave/.

Preparation

Make sure the IP address for NSD is configured on the physical interface. I usually use the primary IP address on the interface for managing the server and add alias IPs dedicated to services:

# ifconfig em0 alias 192.0.2.13 netmask 0xffffffff

Of course you can also configure this address on a dedicated physical interface. Either way you have to make sure the configuration is persistent across device reboots. If you choose to use the alias add the following line to /etc/hostname.em0:

inet alias 192.0.2.13 0xffffffff

Unbound will use a loopback address to query the slave server on the same system because loopback connections are faster than connections to the regular address. For the same reason Unbound will provide its service on the default loopback address 127.0.0.1 to the local system. Because both Unbound and NSD use port 53 you have to add an additional loopback interface on the server. Create the interface with the following command:

# ifconfig lo1 inet 127.0.0.13 netmask 0xff000000

To let OpenBSD automatically create the interface during boot time create the file /etc/hostname.lo1 and add the following line to it:

inet 127.0.0.13 0xff000000

Remember to replace the octet .13 in both addresses by .14 if you configure the second server.

Configuration file

Edit the configuration file /var/nsd/etc/nsd.conf:

The first section contains some general settings. The most important one is the IP address NSD shall bind to.

server:
        ip-address: 192.0.2.13
        ip-address: 127.0.0.13
        server-count: 1
        hide-version: yes
        zonelistfile: "/var/nsd/db/zone.list"

I recommend to turn on remote control on the slave servers too:

remote-control:
        control-enable: yes

The next section contains the ACL key you’ve copied over from the master server.

key:
        name: "dns.example.com"
        algorithm: hmac-sha256
        include: "/var/nsd/etc/nsd-acl.key"

Of course the slave server configuration can also profit from patterns:

pattern:
        name: masters
        outgoing-interface: 192.0.2.13
        allow-notify: 192.0.2.10 dns.example.com
        request-xfr: AXFR 192.0.2.10 dns.example.com

And the slave servers need to know about the zones it is serving:

zone:
        name: "example.com"
        zonefile: "/var/nsd/zones/slave/%s.dns"
        include-pattern: masters

zone:
        name: "2.0.192.in-addr.arpa"
        zonefile: "/var/nsd/zones/slave/%s.dns"
        include-pattern: masters

You can download the complete example file here: nsd-slave.conf

The required steps to start NSD are the same as for the master server.

Resolvers with Unbound

Unbound runs in a chroot similar to NSD. The files required for Unbound configuration are stored in the folder /var/unbound/etc.

Preparation

Make sure the IP address for Unbound is configured on the physical interface. I usually use the primary IP address on the interface for managing the server and add additional IPs dedicated to services as aliases:

# ifconfig em0 alias 192.0.2.11 netmask 0xffffffff

Of course you can also configure this address on a dedicated physical interface. Either way you have to make sure the configuration is persistent across device reboots. If you choose to use the alias add the following line to /etc/hostname.em0:

inet alias 192.0.2.11 0xffffffff

Remember to replace the octet .11 in both addresses by .12 if you configure the second server.

Configuration file

Fire up your editor of choice to edit the configuration file /var/unbound/etc/unbound.conf.

First the general settings for Unbound. Most important are the interface: settings which define the IP addresses Unbound will bind to.

server:
        interface: 192.0.2.12
        interface: 127.0.0.1
        num-threads: 1
        hide-identity: yes
        hide-version: yes
        cache-min-ttl: 3600
        cache-max-ttl: 86400
        prefetch: yes
        rrset-cache-size: 16m
        msg-cache-size: 8m
        statistics-interval: 86400
        statistics-cumulative: yes

The DNSSEC validation in Unbound requires the currently valid key for the root zone in global DNS. Unbound will automatically update the key as it changes. To retrieve this key Unbound needs to know how to reach the authoritative servers for the root zone. These two entries tell Unbound about the root zone servers and where to store the key:

        root-hints: "/var/unbound/db/root.hints"
        auto-trust-anchor-file: "/var/unbound/db/root.key"

Unbound is designed with security in mind. That is an excellent thing, but for the use case in this post I have to tweak a bunch of features in order to make Unbound also resolve internal DNS names:

        unblock-lan-zones: yes
        insecure-lan-zones: yes
        do-not-query-localhost: no

Another security feature of Unbound is to reject answers from public DNS servers which contain private IP addresses. These entries tell unbound which IP ranges are private:

        private-address: 10.0.0.0/8
        private-address: 172.16.0.0/12
        private-address: 192.0.2.0/24
        private-address: 192.168.0.0/16
        private-address: 169.254.0.0/16

But wait, the internal DNS zone delivers private addresses as it is part of a private network. You can solve this chicken-and-egg problem with the following entries:

        domain-insecure: example.com
        domain-insecure: 2.0.192.in-addr.arpa

        private-domain: example.com
        private-domain: 2.0.192.in-addr.arpa

And just to be sure you can define access control entries which tell Unbound from which IP addresses it should accept queries:

        access-control: 127.0.0.0/8 allow
        access-control: 192.0.2.0/24 allow

In order to use unbound-control(8) you have to enable it in the configuration. This is done by the following lines:

remote-control:
        control-enable: yes

Finally Unbound needs to know which server it should query for the internal zones example.com and 2.0.192.in-addr.arpa. Unbound itself cannot host any zones, neither as master nor as slave. The stub-zone sections create the connection to the NSD slave running on the same host:

stub-zone:
        name: "example.com"
        stub-addr: 127.0.0.13

stub-zone:
        name: "2.0.192.in-addr.arpa"
        stub-addr: 127.0.0.13

You can download the complete example file here: unbound.conf

Starting Unbound

You have probably seen that Unbound misses the root hints file /var/unbound/db/root.hints. That is because Unbound comes with built-in root hints. But these may become outdated as entries in the root zone change from time to time. Therefore the use of an up to date root hints file is recommended. You can install the current file by issuing the following command:

# ftp -o /var/unbound/db/root.hints ftp://ftp.internic.net/domain/named.cache

The remote control of Unbound with the unbound-control utility only works if you setup the required certificates and private keys. Fortunately Unbound comes with a handy script that does the job for you: unbound-control-setup(8):

# unbound-control-setup

Before you run Unbound it is advisable to check the configuration. Unbound comes with a handy tool called unbound-checkconf(8) for this task:

# unbound-checkconf

If the tool complains about errors fire up your editor of choice and correct them. Now that everything is in place you can enable and start Unbound with the following commands:

# rcctl enable unbound
# rcctl start unbound

Verifying the setup

Now that all the files are in place and all the daemons are running it is time to check the setup. Wait, wasn’t that done already before the daemons were started? Well, the two tools {nsd|unbound}-checkconf can only check for syntactical correctness. This is not a prove that the setup actually does what it is built for. But with a few additional steps you can verify the functionality by yourself.

Checking Unbound

Important things first, so I start with the resolver instances. I recommend you to run the commands bellow on both servers running Unbound. Make sure that /etc/resolv.conf looks like this:

lookup file bind
search example.com
nameserver 127.0.0.1
nameserver 192.0.2.12

The last entry is correct for the server with the IP 192.0.2.11. On the other server you have to replace the octet .12 by .11.

First you should check if an external name can be resolved by Unbound:

# host www.duckduckgo.com

You may want to try this with different hosts in different domains.Second you should check if an internal name can be resolved by Unbound:

# host router.example.com

Choose one of the entries in your forward zone file. And don’t forget to test the reverse lookup too:

# host 192.0.2.1

Checking NSD

Well, there is not really much to check left if you have followed the checking instructions for Unbound above. Those checks – if successful – have proven that:

  • Unbound can resolve both internal and external names
  • Unbound is able to communicate with the local NSD slave
  • NSD slaves can get zone information from the NSD master

But there is one last check you can only perform on the NSD master. Add an additional record to each the forward and the reverse zone file on the NSD master, e. g.

test    A    192.0.2.99

and

99    PTR    test.example.com.

Don’t forget to increase the serial number in the SOA record of each zone. Execute the following command to notify the NSD daemon about the change to its zone files:

# nsd-control reload

Now repeat the tests for the internal forward and reverse lookup with the hostname and IP you’ve just added to the zone files. If everything works as expected this proves that the NSD master is able to notify the NSD slaves about changes in the zones.

Additional information

If you use DHCP in your network you can now let the DHCP server propagate the IPs of the two Unbound instances as DNS servers to the clients.

You should check the DNS settings in /etc/resolv.conf on all hosts which don’t get their IP configuration by DHCP. Of course this is not necessary if the Unbound instances use the IP addresses which are already configured on those hosts.

You cannot operate with dynamic entries in the DNS because NSD doesn’t support dynamic DNS entries. If you really need a setup with dynamic DNS you have to install the packages isc_bind and isc_dhcpd from ports and use these instead of NSD & Ubound.