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>
102 lines
4.6 KiB
Markdown
102 lines
4.6 KiB
Markdown
# knot-dns
|
|
|
|
Authoritative DNS node — [Knot DNS](https://www.knot-dns.cz/) 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`](../../../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.
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
# 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
|
|
```
|