$_ BSDHowTo.ch
How To... Why not..? Scripts Patches RSS logo

How to build a name server with DNS over TLS (DoT) and XFR over TLS (XoT)

Last update: 2023-09-12

Introduction

This post is about configuring nsd(8) as a public name server for your own domain, providing DNS over TLS (DoT). This version of the configuration includes XFR over TLS (XoT) as defined in RFC 9103. This protects the zone transfers between primaries and secondaries from eavesdropping. Everything needed for this task is already there in OpenBSD base installation. You don’t need to install a single additional package for this.

Certificates

Any certificate provider who supports the ACME protocol can be used for this. Personally I go with the most popular provider nowadays: Let’s Encrypt.

The challenges issued by the certificate provider will be answered by httpd(8) using a configuration in /etc/httpd.conf similar to this one:

server "ns1.example.net" {
    listen on egress port http
    root "/"
    location "/.well-known/acme-challenge/*" {
        request strip 2
        root "/acme"
    }
}

types {
    include "/usr/share/misc/mime.types"
}

Test your configuration, enable and start httpd(8):

$ doas httpd -n
$ doas rcctl enable httpd
$ doas rcctl start httpd

The next step is to configure acme-client(1). To save some typing I suggest you use /etc/examples/acme-client.conf as source and modified accordingly. The resulting acme-client.conf must be saved in /etc and look similar:

authority letsencrypt {
    api url "https://acme-v02.api.letsencrypt.org/directory"
    account key "/etc/acme/letsencrypt-privkey.pem"
}

authority letsencrypt-staging {
    api url "https://acme-staging.api.letsencrypt.org/directory"
    account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

domain ns1.example.net {
    domain key "/etc/ssl/acme/private/ns1.example.net.key"
    domain certificate "/etc/ssl/acme/ns1.example.net.crt"
    domain full chain certificate "/etc/ssl/acme/ns1.example.net.fullchain.pem"
    sign with letsencrypt
}

Before you can get your certificate you must make sure pf(4) lets the requests to httpd(8) pass through. Add a rule similar to the following one to your pf.conf(5):

pass in on egress proto tcp from any to egress port http

Check the configuration in /etc/pf.conf and load it into pf(4) using the following commands:

$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf

Now you are ready to fetch the certificate for nsd(8). Don’t forget to create the OCSP file too:

$ dest=/etc/ssl/ns1.example.net
$ doas acme-client ns1.example.net
$ doas ocspcheck -No ${dest}.ocsp ${dest}.fullchain.pem

You want to make sure that the certificate as well as the OCSP response get renewed before these expire. I suggest you add something like this to /etc/daily.local:

#!/bin/sh

dest=/etc/ssl/ns1.example.net

acme-client ns1.example.net
if [ $? -eq 0 ] ; then
    ocspcheck -No ${dest}.ocsp ${dest}.fullchain.pem
    rcctl restart nsd
fi

oscpcheck -i ${dest}.ocsp ${dest}.fullchain.pem
if [ $? -eq 1 ] ; then
    ocspcheck -No ${dest}.ocsp ${dest}.fullchain.pem
    rcctl restart nsd
fi

The script checks the cerificate first. If it gets renewed the script fetches the OCSP response for the new certificate and restarts nsd(8) to load the new files. In the second step the script checks if the OCSP response has expired. If this is the case it fetches the new OCSP response and restarts nsd(8) to load the new OCSP response.

Configuration of nsd(8)

The correct configuration of nsd(8) depends on the role the server is going to play: primary or secondary. Most domain providers require you to run at least two name servers in preferably two different sub-nets. I will show you both configurations. First the configuration for the primary:

server:
    hide-identity: yes
    hide-version: yes
    ip-address: 192.0.2.11
    ip-address: 192.0.2.11@853
    ip-address: 2001:db8:1::c000:020b
    ip-address: 2001:db8:1::c000:020b@853
    server-count: 1
    statistics: 86400
    tls-service-key: "/etc/ssl/ns1.example.net.key"
    tls-service-pem: "/etc/ssl/ns1.example.net.fullchain.pem"
    tls-service-ocsp: "/etc/ssl/ns1.example.net.ocsp"
    verbosity: 1
    xfrdfile: "/var/nsd/db/xfrd.state"

remote-control:
    control-enable: yes

key:
    name: nskey
    algorithm: sha256
    secret: "IAmASecretKeyForDomainTransfers"

zone:
    name: "example.net"
    zonefile: "/var/nsd/zones/master/%s.dns"
    notify: 198.51.100.12 nskey
    notify: 2001:db8:2::c633:640c nskey
    provide-xfr: 198.51.100.12 nskey
    provide-xfr: 2001:db8:2::c633:640c nskey
    outgoing-interface: 192.0.2.11
    outgoing-interface: 2001:db8:1::c000:020b

Make sure both primary and secondary server use the same secret key for domain transfer or it will not work. You can generate the secret for the domain transfer key using the following command:

$ for i in $(jot -r -s " " 32 0 255) ; do
> echo ${i} | awk '{ printf "%c", $1 }'
> done | sha256 -b

The next step is to make sure the file for your zone contains some valid data. Either you already have a valid zone file or you can use the following one as a starting point:

$ORIGIN .
$TTL 3600   ; 1 hour
example.net IN  SOA ns1.example.net. hostmaster.example.net. (
                1   ; serial
                10800   ; refresh (3 hours)
                600 ; retry (10 minutes)
                241900  ; expire (4 weeks)
                3600    ; minimum (1 hour)
                )
            NS  ns1.example.net.
            NS  ns2.example.net.
$ORIGIN example.net.
ns1         A   192.0.2.11
            AAAA    2001:db8:1::c000:020b
ns2         A   198.51.100.12
            AAAA    2001:db8:2::c633:640c

The configuration of the secondary server needs slightly other options to make it a secondary, but looks similar to the one for the primary server:

server:
    hide-identity: yes
    hide-version: yes
    ip-address: 198.51.100.12
    ip-address: 198.51.100.12@853
    ip-address: 2001:db8:2::c633:640c
    ip-address: 2001:db8:2::c633:640c@853
    server-count: 1
    statistics: 86400
    tls-cert-bundle: "/etc/ssl/cert.pem"
    tls-service-key: "/etc/ssl/ns2.example.net.key"
    tls-service-pem: "/etc/ssl/ns2.example.net.fullchain.pem"
    tls-service-ocsp: "/etc/ssl/ns2.example.net.ocsp"
    verbosity: 1
    xfrdfile: "/var/nsd/db/xfrd.state"

remote-control:
    control-enable: yes

key:
    name: nskey
    algorithm: sha256
    secret: "IAmASecretKeyForDomainTransfers"

tls-auth:
    name: xot.example.net
    auth-domain-name: ns1.example.net

zone:
    name: "example.net"
    zonefile: "/var/nsd/zones/slave/%s.dns"
    allow-notify: 192.0.2.11 nskey
    allow-notify: 2001:db8:1::c000:020b nskey
    request-xfr: AXFR 192.0.2.11 nskey xot.example.net
    request-xfr: AXFR 2001:db8:1::c000:020b nskey xot.example.net
    outgoing-interface: 198.51.100.12
    outgoing-interface: 2001:db8:2::c633:640c

On both servers you must run the following command to make remote control using nsd-control(8) work:

$ doas nsd-control-setup

Now it is time to check your configuration. Run the following command on each server to find the typos:

$ doas nsd-checkconf /var/nsd/etc/nsd.conf

On the secondary server you must make sure that nsd(8) is able to verify the certificate of the primary in order for XoT to work. As nsd(8) runs chroot(2) to /var/nsd you need to copy the original file to the right place:

$ doas mkdir /var/nsd/etc/ssl
$ doas install -m 444 -o root -g bin /etc/ssl/cert.pem /var/nsd/etc/ssl/cert.pem

As the file cert.pem changes from time to time - if you upgrade your system - you may want to enter the above install(1) command into /etc/rc.local to make sure the copy in /var/nsd/etc/ssl gets updated too.

If everything looks good you are ready to enable and start nsd(8) on both servers:

$ doas rcctl enable nsd
$ doas rcctl start nsd

The last piece of the puzzle are the rules for pf(4) that allow external access to nsd(8). Add these two lines to /etc/pf.conf:

pass in on egress proto { tcp udp } from any to egress port domain
pass in on egress proto { tcp udp } from any to egress port domain-s

Check and load the changed configuration:

$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf