Files
automations/deployments/knot-dns/cloud-init.yml
T
57_WolveandClaude Opus 5 0c151ce79b feat(knot-dns): add the missing host-level installer
The deployment shipped its payload but not the thing that installs it.
54a5c09 added README, knot.conf, knsctl, zone.tmpl, secrets.conf.example,
the aliases and the MOTD -- but no deploy.sh, no cloud-init.yml, and no
entry in automations.sh's DEPLOYMENTS. `git log --all` confirms deploy.sh
was never committed and it is not gitignored, yet README.md:96 and :100
tell the operator to run it. So the documented install path did not exist.

Alpine only, native, matching the README: Knot binds :53 directly, needs
real client addresses for RRL and cookies, and keeps its DNSSEC key store on
the host filesystem. The RHEL packaging needs EPEL, which nothing here sets
up, so anything that is not Alpine dies with a clear message rather than
half-installing somewhere untested.

Three decisions worth recording:

The include chain is stubbed. knot.conf include:s seven files the `dns` repo
owns; Knot treats a missing include as a config error, so a node the pipeline
has never delivered to would fail conf-check and never start. deploy.sh
writes a placeholder for each one that is ABSENT -- never over a delivered
file -- so the node comes up healthy serving no zones until the pipeline
lands.

TSIG is generated on a primary and required on a secondary. The keys must
match byte for byte, so a secondary that generated its own would
authenticate nothing; it now refuses to deploy without TSIG_AUTHORITIVE and
TSIG_ADMIN. A primary generates both and prints them once. An existing
secrets.conf is never rewritten, so a re-run cannot rotate a key out from
under a running estate.

PRIMARY_ADDR seeds a minimal remotes.conf on a secondary so it can bootstrap
by AXFR before the pipeline runs -- written only when remotes.conf was
absent, verified by re-running against a delivered file and confirming it is
left untouched.

Re-runs apply changes rather than freezing at first deploy, per the pattern
this repo just adopted elsewhere: knot.conf is re-rendered from .env every
run, env-presence is captured before the ":=" defaults, and values passed to
a re-run are written back to .env with the awk-based set_env from 947c899 --
which matters here because a TSIG secret can contain the characters that
broke the sed-based one.

Two bugs caught while testing this, before it shipped:
- the secrets.conf renderer used `++n` as a gsub argument, which awk
  evaluates on every line, not just matching ones -- both keys would have
  received the SAME secret, making the read-only admin key identical to the
  replication key. Increments on a matching line only now.
- the MOTD is a pre-drawn box, so substituting values of a different width
  than their @TOKEN@ shifted the right border on every login. Values are now
  padded to the token's span, measured over an ASCII-only region so it holds
  under busybox awk in the C locale; an over-long value overflows rather than
  being truncated.

Verified: knot.conf renders identity/NSID/listen and leaves the control
socket alone; all seven stubs are created on a fresh node and skipped on a
re-run; the remotes.conf seed fires only for a fresh secondary; .env seeds
every runtime key; the MOTD renders with no leftover tokens and an aligned
border. Not verified: apk, knotc and the service start, which need an actual
Alpine host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:32:45 -05:00

56 lines
2.2 KiB
YAML

#cloud-config
#
# Authoritative Knot DNS node — harden SSH, then deploy, on a fresh Alpine host.
# Native (no container): Knot binds :53 directly. Alpine only.
#
# Fill in REPO_URL and the values in the runcmd block, then paste this as the
# instance user-data.
#
# A SECONDARY needs the primary's TSIG secrets, byte for byte, or nothing will
# ever transfer — take them from the primary's deploy summary (or its
# /etc/knot/secrets.conf) and paste them below. A PRIMARY generates its own and
# prints them once; capture them from the console before they scroll away.
packages:
- git
runcmd:
- hostnamectl set-hostname anycast-dns-3 || true
- |
set -e
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
REPO_BRANCH=main
HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip)
SSH_PORT=22
ALLOWED_IP= # optional: whitelist your client IP in sshguard
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
cd /opt/automations
# Harden SSH: PQ KEX, key-only auth, sshguard. Seeds root from
# globals/authorized_keys (or SSH_KEYS_URL).
if [ "$HARDEN_SSH" = 1 ]; then
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \
bash scripts/harden-ssh.sh
fi
# Host firewall, so deploy.sh's 53/tcp+udp drop-in has somewhere to register.
SKIP_PROMPTS=1 FORCE=1 bash scripts/harden-firewall.sh || true
# Deploy the node. ROLE=primary holds and signs the zones; ROLE=secondary
# transfers them in and signs nothing. NODE_ID becomes server.identity and
# NSID, so make it the estate's node name.
ROLE=secondary \
NODE_ID=ANYCAST-DNS-3 \
PRIMARY_ADDR=10.1.24.64 \
LISTEN='0.0.0.0@53, ::@53' \
DATACENTER='Stockholm SE' \
PEERS='10.1.24.64 10.1.24.68' \
TSIG_AUTHORITIVE=REPLACE_WITH_THE_PRIMARYS_SECRET \
TSIG_ADMIN=REPLACE_WITH_THE_PRIMARYS_SECRET \
SKIP_PROMPTS=1 \
bash deployments/knot-dns/deploy.sh
# The node now serves NO zones — DNS policy and zone data are delivered by
# the `dns` repo's pipeline. Add this node there (and to the primary's
# remotes.conf, unless a catalog zone picks it up) to put it in rotation.