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

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

Last update: 2020-09-13

Introduction

This post is about configuring nsd(8) as a public name server for your own domain, providing DNS over TLS (DoT). 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"

remote-control:
    control-enable: yes

key:
    name: nskey.example.net
    algorithm: sha256
    secret: "IAmASecretKeyForDomainTransfers"

zone:
    name: "example.net"
    zonefile: "/var/nsd/zones/master/%s.dns"
    notify: 198.51.100.12 nskey.example.net
    notify: 2001:db8:2::c633:640c nskey.example.net
    provide-xfr: 198.51.100.12 nskey.example.net
    provide-xfr: 2001:db8:2::c633:640c nskey.example.net
    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-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"

remote-control:
    control-enable: yes

key:
    name: nskey.example.net
    algorithm: sha256
    secret: "IAmASecretKeyForDomainTransfers"

zone:
    name: "example.net"
    zonefile: "/var/nsd/zones/slave/%s.dns"
    allow-notify: 192.0.2.11 nskey.example.net
    allow-notify: 2001:db8:1::c000:020b nskey.example.net
    request-xfr: AXFR 192.0.2.11 nskey.example.net
    request-xfr: AXFR 2001:db8:1::c000:020b nskey.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

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