DNS and DNSSEC

Knot DNS automatic DNSSEC signing, the secure way

Turning on DNSSEC in Knot is one line, which is exactly why so many zones are signed, look green, and are not validated by anyone. The keys exist, the signatures exist, and the parent has never heard of either.

The short answer

Assign a Knot policy with automatic key management: ECDSA P-256, NSEC3 with zero iterations and no salt, explicit TTLs, and a signature refresh window of days, not hours. Keep the KASP database readable only by knotd, keep the zone file unsigned, publish CDS, put the DS at the parent, then confirm the submission.

Updated Houssam Hammoudi, CTOTested with Knot DNS 3.5.4, BIND delv 9.20.29

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

DNSSEC protects a zone only when a resolver can build a chain of trust from the root to your records. Signing is half of that chain. The other half is a DS record in the parent zone, which only your registrar can publish.

Knot makes the signing half easy. dnssec-signing: on generates keys, signs every record and keeps the signatures fresh. Many operators stop there. The zone serves RRSIG records, a quick dig +dnssec looks right, and no resolver on the internet validates any of it, because there is no DS at the parent. The zone is still open to spoofed answers.

The second failure is quieter. The KASP database holds the private keys. If it is world-readable, sits on a shared volume or is copied into a backup that many people can read, anyone with a copy can sign records for your zone.

The third failure is the default denial of existence. Plain NSEC lets anyone list every name in the zone. Many guides then enable NSEC3 with a salt and many iterations, which RFC 9276 now forbids because it costs resolvers CPU and gives no real protection.

What the docs say

Make sure to set the KASP database permissions correctly. For manual key management, the database must be readable by the server process. For automatic key management, it must be writeable. If no HSM is used, the database also contains private key material – don't set the permissions too weak.

Source: Knot DNS 3.5 docs, Automatic DNSSEC signing

When the initial keys are automatically generated for the first time, the KSK is actually in the ready state, allowing the initial parent DS submission to take place automatically.

Source: Knot DNS 3.5 docs, Automatic KSK management

If NSEC3 must be used, then an iterations count of 0 MUST be used to alleviate computational burdens.

Source: RFC 9276, Section 3.1

The Knot docs describe the signing side in detail. They do not tell you that nothing is secure until a DS record exists at the registrar, and the "ready" state in the log is easy to read as "done". The server logs KSK submission, waiting for confirmation and keeps waiting until you either configure a parent check or run knotc zone-ksk-submitted.

The secure configuration

/etc/knot/knot.conf:

yaml
server:
    rundir: "/run/knot"
    user: knot:knot                # drop root after binding port 53
    listen: [ 0.0.0.0@53, ::@53 ]

log:
  - target: syslog
    any: info

database:
    storage: "/var/lib/knot"      # KASP DB (private keys) is /var/lib/knot/keys

remote:
  - id: parent_ns                 # a validating resolver, or the parent's servers
    address: 198.51.100.53@53

submission:
  - id: parent_check
    parent: [ parent_ns ]
    check-interval: 1h            # poll the parent for the new DS
    parent-delay: 1d              # wait for the parent's DS TTL before moving on

policy:
  - id: secure
    algorithm: ecdsap256sha256    # algorithm 13: small signatures, universal support
    ksk-lifetime: 365d            # a finite KSK lifetime; each roll needs a new DS
    zsk-lifetime: 30d
    rrsig-lifetime: 14d
    rrsig-refresh: 7d             # re-sign with a week left: survives a long outage
    propagation-delay: 1h         # time for every secondary to serve a change
    dnskey-ttl: 3600
    zone-max-ttl: 86400           # set explicitly; rollover timing depends on it
    nsec3: on                     # no zone walking by plain NSEC
    nsec3-iterations: 0           # RFC 9276: zero extra iterations
    nsec3-salt-length: 0          # RFC 9276: no salt
    cds-cdnskey-publish: always   # parents that scan CDS (RFC 8078) can pick it up
    ksk-submission: parent_check

zone:
  - domain: example.com
    file: "/etc/knot/zones/example.com.zone"
    dnssec-signing: on
    dnssec-policy: secure
    zonefile-sync: -1             # never write signatures back into the source file
    zonefile-load: difference-no-serial
    journal-content: all          # the signed zone survives restarts via the journal
    semantic-checks: on
    # no acl: zone transfers and updates are refused by default

File permissions for the key store:

bash
# The KASP DB holds private keys. Only knotd may read or write it.
chown -R knot:knot /var/lib/knot
chmod 0770 /var/lib/knot/keys
chmod 0660 /var/lib/knot/keys/*.mdb
# Back it up encrypted, and exclude it from any shared or cloud-synced volume.

Put the DS at the parent, then tell Knot:

bash
keymgr example.com. ds          # prints DS records for the KSK (digest 2 = SHA-256)
# Submit the digest type 2 DS at the registrar. Wait until the parent serves it.
knotc zone-ksk-submitted example.com   # only needed without a working parent check

Prove it

The test (secure-tests/knot-dns-automatic-dnssec-signing/run.sh) runs Knot 3.5.4 in one container with the policy above and lab paths. It shows the zone before and after signing, validates with delv, confirms the KSK submission, and checks the key store and the transfer ACL.

Before, with dnssec-signing: off, there is no DNSKEY at the apex:

bash
kdig @127.0.0.1 example.com DNSKEY +dnssec +norecurse | grep -E "status|ANSWER:"
text
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 3091
;; Flags: qr aa; QUERY: 1; ANSWER: 0; AUTHORITY: 1; ADDITIONAL: 1

After dnssec-signing: on and knotc reload:

bash
knotc zone-status example.com
keymgr example.com list -e
text
[example.com.] role: master | serial: 2026092402 | re-sign: +6D23h59m57s

4c6c34a7e34628c1273462baeba11ad6afde8cef ksk=no  zsk=yes tag=58227 algorithm=13 size=256  public-only=no  for-later=no  missing=no  created=1790281015 pre-active=0 publish=1790281015 ready=0 active=1790281015 retire-active=0 retire=0 post-active=0 revoke=0 remove=0
c3690a5d00ae3edc9e0b8a854b2212ce2b31b88d ksk=yes zsk=no  tag=44598 algorithm=13 size=256  public-only=no  for-later=no  missing=no  created=1790281015 pre-active=0 publish=1790281015 ready=1790281015 active=0 retire-active=0 retire=0 post-active=0 revoke=0 remove=0

The next re-sign is seven days out: rrsig-lifetime 14d minus rrsig-refresh 7d. The KSK is ready, not active. It signs the DNSKEY set, but Knot is still waiting for the DS.

bash
kdig @127.0.0.1 www.example.com A +dnssec +norecurse +noall +answer
text
www.example.com.    	3600	IN	A	192.0.2.10
www.example.com.    	3600	IN	RRSIG	A 13 3 3600 20261008201655 20260924184655 58227 example.com. pAB9UpNePsNEQMSukwe/QB35FNs2ywKBHb7pfkusHrM4D0sCPsg7IqdocPv9rpBUOJnb4xYytm5dsTGO/zq/jw==

Denial of existence uses NSEC3 with the RFC 9276 parameters 1 0 0 - (SHA-1, zero iterations, no salt):

bash
kdig @127.0.0.1 nothere.example.com A +dnssec +norecurse +noall +authority
text
example.com.        	3600	IN	SOA	ns1.example.com. hostmaster.example.com. 2026092402 7200 3600 1209600 3600
onib9mgub9h0rml3cdf5bgrj59dkjhvk.example.com. 3600	IN	NSEC3	1 0 0 - gufvra2sfio8rsfp7uo41e8ad1kr41fh NS SOA RRSIG DNSKEY NSEC3PARAM CDS CDNSKEY
example.com.        	3600	IN	RRSIG	SOA 13 2 3600 20261008201655 20260924184655 58227 example.com. W6Plo+JA4yiOwRNuJGykVaog05LSse5JA8zLkRrhcCr6o3gruYj41KD4SrJHEh39G91gEavG+rNmcS8qozb1Ow==
onib9mgub9h0rml3cdf5bgrj59dkjhvk.example.com. 3600	IN	RRSIG	NSEC3 13 3 3600 20261008201655 20260924184655 58227 example.com. zLnPH1KbIVZMHHBUD7sPTtYSBhanANnIITyRi7mYfAJrPIoC0TsrNg5z4L4T02d8q3HWDwhqA4rKQueSV5hoGA==

The published CDS record and the DS for the registrar match:

bash
kdig @127.0.0.1 example.com CDS +norecurse +short
keymgr example.com ds 44598
text
44598 13 2 142AE6815A3147C375BB7120CC9878DDAC147D99B560541C4F5BB668EA83BE45

example.com. DS 44598 13 2 142ae6815a3147c375bb7120cc9878ddac147d99b560541c4f5bb668ea83be45
example.com. DS 44598 13 4 6e3ba9eb62f18e9342888f13ae7fdff3870fa3db2dc5c364036adc377ecdb7afa8e7ade03941a192c4b54c1ce257321f

With that DS as the trust anchor (standing in for the parent), a validator accepts the answer:

bash
delv @127.0.0.1 -a anchor.conf +root=example.com www.example.com A
text
; fully validated
www.example.com.	3600	IN	A	192.0.2.10
www.example.com.	3600	IN	RRSIG	A 13 3 3600 20261008201655 20260924184655 58227 example.com. pAB9UpNePsNEQMSukwe/QB35FNs2ywKBHb7pfkusHrM4D0sCPsg7Iqdo cPv9rpBUOJnb4xYytm5dsTGO/zq/jw==

Once the DS is live, confirm the submission. The KSK gets an active time:

bash
knotc zone-ksk-submitted example.com
keymgr example.com list
text
OK

4c6c34a7e34628c1273462baeba11ad6afde8cef 58227 ZSK ECDSAP256SHA256 created=1790281015 publish=1790281015 active=1790281015
c3690a5d00ae3edc9e0b8a854b2212ce2b31b88d 44598 KSK ECDSAP256SHA256 created=1790281015 publish=1790281015 ready=1790281015 active=1790281019

The server log for the whole run:

text
notice: [example.com.] DNSSEC, KSK submission, waiting for confirmation
info: [example.com.] DNSSEC, key, tag 44598, algorithm ECDSAP256SHA256, KSK, public, ready, active+
info: [example.com.] DNSSEC, key, tag 58227, algorithm ECDSAP256SHA256, public, active
info: [example.com.] DNSSEC, successfully signed, serial 2026092402, new RRSIGs 12
info: [example.com.] DNSSEC, next signing at 2026-10-01T20:16:55+0000
notice: [example.com.] DNSSEC, KSK submission, confirmed
info: [example.com.] DNSSEC, key, tag 58227, algorithm ECDSAP256SHA256, public, active
info: [example.com.] DNSSEC, key, tag 44598, algorithm ECDSAP256SHA256, KSK, public, active

The key store is closed to other users, the source file stays unsigned, and a transfer without an ACL is refused:

bash
ls -l /storage/keys
grep -c RRSIG /config/example.com.zone
kdig @127.0.0.1 example.com AXFR +tcp
text
total 52
-rw-rw---- 1 knot knot 45056 Sep 24 20:16 data.mdb
drwxr-x--- 2 knot knot  4096 Sep 24 20:16 keys
-rw-rw---- 1 knot knot  1856 Sep 24 20:17 lock.mdb

0

;; ERROR: server replied with error 'NOTAUTH'
;; ERROR: failed to query server 127.0.0.1@53(TCP)

Mistakes people make

Signed, served, and never chained

A signed zone without a DS at the parent is "insecure" to every resolver. It behaves exactly like an unsigned zone. Check the parent with kdig example.com DS @<a parent server> and validate with delv from a machine that uses the real root trust anchor.

Reading "ready" as "done"

The first KSK starts in the ready state and Knot logs KSK submission, waiting for confirmation. The zone is signed, so it is tempting to move on. Until you confirm, the KSK has no active time and the submission stays open. Confirm it once the DS is live, or configure a submission parent check so Knot confirms it by asking the parent.

NSEC3 with a salt and 100 iterations

Old guides copied from BIND tutorials set salts and high iteration counts. RFC 9276 requires 0 iterations and recommends no salt. Many resolvers now treat high iteration counts as insecure or return SERVFAIL. Knot's defaults are already 0 and 0; do not override them upward.

Letting knotd write signatures into the zone file

With zonefile-sync at its default, Knot flushes the signed zone back to the file. Your git-managed source then fills with RRSIG records, and the next commit either loses them or fights with them. Keep zonefile-sync: -1 and journal-content: all.

Leaving zone-max-ttl and dnskey-ttl implicit

Knot computes them from the zone if you do not set them. One record added later with a long TTL silently stretches every rollover step. Set both, and keep the zone's largest TTL at or under zone-max-ttl.

Checklist

  • Assign an explicit policy with ecdsap256sha256 (or ed25519) to every signed zone.
  • Set nsec3-iterations: 0 and nsec3-salt-length: 0 if you use NSEC3.
  • Set dnskey-ttl, zone-max-ttl and propagation-delay explicitly.
  • Set rrsig-refresh to several days so a stopped signer does not break the zone overnight.
  • Set zonefile-sync: -1 and journal-content: all.
  • Make the KASP database directory 0770 knot:knot and exclude it from shared volumes.
  • Keep backups of the KASP database encrypted.
  • Submit the digest type 2 DS from keymgr <zone> ds at the registrar.
  • Confirm the DS with kdig DS against the parent, then run knotc zone-ksk-submitted or configure a submission parent check.
  • Validate from outside with delv using the real root trust anchor.
  • Confirm kdig AXFR from an unlisted address returns NOTAUTH.

Knot will sign anything you point it at. Whether anyone believes those signatures is decided one level up, at the parent, so finish the job there.

H2-CPQE

Learn it on a live range

DNSSEC with Knot, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.

Start free

The Dome

Want it run for you?

The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.

See the Dome