The deployment shipped its payload but not the thing that installs it.54a5c09added 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 from947c899-- 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>
56 lines
2.2 KiB
YAML
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.
|