"I'm not proud of being a congenital pain in the ass. But I will take money for it."

Converting BIND9 hidden DNSSEC signer to Knot

Mon 05 September 2022 | -- (permalink)

I've been using BIND9 as a hidden bump-on-the-wire DNSSEC signer for about a dozen years now. The configuration was excessively complex, mostly for historical reasons that were my own fault, but it was time to clean it up, and I wanted to try Knot as an alternative to BIND9.

Basic hidden bump-on-the-wire configuration is the same for any DNSSEC signer:

  1. I edit plain old pre-DNSSEC zone files (with Emacs, yes this is old school), check them into a git repository, then push a button to tell the signer to wave its magic wand.

  2. The signer picks up my zone files, adds and updates DNSKEY, RRSIG, and NSEC RRs, makes the signed zones available as a DNS primary server, and notifies the secondaries that new versions are available.

  3. The secondaries pick up the updated zones, and they're the service that the outside world sees. Nobody but the secondaries talks to my signer.

  4. The signer remains active even when I'm not changing anything, because signatures expire, and keys may be configured to time out, so the signer needs to keep this stuff up to date on its own, which means that the SOA SERIAL number presented to the world may not be the one in my hand-edited master file.

Extracting old data from BIND9

Extracting the old data from BIND9 was relatively straightforward. Due to having recently gotten out of the business of hosting DNS primaries for other people, I had only my own zone to worry about, so all I needed was the current zone master file and its DNSSEC keys.

Due to the way that bump-on-the-wire signing works in BIND9, there are really two separate zones as far as BIND9 is concerned: the unsigned zone, and the signed zone. I wanted to extract the latest copy of the unsigned zone from BIND9's journal file, so I did:

named-compilezone -j -s relative -o extracted.zone -f text -F text example.org example.zone

If you already have a good copy of the unsigned master file you can skip this step, it was mostly paranoia on my part. The only thing that was likely to be different was the SOA SERIAL, and since I use the "unixtime" convention for generating serial numbers, I'll be updating that to something current as part of the migration anyway.

Extracting the keys was even easier, because BIND9 just keeps them all in a messy pile of disk files with obscure names like:

Kexample.org.+014+42103.key
Kexample.org.+014+42103.private
Kexample.org.+014+60548.key
Kexample.org.+014+60548.private

example.org is the zone name, in this case the keys are ECDSAP384SHA384 (algorithm number 14) with Key IDs 42103 and 60548. The public keys are in the .key files, the private keys are in the .private files. There are two keysets because one set is a ZSK and the other is a KSK: you can't tell which is which just from the filenames, but there's a comment in each of the .key files.

At any rate, Knot will want all of the key files associated with a particular zone.

At this point the alert reader might be wondering about all the DNSSEC data that's not present in the unsigned master file. Short answer: we could salvage that if necessary, but there's no real need unless the zone in question is so large that re-signing the whole zone will take a significant amount of time. The easy path is just to let Knot re-sign the whole zone when it takes control, so long as we keep the same keys this will just look like normal maintenance to the outside world.

Extracting old data from something other than BIND9

Sorry, I have not tried this. In theory any signing authoritative name server should have the same information, somewhere, but extracting it from a particular implementation may be exciting.

Keys, in particular, may be a problem. The general problem here partitions into two cases:

  1. Software key store, where the problem is just finding, extracting, and converting the keys; or

  2. Hardware Signing Module (HSM) where the keys live on separate hardware designed with the express purpose of making it impossible for you to extract the private keys.

The first case is tedious; the second case is impossible (if it's not, demand a refund on that HSM...after extracting the keys). Knot claims to support using a HSM via PKCS #11, I have no reason to disbelieve this but haven't tried it myself, and identifying the keys to be used correctly when switching from one application to another may be challenging.

If it's "just" a software problem, well...at the end of the day there are only so many ways to represent key pairs for the handful of algorithms that DNSSEC supports, so this is mostly a data retrieval and decoding problem. Knot's keymgr tool knows how to import several key formats directly, including so-called "PEM" format, so if you can coerce your private keys into anything that OpenSSL can handle, you should be able to import them into Knot.

Alternatively, you could just decide that it's time for a key roll, in which case you will want to look at the keymgr man page.

Installing Knot

sudo apt-get install knot

Hard, I know. Debian Bullseye is only at Knot version 3.0.5, upstream is at version 3.2, but I didn't find any pressing need for bleeding edge so I just went with what APT installed.

Importing DNSSEC keys into Knot

This was the part that took me longest to figure out, for what turned out to be very silly reasons: I mis-read the manual page, and the error message telling me that I'd gotten the command line syntax for the keymgr tool wrong was, um, not helpful. To save others the tuition I paid for this: for most keymgr commands, the syntax is

keymgr <zone-name> <command> <arguments>

So, once I had that sorted out, importing keys was simple:

sudo keymgr example.org import-bind Kexample.org.+014+42103.key
sudo keymgr example.org import-bind Kexample.org.+014+60548.key

As noted in the Knot documentation, both files (.key and .private) must be present, even though one only specifies one of them per command. Supposedly one can give either filename, or omit the .key / .private suffix entirely, but I didn't try that.

Since keymgr reads /etc/knot/knot.conf to figure out where to put the key database, you can't do this until you have at least a minimal knot.conf in place. The minimal version installed by apt-get is probably good enough (it specifies the database / storage directory, and there's no reason to change that) but, full confession, I had already started tinkering with knot.conf by the time I figured out what I was doing wrong with keymgr so if you run into problems here you may want to try again after setting up knot.conf.

Configuration

Knot uses a mutant variant of YAML as its configuration language. To a first approximation, this means that YAML syntax just works, but there are a few things it doesn't support, and a few extensions that would not parse as correct YAML. When in doubt, stick to syntax as shown in the Knot manual.

Most of the following is a straightforward setup for a signing primary with two secondaries, but there are a few peculiarities which you might not want to copy.

Sanitized version of my /etc/knot/knot.conf:

server:
  rundir: "/run/knot"
  user: knot:knot
  listen: [ 0.0.0.0@53, ::@53 ]

log:
  - target: syslog
    any: info

database:
  storage: "/var/lib/knot"

key:
  - id: "ns0-ns1.tsig.example.org"
    algorithm: hmac-sha256
    secret: "..."
  - id: "ns0-ns2.tsig.example.org"
    algorithm: hmac-sha256
    secret: "..."

acl:
  - id: tsig
    key: ["ns0-ns1.tsig.example.org", "ns0-ns2.tsig.example.org"]
    action: transfer

remote:
  - id: ns1
    address: 192.0.2.53
    key: "ns0-ns1.tsig.example.org"
  - id: ns2
    address: 198.51.100.53
    key: "ns0-ns2.tsig.example.org"

policy:
  - id: P-384-manual
    algorithm: ECDSAP384SHA384
    manual: on

template:
  - id: primary
    storage: "/etc/knot/zones"
    file: "%s"
    zonefile-sync: -1
    zonefile-load: difference
    journal-content: all
    serial-policy: unixtime

zone:
  - domain: example.org
    template: primary
    dnssec-signing: on
    dnssec-policy: P-384-manual
    acl: tsig
    notify: [ns1, ns2]

# Local Variables:
# mode:yaml
# compile-command: "knotc reload"
# End:

Taking this from the top:

  • The server, log, and database sections are bog-standard.

  • The key section defines two TSIG keys. The values of the id fields here must match the TSIG key names used by the peers.

  • The acl section defines a single ACL named tsig which allows transfer authenticated by either of the specified TSIG keys.

  • The remote section defines two remotes, associating addresses and a TSIG key with each. This isn't needed for inbound *XFR requests, but is needed for outbound NOTIFY messages (Knot needs to know where to send the notifications and how to sign them).

  • The policy section defines a single DNSSEC policy named P-384-manual, with DNSSEC algorithm ECDSAP384SHA384 and specifying that key rolls are only done under manual control.

    Long term, this should probably be one of the automatic key roll policies, at least for the ZSK. Knot supports automatic KSK rolls via the mechanism described in RFC 7344, but I don't know whether my zone's parent supports that yet, so that's a research topic.

  • The template section defines a template for individual primary zones.

    This is where things start to get weird: I want to be able to edit my zone files with Emacs and keep them under version control, so I'm putting them in a non-standard place (/etc/knot/zones/) and am telling Knot that I want it to leave my hand-edited zone files alone and keep its own additions and changes in the journal file. Oh, and I like the unixtime convention for the SOA SERIAL number so please use that when changing anything, thanks.

    This may happen to fit your usage pattern. It may not. This is not, for example, the way I would configure things for use with zc, and by configuring things this way I've taken on the responsibility for updating the SOA SERIAL number myself when I edit anything by hand (not a big deal since Emacs's dns-mode wants to update that for me by default anyway).

    But anyway, if there is any point in this recipe where you really ought to read the manual rather than just blindly copying what I did, this is it.

  • The zone section defines one zone (example.org), mostly using stuff (template, acl, remotes) defined in the previous sections.

    Given that this sample configuration defines only one zone, everything that's defined in the template could have been defined here instead. More of what's done here could have been done in the template. Just a matter of taste. In general, the point of Knot templates is to minimize the amount of repetitive configuration, so slice and dice this as needed.

  • The Emacs file variables at the end just tell Emacs that the syntax is (approximately) YAML and that when I type M-x compile I want Emacs to run knotc reload for me.

    knotc reload is a moderately large hammer, one might conceivably want to fine tune this, but since part of the point of a hidden signer is that it doesn't get a lot of traffic, keeping things simple here works for me.

Update 2022-09-06

The original version of this article reported that the NOTIFY setup above was untested due to my not having figured out how to tunnel the NOTIFY message past Comcast's DNS filtering. It turns out that both the Knot configuration and the tunnel configuration were fine, the real problem was that the allow-notify ACL on the nsd secondary was rejecting the NOTIFY messages (and was returning FORMERR, which makes no sense -- so many windmills, so little time). After fixing the nsd ACL, notifications from Knot work fine.

Update 2022-09-22

Enabling automatic ZSK rollover turns out to be trivial:

  1. Remove manual: on from the policy definition (and rename the policy if the name is now wrong, but Knot won't care);

  2. knotc reload, or restart knotd, whichever is easier.

Yes, it really is that simple. You can configure the ZSK validity period if you like, but the default is probably fine.

I haven't yet tried enabling KSK rollovers. In theory, this should also be simple, just follow instructions in the manual to enable KSK rollover via CDNSKEY and CDS records, then come back in a few days to see whether anything happened. I haven't been able to find any documentation on whether my zone's parent supports KSK rollover via this mechanism, but if I understand Knot's algorithm correctly nothing terrible will happen if not, the rollover will just never complete. Not urgent, so leaving well enough alone for the moment.