8 minutes
Knot, dnscontrol, and catalog zones.. how to run DNS as code without a cloud provider
Overview
DNS
The domain name system is a fundamental part of the internet, translating ip address into names. As we lurch towards IPv6 this becomes even more important as you may be able to remember 12.45.23.19 you definitely won’t remember 2000:ee80:2000:700:71db:5273:628c:bae5 !
Running your own DNS gives you.. well, not a lot to be honest. It’s cheap, and lets you play with things like DNSSEC, etc. So let’s pretend that’s why we do it.
This is not intended to be a comprehensive overview of the DNS, merely a path towards IaaC bliss with dnscontrol and RFC9432 catalog zones. It’s equally applicable to dnscontrol with Route53 or any other cloud provider.
Knot
Knot DNS is an authoritative-only name server written by the fine folk at CZ.nic. It’s fast, featureful and cooler than BIND.
dnscontrol
There was a book once, Time Management for Sysadmins that changed my life. Turns out that same guy writes useful software too! Tom Limoncelli helped get a project by StackExchange called dnscontrol off the ground and it’s pretty amazing. Except for the DSL being javascript.. but we can’t have everything
rfc2136
Before Cloud, and everything-being-an-API, there was rfc2136. A way to signal to a DNS server you want to update a record. This is a fast, secure and easy way to change DNS zones without having to login to a name server, remember how to use vi, muck it up anyhow then spend the next 20 minutes figuring out why your zone won’t load.
rfc9432
Like the previous item, rfc9432 defines some useful things to do with operating a DNS server, this time catalog zones. In short it’s a way to put zone provisioning in-band with DNS. You put zone configuration in a zone. woah.
Put it all together
Combining all thes things means we can setup an authoritative instance once, and never have to login to it again to make zone changes. This makes it bearable to run your own authoritative DNS on platforms like Kubernetes without too much jiggery pokery of logging in and editing files, or creating sidecars to muck with with zone files, etc.
Knot configuration
Due to the way dnscontrol works, first it will AXFR a zone to compute a diff, it will then use rfc2136 to send updates that don’t exist on the server we will separate the authentication of each task, and one use TSIG key each.
So use your favourite tool to create the two TSIG keys, we’ll use keymgr from Knot DNS itself.
For AXFR;
keymgr -t dnscontrol-axfr
Will generate following output:
# hmac-sha256:dnscontrol-axfr:62ZtJejUf8ci6VPXMrwV+FlUAZlOXlK12zSZ83bGRQo=
key:
- id: dnscontrol-axfr
algorithm: hmac-sha256
secret: 62ZtJejUf8ci6VPXMrwV+FlUAZlOXlK12zSZ83bGRQo=
And for updates:
keymgr -t dnscontrol-ddns
will generate the following output:
# hmac-sha256:dnscontrol-ddns:a1F+aYbRT2AABmplNR/1KXsmfAyrPatRCrfs9fs8gIA=
key:
- id: dnscontrol-ddns
algorithm: hmac-sha256
secret: a1F+aYbRT2AABmplNR/1KXsmfAyrPatRCrfs9fs8gIA=
Now plug these into the knot config below:
server:
tcp-idle-timeout: "60"
tcp-io-timeout: "0"
tcp-remote-io-timeout: "0"
tcp-reuseport: "on"
listen: [ "0.0.0.0@53", "::@53" ]
log:
- target: "stdout"
any: "info"
statistics:
timer: "300"
key:
- id: dnscontrol-axfr
algorithm: hmac-sha256
secret: 62ZtJejUf8ci6VPXMrwV+FlUAZlOXlK12zSZ83bGRQo=
- id: dnscontrol-ddns
algorithm: hmac-sha256
secret: a1F+aYbRT2AABmplNR/1KXsmfAyrPatRCrfs9fs8gIA=
acl:
- id: "dnscontrol-axfr"
key: "dnscontrol-axfr."
action: "transfer"
- id: "dnscontrol-ddns"
key: "dnscontrol-ddns."
action: "update"
template:
- id: "catalog-zone-template"
storage: "/zones/"
acl: [ "dnscontrol-axfr", "dnscontrol-ddns" ]
semantic-checks: "on"
zonefile-sync: "60"
zonefile-load: "difference"
serial-policy: "unixtime"
- id: "default"
global-module: "mod-stats"
storage: "/zones/"
acl: [ "dnscontrol-axfr", "dnscontrol-ddns"]
semantic-checks: "on"
zonefile-sync: "60"
serial-policy: "unixtime"
zone:
- domain: "catalog.home.arpa."
catalog-role: "interpret"
catalog-template: "catalog-zone-template"
You can see the keys we generated, ACLs to give them some permissions, and then we apply the ACL to a catalog zone template. Finally, we configure a catalog zone, catalog.home.arpa, using the template catalog-zone-template.
That’s enough to get us going, even though knot will complain about the zone not existing:
2024-03-15T14:40:57+1300 info: Knot DNS 3.3.5 starting
2024-03-15T14:40:57+1300 info: loaded configuration file 'knot.conf', mapsize 500 MiB
2024-03-15T14:40:57+1300 info: using incoming TCP Fast Open
2024-03-15T14:40:57+1300 info: binding to interface 0.0.0.0@53
2024-03-15T14:40:57+1300 info: binding to interface ::@53
2024-03-15T14:40:57+1300 info: loading 1 zones
2024-03-15T14:40:57+1300 info: [catalog.home.arpa.] zone will be loaded
2024-03-15T14:40:57+1300 info: starting server
2024-03-15T14:40:57+1300 error: [catalog.home.arpa.] failed to parse zone file '/zones/catalog.home.arpa.zone' (not exists)
2024-03-15T14:40:57+1300 info: [catalog.home.arpa.] zone not found
2024-03-15T14:40:57+1300 error: [catalog.home.arpa.] zone event 'load' failed (not exists)
2024-03-15T14:40:57+1300 info: control, binding to '/opt/homebrew/var/run/knot/knot.sock'
2024-03-15T14:40:57+1300 info: server started in the foreground, PID 96700
We’ll use dnscontrol to fix that.
dnscontrol configuration
create a directory to hold your dnsconfig config and start with the following content in dnsconfig.js
var REG_NONE = NewRegistrar('none');
var BIND = NewDnsProvider('bind');
var DNS_MASTER = NewDnsProvider('axfrddns');
var DNS_BIND = NewDnsProvider('bind', {
'default_soa': {
'master': '127.0.0.1.',
'mbox': 'dene.slush.ca.',
'refresh': 14400,
'retry': 3600,
'expire': 604800,
'minttl': 3600
},
}
);
require_glob('source/');
Now create a file named creds.json with the following:
{
"bind": {
"TYPE": "BIND"
},
"axfrddns": {
"TYPE": "AXFRDDNS",
"transfer-key": "hmac-sha256:dnscontrol-axfr:62ZtJejUf8ci6VPXMrwV+FlUAZlOXlK12zSZ83bGRQo=",
"update-key": "hmac-sha256:dnscontrol-ddns:a1F+aYbRT2AABmplNR/1KXsmfAyrPatRCrfs9fs8gIA=",
"master": "203.0.113.53"
}
}
Putting creds in a config file likely to be put into git is bad, you can us tools like git-crypt, or Blackbox to solve tha tproblem, but for us it’s beyond the scope of this document. The master IP address should point to the ip address of your Knot DNS server.
One more file to create to get started, source/catalog.home.arpa.
cat >source/catalog.home.arpa<<EOF
D("catalog.home.arpa", REG_NONE,
DnsProvider(BIND),
DnsProvider(DNS_MASTER),
DefaultTTL(3600),
NAMESERVER('localhost.'),
NAMESERVER_TTL('2d'),
TXT('version', '2')
)
EOF
Without going into too much detail, catalog zones look a lot like a normal zone. They have SOA records, and we’ll add more records later but the key record so far is the TXT(‘version’, ‘2’) record. That tells dnscontrol to create a TXT record in the zone with a value of 2, which is the required version for Knot DNS.
Running dnscontrol and making our workflow clear
Now if we run dnscontrol push on that, we’ll see the following:
******************** Domain: catalog.home.arpa
ERROR
Error getting corrections (axfrddns): [Error] AXFRDDNS: nameserver refused to transfer the zone catalog.home.arpa: dns: bad xfr rcode: 2
Done. 0 corrections.
completed with errors
Wait, you said there would be no faffing around ? Well, no logging in and running vi I said. We’ll have to do a little bit of faffing in dnscontrol to make it all work.
The workflow will be:
dnscontrol push --providers bind
to create a local zone file- copy zone file to Knot DNS server.
- reload the zone in knot.
- make changes to catalog zone, configuring a new zone.
- Do it all again to create the file on the Knot DNS server.
Create local zone file
So, step 1:
dnscontrol push --providers bind
******************** Domain: catalog.home.arpa
File does not yet exist: "zones/catalog.home.arpa.zone" (will create)
1 correction (bind)
#1: + CREATE catalog.home.arpa SOA 127.0.0.1. dene.slush.ca. 14400 3600 604800 3600 ttl=300
+ CREATE catalog.home.arpa NS localhost. ttl=172800
+ CREATE version.catalog.home.arpa TXT "2" ttl=3600
WRITING ZONEFILE: zones/catalog.home.arpa.zone
SUCCESS!
Done. 1 corrections.
Copy zone file to Knot DNS
Step 2, which in my case is a kubernetes pod with the label app=knot-master, storing zones in /zones:
kubectl cp zones/catalog.home.arpa.zone $(kubectl get pod -l "app=knot-master" -o jsonpath='{.items[0].metadata.name}'):/zones/catalog.home.arpa.zone
Reload the zone file in KnotDNS
Step 3, reload the zone, again in the k8s container:
kubectl exec -it $(kubectl get pod -l "app=knot-master" -o jsonpath='{.items[0].metadata.name}') knotc reload-zone catalog.home.arpa
OK
Adding new zones to Knot DNS
At this point, Knot DNS should know about, and have loaded our catalog zone, so let’s add a new entry (remember, the whole point of doing this is right here, adding a record is creating a new hosted zone configuration in Knot). Edit your source/catalog.home.arpa.js to look like the following:
("catalog.home.arpa", REG_NONE,
DnsProvider(BIND),
DnsProvider(DNS_MASTER),
DefaultTTL(3600),
NAMESERVER('localhost.'),
NAMESERVER_TTL('2d'),
TXT('version', '2'),
PTR('2.zones', 'testing.home.arpa.')
)
As you can see, catalog zone records are PTR, or pointer records. This tells Knot DNS to create a zone named testing.home.arpa using our zone-catalog-template and start serving it.
So, now we can push changes with dnscontrol as usual,
dnscontrol push
******************** Domain: catalog.home.arpa
1 correction (axfrddns)
#1: DDNS UPDATES to 'catalog.home.arpa' (primary master: '127.0.0.1:53'). Changes:
+ CREATE 2.zones.catalog.home.arpa PTR testing.home.arpa. ttl=3600
SUCCESS!
Done. 1 corrections.
As you can see, we’ve now pushed an update and in our Knot DNS logs we should see:
2024-03-15T16:22:49+1300 error: [testing.home.arpa.] failed to parse zone file '/tmp/zones/testing.home.arpa.zone' (not exists)
2024-03-15T16:22:49+1300 info: [testing.home.arpa.] zone not found
2024-03-15T16:22:49+1300 error: [testing.home.arpa.] zone event 'load' failed (not exists)
That’s expected, we now create the source file in dnscontrol, do our dnscontrol push –providers bind to create the zone file then copy and we’re in business.
Create the zone in dnscontrol:
cat >source/testing.home.arpa.js<<EOF
D("testing.home.arpa", REG_NONE,
DnsProvider(BIND),
DnsProvider(DNS_MASTER),
DefaultTTL(3600),
NAMESERVER('localhost.'),
NAMESERVER_TTL('2d'),
A('loopback', '127.0.0.1')
)
EOF
Generate the zone file locally:
dnscontrol push --providers bind
******************* Domain: catalog.home.arpa
******************** Domain: testing.home.arpa
File does not yet exist: "zones/testing.home.arpa.zone" (will create)
1 correction (bind)
#1: + CREATE testing.home.arpa SOA 127.0.0.1. dene.slush.ca. 14400 3600 604800 3600 ttl=300
+ CREATE testing.home.arpa NS localhost. ttl=172800
+ CREATE loopback.testing.home.arpa A 127.0.0.1 ttl=3600
WRITING ZONEFILE: zones/testing.home.arpa.zone
SUCCESS!
Done. 1 corrections.
Now copy the file to Knot DNS and issue a zone-reload and we’re set.
Finis.
It’s a bit long winded, and definitely more convoluted than just using say, route53 but if you host your own you know that’s a lot easier than dealing with adding zone config, reloading the server and all that jazz. We let dnscontrol do the heavy lifting and just have to copy a file once in awhile.