Setting up Knot DNS - DNSSEC, GeoDNS, RFC2136, authenticated zonefile sync and more!

Wed, 1 Mar 2023

Knot DNS is one of the easier to setup authoritative dns servers out there, made by NIC.CZ.

In this tutorial I’ll show how to setup Knot with DNSSEC, authenticated master -> slave sync, RFC2136 (for automatic dns-based certs in caddy and such) and GeoDNS, which makes the server give an IP closest to the user.

I assume you have two Debian based systems, with port 53 (tcp+udp) forwarded.

Installing Knot

This guide covers debian, but instructions for other distributions can be found on the download page Run the following on both the master and the slave:

apt-get -y install apt-transport-https lsb-release ca-certificates wget
wget -O /usr/share/keyrings/knot.gpg https://deb.knot-dns.cz/apt.gpg
sh -c 'echo "deb [signed-by=/usr/share/keyrings/knot.gpg] https://deb.knot-dns.cz/knot-latest/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/knot-latest.list'
apt-get update
apt-get install knot knot-dnsutils

Basic Configuration

knot.conf

Now, to add in the basic configuration, overwrite the /etc/knot/knot.conf file on the master with the following text:

server:
    rundir: "/run/knot"
    user: knot:knot
    listen: 0.0.0.0@53
log:
  - target: syslog
    any: info
database:
    storage: "/var/lib/knot"
remote:
  - id: secondary
    address: your.other.servers.ip@53
acl:
  - id: acl_secondary
    address: your.other.servers.ip
    action: transfer
template:
  - id: default
    storage: "/etc/knot/zones"
    file: "%s.zone"
    semantic-checks: on
    # Don't override zonefile
    zonefile-sync: -1
    zonefile-load: difference-no-serial
    journal-content: all
zone:
  - domain: your.domain
    notify: secondary
    acl: acl_secondary

On the slave, overwrite the same file with the following text:

server:
    rundir: "/run/knot"
    user: knot:knot
    listen: 0.0.0.0@53
log:
  - target: syslog
    any: info
database:
    storage: "/var/lib/knot"
remote:
  - id: primary
    address: your.main.servers.ip@53
acl:
  - id: acl_primary
    address: your.main.servers.ip
    action: notify
template:
  - id: default
    storage: "/etc/knot/zones"
    file: "%s.zone"
zone:
  - domain: your.domain
    master: primary
    acl: acl_primary

Zonefile

At this point you need to create /etc/knot/zones on both the master and slave as that is where the zonefiles will be stored. Now create a file named your.domain.zone in the directory on the master alone and add the following text to it:

$ORIGIN your.domain. ; 'default' domain as FQDN for this zone
$TTL 3600 ; default time-to-live for this zone

your.domain.   IN  SOA     ns1.your.domain. ns2.your.domain. (
        YYYYMMDD01  ;Serial
        14400       ;Refresh
        3600        ;Retry
        1209600     ;Expire
        3600        ;Negative response caching TTL
)
@   IN  NS  ns1.your.domain.
@   IN  NS  ns2.your.domain.
ns1 A   your.main.servers.ip
ns2 A   your.other.servers.ip
@   A   your.main.servers.ip

; PTR Records (for mailservers)
your.ip.in.reverse.in-addr.arpa.    PTR mail.your.domain.

At this point, you can run systemctl restart knot (restart since its a major change) on both nodes.

Updating the zonefile

Updating the zonefile is not hard.

Token-based zonefile authentication

At the moment, the authentication for sending and receiving the zonefile is completely based on the IP address, which is not very secure.

To remediate this, we can use token-based authentication.

First, you need to generate a key with keymgr -t zonesync hmac-sha256

This is the key that will authenticate the zone transfers.

You can copy-paste the output it gives you into your knot.conf, above the remote section on both master and slave.

Now, you need to add key: zonesync to the remote, acl_primary/secondary sections on both master and slave.

At this point, you can run systemctl restart knot, and all the syncs will be more secure!

DNSSEC

DNSSEC is an extension to DNS designed to protect applications using DNS from accepting forged or manipulated DNS data by using zone signing.

Enabling DNSSEC on knot is as simple as adding the following line to the template section:

template:
    ...
    dnssec-signing: on
    ...

At this point, you need to upload the DS record to your Registry. This is usually done through your registrar’s WebUI.

The DNSSEC-enabled DNS servers use the DS record from your domain registry to validate your records.

You can get your DS record with the following command:

keymgr your.domain ds

The output will look something like this:

54674 13 2 E28E3DB78E5517A577353A43799AD14EC044720BAE4906D134F5EA40 74AC0287

In the example given:

On namecheap, you add this at Advanced DNS -> DNSSEC

You can check if your DNSSEC is working properly at DNSViz and DNSSEC-Analyzer.

RFC2136

RFC2136 is an RFC that allows dynamic updates for DNS.

This works completely over DNS and does not require a special API.

To set it up, you need to create an ACL as follows:

acl:
    ...
    - id: acl_dynupdates
    address: [an.authorized.ip.addr, another.authorized.ip.addr]
    action: update
...
zone:
  - domain: your.domain
    notify: secondary
    acl: [acl_secondary, acl_dynupdates]

You can also add token-based auth by generating another key with keymgr -t rfc2136 hmac-sha256

You can add it to the config as follows:

key:
    ...
    - id: rfc2136
    algorithm: hmac-sha256
    secret: xxx
...
acl:
    ...
    - id: acl_dynupdates
    address: [an.authorized.ip.addr, another.authorized.ip.addr]
    action: update
    key: rfc2136

After this, you can run systemctl restart knot to apply the changes.

Caddy

Caddy is a modern webserver which supports automatic cert generation from letsencrypt/zerossl with acme.

It uses http-01 for the challenge by default, but can use a dns challenge too.

For the DNS challenge, you first need to install the RFC2136 DNS plugin with the following command:

xcaddy build --with github.com/caddy-dns/rfc2136@master

Now, add the following lines to the top of your caddyfile

{
    acme_dns rfc2136 {
        key_name "rfc2136"
        key_alg "hmac-sha256"
        key "xxx"
        server "your.main.servers.ip:53"
    }
    acme_ca https://acme-v02.api.letsencrypt.org/directory
}

Now, all certs can be generated using the DNS challenge!

This is especially useful in a GeoDNS environment.

GeoDNS

GeoDNS allows geographical split horizon based on a GeoIP database, such as Maxmind’s free GeoLite2.

Firstly, you need to procure the GeoLite2 database from Maxmind.

Due to policy changes, you now need to signup on Maxmind’s website in order to get access.

However, older GeoIP DBs can still be found in many places, including distribution package repositories.

Once you procure your copy of GeoLite2, you need to install the GeoIP module for knot-dns on both master and slave.

On Debian, the package’s name is knot-module-geoip

After installing the module, you can add it to the knot.conf (must be before zone section however):

mod-geoip:
  - id: geo 
    config-file: "/etc/knot/geo.conf"
    mode: geodb
    geodb-file: "/var/lib/knot/GeoLite2-City.mmdb"
    geodb-key: [ continent/code, country/iso_code, city/names/en ]

You also need to include the GeoIP module for your domain. You can do that by adding this line to your domain’s zone section:

zone:
    - domain: your.domain
      ...
      module: mod-geoip/geo

Now, you need to configure the GeoDNS.

To do so, create a file called /etc/knot/geo.conf with the following:

geodnsubdom.your.domain:
  - geo: "*;*;*" # Fallback incase DNS server doesn't send ECS
    A: your.main.servers.ip
    TXT: "Worldwide"
  - geo: "EU;*;*" # Europe
    A: your.europe.servers.ip
    TXT: "Europe"
    ...

However, the file needs to be manually synced to the slave on every update.

It is also painful to use if you have multiple root subdomains you want to use GeoDNS with.

Due to these reasons, I made a kinda hacky script to remediate this:

#!/usr/bin/env bash
geoconf=/etc/knot/geo.conf
remote='geodns@other.servers.ip'
printf '' > $geoconf
for i in $(</etc/knot/geodnsdomains); do
    cat /etc/knot/geodnstemplate >> $geoconf
    sed -i "s/REPLACEME/${i}/" $geoconf
done
scp $geoconf "${remote}":/var/geo.conf
ssh $remote "sudo systemctl restart knot"
systemctl restart knot

In order to allow the unprivileged user on the slave to restart knot, I used a sudo ALLOW_CMDS flag:

Cmnd_Alias KNOT_CMDS = /usr/bin/systemctl restart knot
geodns ALL=(ALL) NOPASSWD: KNOT_CMDS

And thats it. Thanks for reading!