#!/usr/bin/env bash # # deploy.sh -- deploy an authoritative Knot DNS node on Alpine, native (no # container). Single-purpose box, runs as root. # # What this does: # 1. Installs knot + knot-utils and creates the knot user/directories. # 2. Seeds /etc/knot/.env on first run; on a re-run updates only the values # passed that run, and re-renders knot.conf from it either way. # 3. Renders /etc/knot/knot.conf from the skeleton (@NODE_ID@, @LISTEN@). # 4. TSIG: ROLE=primary generates secrets.conf and prints the keys once; # ROLE=secondary REQUIRES them to be supplied, because they must byte-match # the primary's or no transfer will ever authenticate. # 5. Stubs the seven policy files knot.conf includes, so conf-check passes and # knotd starts clean on a box the `dns` repo has never delivered to. # 6. Installs knsctl, zone.tmpl, the login aliases and the MOTD. # 7. Opens 53/tcp + 53/udp on the host firewall. # 8. conf-check, then enables and starts knot. # # What this does NOT do: manage zones or DNS policy. Those come from the `dns` # repo's pipeline -- see the README's split table. This script never overwrites # a policy file the pipeline owns, nor an existing secrets.conf. # # Idempotent: re-run to change identity/listen/peers or pull a newer knot. # # Self-contained: knot.conf, secrets.conf.example, knsctl, zone.tmpl, # knotdns-aliases.sh, knot-dns.motd and .env.example are embedded as a base64 # tar.gz at the bottom. Rebuild with build.sh after editing the loose files. # # Usage: # ROLE=primary NODE_ID=ANYCAST-DNS-1 bash deploy.sh # ROLE=secondary NODE_ID=ANYCAST-DNS-3 PRIMARY_ADDR=10.1.24.64 \ # TSIG_AUTHORITIVE=... TSIG_ADMIN=... SKIP_PROMPTS=1 bash deploy.sh set -euo pipefail : "${CONF_DIR:=/etc/knot}" : "${DATA_DIR:=/var/lib/knot}" : "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts : "${FORCE:=0}" [[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1 ENV_FILE="$CONF_DIR/.env" # Which keys arrived in this script's ENVIRONMENT has to be recorded BEFORE the # ":=" defaults below, which make an unset variable look like an empty one -- # and a blank written over a live TSIG secret would break every transfer. RUNTIME_KEYS=(NODE_ID ROLE LISTEN PRIMARY_ADDR DATACENTER PEERS) KEYS_FROM_ENV=() for _k in "${RUNTIME_KEYS[@]}"; do if [[ -n "${!_k+x}" ]]; then KEYS_FROM_ENV+=("$_k"); fi done : "${NODE_ID:=}" : "${ROLE:=}" : "${LISTEN:=}" : "${PRIMARY_ADDR:=}" : "${DATACENTER:=}" : "${PEERS:=}" : "${TSIG_AUTHORITIVE:=}" # secondary: must match the primary's : "${TSIG_ADMIN:=}" LISTEN_DEFAULT='0.0.0.0@53, ::@53' log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "Run as root." # Alpine only, deliberately: Knot binds :53 directly and keeps its DNSSEC key # store on the host filesystem, and this estate's nodes are Alpine. The RHEL # packaging needs EPEL, which nothing in this repo sets up, so fail loudly # rather than half-install somewhere untested. _osid="$( [[ -r /etc/os-release ]] && . /etc/os-release 2>/dev/null && echo "${ID:-}" )" [[ "$_osid" == alpine ]] || \ die "This deployment is Alpine-only (detected: ${_osid:-unknown}). Knot on other distros is not packaged or tested here." # --------------------------------------------------------------------------- # Extract embedded archive (falls back to the source dir before build.sh runs) # --------------------------------------------------------------------------- SCRIPT_DIR=$(mktemp -d -t knot-deploy.XXXXXX) trap 'rm -rf "$SCRIPT_DIR"' EXIT grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" \ | tail -n +2 \ | base64 -d \ | tar -xz -C "$SCRIPT_DIR" 2>/dev/null || true if [[ ! -f "$SCRIPT_DIR/knot.conf" ]]; then SRC=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) for f in knot.conf secrets.conf.example knsctl zone.tmpl knotdns-aliases.sh knot-dns.motd .env.example; do [[ -f "$SRC/$f" ]] || die "Missing $f (no embedded payload and not in $SRC -- run build.sh)." cp "$SRC/$f" "$SCRIPT_DIR/$f" done fi # --------------------------------------------------------------------------- # .env: seed once, then update only what this run passed # --------------------------------------------------------------------------- set_env() { # : update KEY in .env, or append if absent # The value goes through the ENVIRONMENT, never interpolated into a sed # script: a TSIG secret or a listen list containing & or the s||| delimiter # would otherwise be silently corrupted or abort the run. local key="$1" val="$2" tmp if [[ ! -f "$ENV_FILE" ]]; then printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE" return 0 fi tmp="$(mktemp)" _SE_KEY="$key" _SE_VAL="$val" awk ' BEGIN { k = ENVIRON["_SE_KEY"]; v = ENVIRON["_SE_VAL"]; seen = 0 } !seen && index($0, k "=") == 1 { print k "=" v; seen = 1; next } { print } END { if (!seen) print k "=" v } ' "$ENV_FILE" > "$tmp" cat "$tmp" > "$ENV_FILE" # rewrite in place: keeps the original mode/owner rm -f "$tmp" } install -d -m 0755 "$CONF_DIR" # On a re-run the deployed values live in .env. Load them into any key NOT # passed this run, so the prompts below do not demand a full retype and # SKIP_PROMPTS=1 does not die over values the node already has. if [[ -f "$ENV_FILE" ]]; then _preloaded=() for _k in "${RUNTIME_KEYS[@]}"; do if [[ " ${KEYS_FROM_ENV[*]-} " == *" ${_k} "* ]]; then continue; fi _v=$(sed -n "s/^${_k}=//p" "$ENV_FILE" | tail -n1) if [[ -z "$_v" ]]; then continue; fi printf -v "$_k" '%s' "$_v" _preloaded+=("$_k") done if (( ${#_preloaded[@]} > 0 )); then log "Reusing ${ENV_FILE} values for: ${_preloaded[*]}" log " (pass VAR=... to change one; it is then written back to .env)" fi fi prompt() { local varname="$1" message="$2" local -n ref="$varname" if [[ -z "${ref:-}" ]]; then [[ "$SKIP_PROMPTS" == "1" ]] && die "$varname required (set it in the environment; running with SKIP_PROMPTS=1)." read -r -p "$message: " ref [[ -n "$ref" ]] || die "$varname required." fi } prompt NODE_ID "Node ID for server.identity / NSID (e.g. ANYCAST-DNS-3)" prompt ROLE "Role: primary or secondary" case "$ROLE" in primary|secondary) ;; *) die "ROLE must be 'primary' or 'secondary' (got '$ROLE')." ;; esac [[ -n "$LISTEN" ]] || LISTEN="$LISTEN_DEFAULT" if [[ "$ROLE" == secondary ]]; then prompt PRIMARY_ADDR "Address of the primary this node transfers from" fi # --------------------------------------------------------------------------- # Packages, user, directories # --------------------------------------------------------------------------- log "Installing Knot DNS..." apk add -q knot knot-utils # The package normally creates knot:knot; make that certain before any chown. getent group knot >/dev/null 2>&1 || addgroup -S knot getent passwd knot >/dev/null 2>&1 || adduser -S -D -H -G knot -s /sbin/nologin knot # storage/journal/kasp/timer/catalog as declared in knot.conf's database: # section, plus the zone root knsctl writes into. for d in database journal keys timer catalog zones; do install -d -m 0750 -o knot -g knot "$DATA_DIR/$d" done install -d -m 0750 -o knot -g knot /run/knot # --------------------------------------------------------------------------- # knot.conf -- re-rendered every run, since it is derived purely from .env # --------------------------------------------------------------------------- log "Rendering $CONF_DIR/knot.conf (NODE_ID=$NODE_ID, ROLE=$ROLE)..." _KC_NODE_ID="$NODE_ID" _KC_LISTEN="$LISTEN" awk ' { gsub(/@NODE_ID@/, ENVIRON["_KC_NODE_ID"]); gsub(/@LISTEN@/, ENVIRON["_KC_LISTEN"]); print } ' "$SCRIPT_DIR/knot.conf" > "$CONF_DIR/knot.conf.new" install -m 0640 -o root -g knot "$CONF_DIR/knot.conf.new" "$CONF_DIR/knot.conf" rm -f "$CONF_DIR/knot.conf.new" # --------------------------------------------------------------------------- # TSIG. A secondary's keys must byte-match the primary's, so they can never be # generated here -- a generated key would authenticate nothing. # --------------------------------------------------------------------------- gen_tsig() { # gen_tsig -> base64 secret on stdout local id="$1" out="" if command -v keymgr >/dev/null 2>&1; then # keymgr prints a whole key: block; take just the secret. out="$(keymgr -t "$id" hmac-sha256 2>/dev/null \ | awk '/secret:/ { sub(/^[[:space:]]*secret:[[:space:]]*/, ""); print; exit }' || true)" fi [[ -n "$out" ]] || out="$(openssl rand -base64 32)" printf '%s' "$out" } SECRETS="$CONF_DIR/secrets.conf" TSIG_PRINTED=0 if [[ -f "$SECRETS" ]]; then log "$SECRETS exists; leaving it alone (TSIG keys are never rotated by a re-run)." elif [[ "$ROLE" == primary ]]; then log "Generating TSIG keys for $SECRETS..." [[ -n "$TSIG_AUTHORITIVE" ]] || TSIG_AUTHORITIVE="$(gen_tsig authortive-tsig)" [[ -n "$TSIG_ADMIN" ]] || TSIG_ADMIN="$(gen_tsig admin-tsig)" TSIG_PRINTED=1 else [[ -n "$TSIG_AUTHORITIVE" && -n "$TSIG_ADMIN" ]] || die \ "A secondary's TSIG keys must match the primary's byte for byte, so they cannot be generated here. Copy them off the primary ($CONF_DIR/secrets.conf) and re-run with: TSIG_AUTHORITIVE= TSIG_ADMIN= bash deploy.sh" fi if [[ ! -f "$SECRETS" ]]; then # Increment only on a MATCHING line: as a gsub argument, `++n` would be # evaluated on every line of the file, so both keys would get the second # secret -- making the read-only admin key identical to the replication key. _TS_AUTH="$TSIG_AUTHORITIVE" _TS_ADMIN="$TSIG_ADMIN" awk ' /REPLACE_ME/ { n++; sub(/REPLACE_ME/, n == 1 ? ENVIRON["_TS_AUTH"] : ENVIRON["_TS_ADMIN"]) } { print } ' "$SCRIPT_DIR/secrets.conf.example" > "$SECRETS.new" install -m 0640 -o root -g knot "$SECRETS.new" "$SECRETS" rm -f "$SECRETS.new" fi # --------------------------------------------------------------------------- # The seven policy files knot.conf includes. The `dns` repo owns them; a fresh # node has none, and Knot treats a missing include as a config error -- so # conf-check would fail and knotd would never start. Stub only what is absent; # never touch a file the pipeline has already delivered. # --------------------------------------------------------------------------- _stubbed=() for f in remotes policy modules templates arpa dnssec public; do [[ -f "$CONF_DIR/$f.conf" ]] && continue cat > "$CONF_DIR/$f.conf" < "$CONF_DIR/remotes.conf" < /etc/profile.d/knotdns-peers.sh chmod 0644 /etc/profile.d/knotdns-peers.sh fi _FQDN="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "$NODE_ID")" # The MOTD is a pre-drawn box, so a value wider or narrower than its @TOKEN@ # would shift the right-hand border on every login. Replace "@TOKEN@ + the run # of spaces after it" with the value padded to that same span, which keeps the # border put. That span is pure ASCII, so byte length == display width and this # stays correct under busybox awk in the C locale (the box-drawing characters # themselves are never measured). A value longer than its span is left to # overflow rather than truncated: a clipped hostname in an ops banner is worse # than a ragged edge. _MD_NODE_ID="$NODE_ID" _MD_ROLE="$ROLE" _MD_DC="${DATACENTER:-unset}" _MD_FQDN="$_FQDN" awk ' function fit(line, tok, val, n, span, pad) { while (match(line, tok " *")) { span = RLENGTH pad = val while (length(pad) < span) pad = pad " " line = substr(line, 1, RSTART - 1) pad substr(line, RSTART + RLENGTH) } return line } { $0 = fit($0, "@NODE_ID@", ENVIRON["_MD_NODE_ID"]) $0 = fit($0, "@ROLE@", ENVIRON["_MD_ROLE"]) $0 = fit($0, "@DATACENTER@", ENVIRON["_MD_DC"]) $0 = fit($0, "@FQDN@", ENVIRON["_MD_FQDN"]) print } ' "$SCRIPT_DIR/knot-dns.motd" > /etc/motd chmod 0644 /etc/motd # --------------------------------------------------------------------------- # .env: seed on first run, else update only what was passed this run # --------------------------------------------------------------------------- if [[ ! -f "$ENV_FILE" ]]; then log "Seeding $ENV_FILE..." install -m 0640 -o root -g knot "$SCRIPT_DIR/.env.example" "$ENV_FILE" for _k in "${RUNTIME_KEYS[@]}"; do set_env "$_k" "${!_k}"; done else # knot.conf is re-rendered from .env above, so letting .env drift from what # was passed would silently redeploy the old identity on the next run. log ".env exists; keeping it (only values passed this run are updated)." for _k in ${KEYS_FROM_ENV[@]+"${KEYS_FROM_ENV[@]}"}; do _cur=$(sed -n "s/^${_k}=//p" "$ENV_FILE" | tail -n1) if [[ "$_cur" == "${!_k}" ]]; then continue; fi set_env "$_k" "${!_k}" log " ${_k}: ${_cur:-} -> ${!_k:-}" done fi # --------------------------------------------------------------------------- # Firewall: authoritative DNS answers on 53, TCP and UDP both. # --------------------------------------------------------------------------- if [[ -d /etc/firewall/ports.d && -x /usr/local/sbin/firewall-apply ]]; then log "Registering 53/tcp + 53/udp with the host firewall..." printf '53/tcp\n53/udp\n' > /etc/firewall/ports.d/knot-dns.rule /usr/local/sbin/firewall-apply else warn "No host firewall found (scripts/harden-firewall.sh not installed); 53/tcp+udp not registered." fi # --------------------------------------------------------------------------- # Validate, then start. -b so a rejected config fails here, not silently. # --------------------------------------------------------------------------- log "Validating configuration..." knotc -b conf-check || die "knotc conf-check failed -- not starting. Fix $CONF_DIR/knot.conf and re-run." rc-update add knot default >/dev/null 2>&1 || true if rc-service knot status >/dev/null 2>&1; then log "Reloading knot..." knotc -b reload || warn "Reload failed; check 'rc-service knot status' and the syslog." else log "Starting knot..." rc-service knot start || die "knot failed to start -- check the syslog (log target is syslog, zone at info)." fi cat <} with authortive-tsig. Add this node's address to the PRIMARY's remotes.conf (or let the catalog zone do it) before it can transfer. EOF fi if [[ "$TSIG_PRINTED" == "1" ]]; then cat <> TSIG KEYS -- SHOWN ONCE. Every secondary needs these byte for byte: TSIG_AUTHORITIVE=${TSIG_AUTHORITIVE} TSIG_ADMIN=${TSIG_ADMIN} Store them out of band. They are in ${SECRETS} on this node. EOF fi cat <> BACK UP THE KASP DATABASE (${DATA_DIR}/keys). It replicates nowhere, and losing it makes every signed domain go bogus until each registrar is updated by hand: knotc -b zone-backup +backupdir /var/backups/knot/\$(date -u +%FT%H) +journal EOF __ARCHIVE_BELOW__ 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 ohWtaP8r7Xext/XSAHgAAA==