bsdbox

openbsd dns server with unbound and nsd

<2019-07-28>

Introduction

The default installation of OpenBSD comes with both unbound(8) and nsd(8); unbound is a validating, recursive, and caching DNS resolver that provides DNSSEC validation, while nsd is an authoritative name server that holds DNS records. The combination of the two running locally, means that name server lookups (i.e., requests to resolve domain names into IP addresses and vice versa) can be handled locally without being sent upstream to your ISP or another public name server such as Google. This almost completely prevents snooping or tampering such as DNS cache poisoning or spoofing attacks. Both programs have a small memory footprint, offer a secure environment to provide lightning quick retrieval of both forward and reverse DNS requests, and are exceedingly simple to setup. This article will detail the steps to configure both unbound and nsd on your OpenBSD box.

Start Unbound

First, use rc to enable and start unbound:

# rcctl enable unbound
# rcctl start unbound
unbound(ok)

Before testing that it's working, we need to assign it as our primary nameserver in /etc/resolv.conf by either commenting out or removing the existing nameserver, and adding unbound, which is running on localhost:

nameserver 127.0.0.1

Test that it's working with a dig(1) DNS lookup request:

% dig bsdbox.org

; <<>> DiG 9.4.2-P2 <<>> bsdbox.org
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57451
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;bsdbox.org.                    IN      A

;; ANSWER SECTION:
bsdbox.org.             3600    IN      A       101.161.32.217

;; Query time: 119 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Wed Apr  4 23:16:09 2018
;; MSG SIZE  rcvd: 44

The SERVER: 127.0.0.1#53 shows that requests are being served locally by unbound on port 53. Given that unbound is a caching resolver, another dig request should show a faster query time:

% dig bsdbox.org 

; <<>> DiG 9.4.2-P2 <<>> bsdbox.org
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20855
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;bsdbox.org.                    IN      A

;; ANSWER SECTION:
bsdbox.org.             3595    IN      A       101.161.32.217

;; Query time: 2 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Wed Apr  4 23:16:14 2018
;; MSG SIZE  rcvd: 44

From 119 down to 2 msec, so all appears to be operating as expected.

Configure DNSSEC

DNSSEC validation is enabled by obtaining an initial trust anchor. This is achieved by downloading the KSK (Root Key Signing Key) with:

# unbound-anchor -a "/var/unbound/db/root.key"

And adding it under the server: directive in unbound's configuration file /var/unbound/etc/unbound.conf; while there, activate QNAME minimisation to improve DNS privacy by uncommenting the corresponding line shown below:

server:
	root-hints: "/var/unbound/db/root.hints"
	auto-trust-anchor-file: "/var/unbound/db/root.key"
	qname-minimisation: yes

The KSK initiates a chain of trust akin to the Public Key Infrastructure by essentially co-signing DNS entries to verify identities (i.e., confirming that our URI destinations are who they say they are). Therefore, it is essential that the key is authenticated at the time of anchoring and, thereafter, kept current. Apart from the key, we also need to know all the primary root DNS servers; we can achieve this by either downloading from Internic the root-hints file containing the definitive list of all primary root DNS servers, or by using the hardcoded list stored within unbound. The former ensures we're using the most up-to-date servers, but the latter is perfectly viable. If you choose the latter, remove or comment out the line above that begins with root-hints, but should you want the former, download the file with:

ftp -S do -o /var/unbound/db/root.hints https://www.internic.net/domain/named.root

Once complete, check configuration is syntactically correct before restarting, then confirm DNSSEC validation is operational.

# unbound-checkconf
unbound-checkconf: no errors in /var/unbound/etc/unbound.conf
# rcctl restart unbound
# dig com. SOA +dnssec

; <<>> DiG 9.4.2-P2 <<>> com. SOA +dnssec
;; global options: printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31120
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 14, ADDITIONAL: 1

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

;; ANSWER SECTION:
com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1523108041 1800
900 604800 86400
com. 900 IN RRSIG SOA 8 1 900 20180414133401 20180407122401 46967 com.
EPhpNR1CLIzYifJ6f5y3F8Lkg48tBndHptGkeBQOuJyMQUQgRwOjq5yl
V8XFu8kTIhj1c0tH+ZbeP29iclRBfbV2F3eFUXlkjHQqI0hrFgL3dBod
7bLe6HCwn0acdDbqlkLlGRJe58peR3lNMr8NpaJ+Y2KDU754tzYSnmZz AV4=

;; AUTHORITY SECTION:
com. 172800 IN NS j.gtld-servers.net.
com. 172800 IN NS m.gtld-servers.net.
com. 172800 IN NS h.gtld-servers.net.
com. 172800 IN NS l.gtld-servers.net.
com. 172800 IN NS g.gtld-servers.net.
com. 172800 IN NS d.gtld-servers.net.
com. 172800 IN NS i.gtld-servers.net.
com. 172800 IN NS f.gtld-servers.net.
com. 172800 IN NS e.gtld-servers.net.
com. 172800 IN NS k.gtld-servers.net.
com. 172800 IN NS b.gtld-servers.net.
com. 172800 IN NS a.gtld-servers.net.
com. 172800 IN NS c.gtld-servers.net.
com. 172800 IN RRSIG NS 8 1 172800 20180414044601 20180407033601 46967 com.
gURGiJt0j/b19Zi27P4QwYpxzswL0q2EAG7L9EzYgEVOJm6qZbCyR6RQ
61OiOkPCCVhpZyD/rbxKuEGHACJaXZ5TaWUbrL/AUDNC1UtiXAVgNyG1
iqNHf1+qAS/f5EQcKDvEJA1ISeKYExENG9E3GWbQ3pRcgMWLK9WmTdgc y3o=

;; Query time: 87 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Apr 7 23:34:18 2018
;; MSG SIZE rcvd: 637

From the ad flag, we can see that DNSSEC is operational but not that responses are valid; to verify that correct responses are being returned, we can use the DNSSEC validation test from https://verteiltesysteme.net by checking the following signed domain, which should return an A record:

# dig sigok.verteiltesysteme.net @127.0.0.1

; <<>> DiG 9.4.2-P2 <<>> sigok.verteiltesysteme.net @127.0.0.1
;; global options: printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49205
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 4

;; QUESTION SECTION:
;sigok.verteiltesysteme.net. IN A

;; ANSWER SECTION:
sigok.verteiltesysteme.net. 60 IN A 134.91.78.139

;; AUTHORITY SECTION:
verteiltesysteme.net. 1340 IN NS ns2.verteiltesysteme.net.
verteiltesysteme.net. 1340 IN NS ns1.verteiltesysteme.net.

;; ADDITIONAL SECTION:
ns1.verteiltesysteme.net. 1340 IN A 134.91.78.139
ns1.verteiltesysteme.net. 1340 IN AAAA 2001:638:501:8efc::139
ns2.verteiltesysteme.net. 1340 IN A 134.91.78.141
ns2.verteiltesysteme.net. 1340 IN AAAA 2001:638:501:8efc::141

Confirmed. And the following domain should return a SERVFAIL:

# dig sigfail.verteiltesysteme.net @127.0.0.1

; <<>> DiG 9.4.2-P2 <<>> sigfail.verteiltesysteme.net @127.0.0.1
;; global options: printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 62744
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;sigfail.verteiltesysteme.net. IN A

;; Query time: 148 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Apr 7 21:26:28 2018
;; MSG SIZE rcvd: 46

Confirmed! Now that DNSSEC validation has been activated, restart the daemon with rcctl restart unbound, and check the log for any errors:

# tail -20 /var/log/daemon
Apr 3 23:31:55 nsx unbound: [93843:0] info: service stopped (unbound
1.6.6).
Apr 3 23:31:55 nsx unbound: [93843:0] info: server stats for thread 0: 20
queries, 5 answers from cache, 15 recursions, 0 prefetch, 0 rejected by ip
ratelimiting
Apr 3 23:31:55 nsx unbound: [93843:0] info: server stats for thread 0:
requestlist max 0 avg 0 exceeded 0 jostled 0
Apr 3 23:31:55 nsx unbound: [93843:0] info: average recursion processing
time 0.529802 sec
Apr 3 23:31:55 nsx unbound: [93843:0] info: histogram of recursion
processing times
Apr 3 23:31:55 nsx unbound: [93843:0] info: [25%]=0.013824
median[50%]=0.057344 [75%]=0.821608
Apr 3 23:31:55 nsx unbound: [93843:0] info: lower(secs) upper(secs)
recursions
Apr 3 23:31:55 nsx unbound: [93843:0] info: 0.004096 0.008192 1
Apr 3 23:31:55 nsx unbound: [93843:0] info: 0.008192 0.016384 4
Apr 3 23:31:55 nsx unbound: [93843:0] info: 0.016384 0.032768 1
Apr 3 23:31:55 nsx unbound: [93843:0] info: 0.032768 0.065536 2
Apr 3 23:31:55 nsx unbound: [93843:0] info: 0.065536 0.131072 1
Apr 3 23:31:55 nsx unbound: [93843:0] info: 0.131072 0.262144 1
Apr 3 23:31:55 nsx unbound: [93843:0] info: 0.524288 1.000000 2
Apr 3 23:31:55 nsx unbound: [93843:0] info: 1.000000 2.000000 1
Apr 3 23:31:55 nsx unbound: [93843:0] info: 2.000000 4.000000 2
Apr 3 23:31:56 nsx unbound: [93111:0] notice: init module 0: validator
Apr 3 23:31:56 nsx unbound: [93111:0] notice: init module 1: iterator
Apr 3 23:31:56 nsx unbound: [93111:0] info: start of service (unbound
1.6.6).

Everything is operating as expected. We can check a domain's DNSSEC signature like so:

# unbound-host -C /var/unbound/etc/unbound.conf -v sigok.verteiltesysteme.net 
sigok.verteiltesysteme.net has address 134.91.78.139 (secure)
sigok.verteiltesysteme.net has IPv6 address 2001:638:501:8efc::139 (secure)
sigok.verteiltesysteme.net has no mail handler record (secure)

For comparison, an unsigned domain without DNSSEC:

# unbound-host -C /var/unbound/etc/unbound.conf -v sigfail.verteiltesysteme.net
sigfail.verteiltesysteme.net has address 134.91.78.139 (BOGUS (security
failure))
validation failure <sigfail.verteiltesysteme.net. A IN>: signature crypto
failed from 2001:638:501:8efc::139
sigfail.verteiltesysteme.net has IPv6 address 2001:638:501:8efc::139 (BOGUS
(security failure))
validation failure <sigfail.verteiltesysteme.net. AAAA IN>: signature crypto
failed from 2001:638:501:8efc::139

Now that unbound is up and running, serving our DNS requests locally, we can move onto nsd.

NSD Configuration

Unlike unbound, which resolves our outgoing queries for domain name resolution, nsd is an authoritative nameserver, which holds our own DNS records, and provides responses to incoming queries for names in our own zone. Because of this, it's highly recommended that you configure both a primary and a secondary nameserver so that in the event one is unreachable, requests of your zone are still received; this article only covers setting up the primary (master) server—configuring the slave will be left as an exercise to the reader.

First, edit the configuration file at /var/nsd/etc/nsd.conf to add the master zone record as shown (substituting with your server's particulars):

server:
	hide-version: yes
	ip-address: 199.22.33.44    # ipaddr of server

remote-control:
	control-enable: yes

## master zone example
zone:
	name: "nsx.bsdbox.org"
	zonefile: "nsx.bsdbox.org.zone"

Next, edit the specified zone file at /var/nsd/zones/nsx.bsdbox.org.zone:

$ORIGIN nsx.bsdbox.org.
$TTL 4h

@        IN  SOA  a.ns.nsx.bsdbox.org. hostmaster.nsx.bsdbox.org. (
		  2018040502      ; serial
		  1h              ; refresh
		  30m             ; retry
		  7d              ; expire
		  1h )            ; negative
	 IN  NS   a.ns.nsx.bsdbox.org.
	 IN  NS   b.ns.nsx.bsdbox.org.

	 IN  MX   0 mail.nsx.bsdbox.org.

a.ns     IN  A    199.222.33.44
b.ns     IN  A    199.222.33.44
mail     IN  A    199.222.33.44

Now check the configuration before starting the service:

# nsd-checkconf /var/nsd/etc/nsd.conf
# nsd-checkzone nsx.bsdbox.org /var/nsd/zones/nsx.bsdbox.org.zone
zone nsx.bsdbox.org is ok
# rcctl enable nsd && rcctl start nsd
nsd(ok)
nsd(ok)

Check the log for errors:

# tail -5 /var/log/daemon
Apr 4 00:33:43 nsx last message repeated 2 times
Apr 4 00:34:20 nsx nsd[59379]: signal received, shutting down...
Apr 4 00:34:21 nsx nsd[24345]: nsd starting (NSD 4.1.17)
Apr 4 00:34:22 nsx nsd[8356]: zone nsx.bsdbox.org read with success
Apr 4 00:34:22 nsx nsd[8356]: nsd started (NSD 4.1.17), pid 79289

The log indicates a successful, error-free start, which means we have both a local DNS server to resolve our outgoing queries—freeing us from the prying eyes of Google—and an authoritative nameserver serving requests for our own records.

DNSCrypt

If you have DNSCrypt setup, and want to forward outgoing DNS queries to a DNScrypt nameserver rather than resolving them locally with unbound, replace the existing unbound.conf with the following:

server:
	interface: 127.0.0.1
	interface: ::1
	access-control: 0.0.0.0/0 refuse
	access-control: 127.0.0.0/8 allow
	access-control: ::0/0 refuse
	access-control: ::1 allow
	do-not-query-localhost: no
	hide-identity: yes
	hide-version: yes
	root-hints: "/var/unbound/db/root.hints"
	auto-trust-anchor-file: "/var/unbound/db/root.key"
	qname-minimisation: yes

remote-control:
	control-enable: yes
	control-use-cert: no
	control-interface: /var/run/unbound.sock

forward-zone:
	name: "."
	forward-addr: 127.0.0.1@40 # primary DNScrypt server
	forward-addr: 127.0.0.1@41 # failover DNScrypt server

Check unbound configuration and, if no complaints, restart the service:

# unbound-checkconf
unbound-checkconf: no errors in /var/unbound/etc/unbound.conf
# rcctl restart unbound
unbound(ok)
unbound(ok)
DNSSEC validation test
Figure 1: DNSSEC validation test

Further Reading

Tags: openbsd
send comments to mark AT jamsek DOT net

Generated by emacs org mode

Copyright © 2023 Mark Jamsek