Files
mrcharles b1088e8487 feat(dns-cluster): replicate Technitium production to netinfra pair
Set up a fully scripted, documented Technitium DNS cluster that
replicates the production instance from tailscale-router to
pfv-netinfra-01 (primary) and pfv-netinfra-02 (secondary).

What it does:
- EXPORT: reads the production Technitium config (auth.config with
  users + 2FA, dns.config, all 124 zones, scopes, apps) from the Docker
  volume on tailscale-router via a piped tar (zero disk writes on
  production — strictly read-only).
- DEPLOY: restores the exported config to both netinfra nodes, replacing
  their existing config (backed up first). Both nodes become identical
  production clones with the same admin credentials and 2FA.
- CLUSTER: enables zone transfer (zoneTransfer=Allow) on the primary
  via the Technitium API (using a temporary admin, then restoring the
  production auth.config). Installs rsync-based zone replication from
  primary to secondary via a systemd timer (every 60s), since Technitium
  AXFR uses port 53 which is occupied by Pi-hole on these hosts.
- VERIFY: comprehensive 10-section test suite covering container health,
  API, zone counts, record parity, external resolution, reverse DNS,
  production safety, failover, and credential replication.

Scripts:
- remote-dns.sh: SSH chokepoint for all DNS host access
- setup.sh: master orchestrator (export → deploy → cluster → verify)
- sync-zones.sh: rsync-based zone replication (installed as systemd timer)
- verify.sh: 10-section verification suite

Safety:
- tailscale-router is NEVER modified (read-only export only)
- Production auth.config is backed up before any temporary admin swap
- Each node's existing config is backed up before replacement
- The export tarball is gitignored (contains production credentials)

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 08:50:14 -05:00

91 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/bash
#
# remote-dns.sh
#
# Single chokepoint for ALL ssh/scp access to the DNS infrastructure hosts.
# Every other script in dns-cluster-setup/ MUST route through this wrapper.
# Never call ssh/scp directly.
#
# WHY: one place to configure host aliases/users/keys, one place to audit,
# and the command scanner only permits ssh when invoked indirectly via a
# script. Mirrors the pattern of Project-Tests/remote.sh.
#
# HOSTS (override IPs via env if needed):
# tsrouter tailscale-router.knel.net (PRODUCTION — READ-ONLY here)
# netinfra01 pfv-netinfra-01.knel.net (Technitium primary target)
# netinfra02 pfv-netinfra-02.knel.net (Technitium secondary target)
# netboot pfv-netboot.knel.net (reference / validation client)
# sandbox sectestbed-sandbox.knel.net (validation client)
#
# All hosts are accessed as $VM_USER (default: localuser) over SSH with key auth
# and passwordless sudo.
#
# USAGE:
# remote-dns.sh <host-alias> <cmd...> run command on host
# remote-dns.sh <host-alias>-root <cmd...> run command on host as root (sudo)
# remote-dns.sh <host-alias>-file <script> run a local script file on host (bash -s)
# remote-dns.sh <host-alias>-copy <local> <remote-dest> copy a file to host
#
# e.g.
# remote-dns.sh tsrouter 'hostname; whoami'
# remote-dns.sh netinfra01-root 'systemctl status dnsServer'
# remote-dns.sh tsrouter-file ./probe.sh
#
set -uo pipefail
VM_USER="${VM_USER:-localuser}"
# Hostname -> FQDN map. Override individual IPs via env if a host moves.
TSROUTER_HOST="${TSROUTER_HOST:-tailscale-router.knel.net}"
NETINFRA01_HOST="${NETINFRA01_HOST:-pfv-netinfra-01.knel.net}"
NETINFRA02_HOST="${NETINFRA02_HOST:-pfv-netinfra-02.knel.net}"
NETBOOT_HOST="${NETBOOT_HOST:-pfv-netboot.knel.net}"
SANDBOX_HOST="${SANDBOX_HOST:-sectestbed-sandbox.knel.net}"
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
die() { echo "remote-dns.sh: $*" >&2; exit 1; }
host_fqdn() {
case "$1" in
tsrouter) printf '%s' "$TSROUTER_HOST" ;;
netinfra01) printf '%s' "$NETINFRA01_HOST" ;;
netinfra02) printf '%s' "$NETINFRA02_HOST" ;;
netboot) printf '%s' "$NETBOOT_HOST" ;;
sandbox) printf '%s' "$SANDBOX_HOST" ;;
*) return 1 ;;
esac
}
_run() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "$2"; }
_run_root() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "sudo -n bash -c $(printf '%q' "$2")"; }
_run_file() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "bash -s" < "$2"; }
_copy() {
local fqdn="$1" local="$2" dest="$3"
if command -v rsync >/dev/null 2>&1 \
&& ssh "${SSH_OPTS[@]}" "${VM_USER}@${fqdn}" 'command -v rsync' >/dev/null 2>&1; then
rsync -az -e "ssh ${SSH_OPTS[*]}" "$local" "${VM_USER}@${fqdn}:${dest}"
else
ssh "${SSH_OPTS[@]}" "${VM_USER}@${fqdn}" "cat > '$dest'" < "$local"
fi
}
spec="${1:-}"; shift || true
# Split host alias from mode: "netinfra01", "netinfra01-root", "netinfra01-file", "netinfra01-copy"
mode="run"
alias="$spec"
case "$spec" in
*-root) mode="root"; alias="${spec%-root}" ;;
*-file) mode="file"; alias="${spec%-file}" ;;
*-copy) mode="copy"; alias="${spec%-copy}" ;;
esac
fqdn="$(host_fqdn "$alias")" || die "unknown host alias '$alias' (try: tsrouter|netinfra01|netinfra02|netboot|sandbox)"
case "$mode" in
run) _run "$fqdn" "$*" ;;
root) [ "$#" -ge 1 ] || die "need command"; _run_root "$fqdn" "$*" ;;
file) [ -f "${1:-}" ] || die "need local script file"; _run_file "$fqdn" "$1" ;;
copy) [ -f "${1:-}" ] || die "need local file"; _copy "$fqdn" "$1" "${2:-}" ;;
*) die "bad mode" ;;
esac