Files
automations/deployments/knot-dns
57_WolveandClaude Opus 5 54a5c0931a feat(knot-dns): authoritative Knot DNS node deployment
Native Alpine deployment for the anycast DNS estate -- no Docker, no Caddy,
alongside squid and openbao as an exception to the repo norm. Knot binds :53
directly, needs real client addresses for RRL and DNS cookies, and its DNSSEC
key store must live on the host filesystem.

Deploys a NODE. Zone data lives in the separate dns repo and arrives from its
pipeline. The split is /etc/knot/knot.conf: written here once as a skeleton of
include: lines covering only what belongs to a box (identity, NSID, storage
paths, listen, logging, control socket); everything that is DNS policy --
templates, dnssec policy, remotes/ACLs, modules, the domain inventory and the
zone files -- is delivered by the dns repo.

knsctl replaces adddns.pl and adddnssec.pl, fixing four defects:
- the duplicate check searched for the domain in BIND named.conf double-quote
  syntax (/"$domain"/) against unquoted YAML, so it could never match; only
  the -f zone-file test ever caught anything
- neither script consulted the other class's manifest, so a domain already in
  public.conf could be appended to dnssec.conf and fail the reload AFTER both
  files had been written
- nothing validated before reloading
- the reload was non-blocking, so a rejected config reported success

Its manifest matching is anchored on the YAML key and escapes the dot, so
barsrvno.de and srvnoXde no longer false-positive against srvno.de.

Aliases preserve the existing muscle memory with three corrections: -b on
every triggering knotc command (without it knotc returns OK when the command
was SENT, not when it succeeded); knzr (zone-reload) added alongside knrl
(reload), since reloading one zone's data is the right verb for a record
change and a full reload is only needed when a zone is added or removed; and
serial/NSID helpers that query unicast addresses, because asking the anycast
service address reaches whichever node is nearest and says nothing about
which node is stale.

Break-glass writes (add/remove/edit) warn and audit-log: they are overwritten
by the next pipeline deploy unless the change also lands in git. Removal
refuses to purge DNSSEC keys -- zone-purge +keys is irreversible on Knot
3.5.x, the key trash bin having arrived in 3.6.0 -- and prints the ordering
requirement, since removing a signed zone before the parent DS is withdrawn
is an outage for validating resolvers rather than a graceful shutdown.

deploy.sh, build.sh and cloud-init.yml are deliberately not included yet;
they are blocked on the Knot version decision, which sets the apk pin and
feature availability. See the Status section in the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 00:36:38 -05:00
..

knot-dns

Authoritative DNS node — Knot DNS on Alpine, native (no container), for a global anycast estate with automatic DNSSEC.

Exception to the repo norm, alongside squid and openbao: no Docker, no Caddy, no Let's Encrypt. Knot binds :53 directly, needs real client addresses for RRL and DNS cookies, and its DNSSEC key store must live on the host filesystem. Containerising it buys nothing and costs the host firewall.

What this deploys, and what it does not

This stands up a node. It does not manage zones — that is the dns repo, and its pipeline delivers zone data here.

The split point is /etc/knot/knot.conf. This deployment writes it once as a skeleton of include: lines covering only what belongs to a box: identity, NSID, storage paths, listen addresses, logging, control socket. Everything that belongs to DNS policy — templates, DNSSEC policy, remotes/ACLs, modules, the domain inventory, the zone files — arrives from the dns repo.

Deployed here, once Delivered by the dns repo, continuously
knot.conf skeleton templates.conf, policy.conf, remotes.conf, modules.conf
secrets.conf (TSIG, rendered locally, never in git) dnssec.conf, public.conf, arpa.conf
knsctl, aliases, MOTD, zone.tmpl zones/**/*.zone
packages, users, directories, firewall

Files

File Purpose
knot.conf Bootstrap skeleton. @NODE_ID@ / @LISTEN@ substituted from .env.
secrets.conf.example TSIG key: block. Rendered to /etc/knot/secrets.conf, 0640 root:knot. Never committed.
knsctl Admin CLI. Replaces adddns.pl / adddnssec.pl and fixes four defects in them.
knotdns-aliases.sh /etc/profile.d/ — the existing muscle memory, with -b added everywhere.
knot-dns.motd The cheat-sheet MOTD.
zone.tmpl New-zone skeleton (mirrors dns/skeleton/zone.tmpl).

Roles

ROLE in .env selects what the node is:

  • primary — holds zone files, signs with automatic DNSSEC, owns the KASP database, notifies the secondaries. Exactly one node. Not necessarily public-facing: a hidden (stealth) primary is the recommended shape.
  • secondary — receives zones by AXFR/IXFR with TSIG, serves queries, signs nothing. dnssec-signing must be off; a secondary serves data that is already signed.

Adding a public node is a secondary deploy plus one address in the primary's remotes.conf. With catalog zones configured, the new node self-populates.

What survives from the previous setup

Everything an operator types. knrl, knsc, knzc, knpbz, knsec, knarpa, ozf all still work, from the same paths, on the same box. Three corrections were folded in:

  1. -b on every triggering knotc command. Without it knotc returns OK when the command was sent, not when it succeeded, so a rejected zone file reports green. knrl was a bare knotc reload.
  2. knzr (zone-reload) added next to knrl (reload). Reloading one zone's data is the right verb for a record change — smaller blast radius, and a parse error in one zone cannot disturb the others. knrl reloads configuration and is only needed when a zone is added or removed.
  3. knsctl replaces the Perl scripts, whose duplicate check searched for the domain in BIND named.conf double-quote syntax against unquoted YAML and therefore never matched; which never consulted the other class's manifest, so a domain in public.conf could be appended to dnssec.conf and fail the reload after both files were already written; and which reloaded without validating.

Key material

/var/lib/knot/keys (the KASP LMDB) is node-local to the primary and replicates nowhere. Losing it is the one unrecoverable failure in this system: Knot will happily generate fresh KSKs, every published DS will point at keys that no longer exist, and every signed domain goes bogus until each registrar is updated by hand.

knotc -b zone-backup +backupdir /var/backups/knot/$(date -u +%FT%H) +journal

+journal is not the default — the documented default filter set excludes it, and in this configuration the journal holds the only durable copy of the signed zone and the last real SOA serial. A default zone-backup captures your keys and loses your zone data. Restore order is config → KASP + journal → then start knotd, never the reverse.

Usage

# interactive
bash deploy.sh

# non-interactive (this is what cloud-init.yml does)
ROLE=secondary NODE_ID=ANYCAST-DNS-3 PRIMARY_ADDR=10.1.24.64 \
    SKIP_PROMPTS=1 bash deploy.sh