Merge pull request 'refactor(provision): Phase 3 — remove legacy host-keys/ fallback' (#59) from worktree-phase0-provision-ordering-fix into main
Check NixOS configurations / eval-hosts (push) Successful in 10m23s

Merge PR #59: Phase 3 — remove legacy host-keys/ fallback
This commit was merged in pull request #59.
This commit is contained in:
2026-07-25 11:24:55 +00:00
+32 -31
View File
@@ -8,9 +8,12 @@
# script -- there's no multi-gigabyte image to transfer afterward. The first # script -- there's no multi-gigabyte image to transfer afterward. The first
# time a node doesn't have that repo path yet, it's bootstrapped: cloned from # time a node doesn't have that repo path yet, it's bootstrapped: cloned from
# this checkout's own `origin` remote, then scripts/codex-setup.sh installs # this checkout's own `origin` remote, then scripts/codex-setup.sh installs
# the build tooling (Nix, etc.). Every run after that just `git pull`s it and # the build tooling (Nix, etc.). Every run after that just `git pull`s it.
# copies over the locally-managed host-keys/ (gitignored, so a git pull # SSH host keys are stored as clan vars (vars/per-machine/<target>/openssh/,
# alone wouldn't carry it) before building. # committed and sops-encrypted) -- the script decrypts them locally and
# copies only the two files for this target to the node's host-keys/ before
# building. A target with no clan var is an error (generate one first with
# scripts/secrets/sync-host-keys.sh <target>).
# #
# --node (default: $PROXMOX_HOST, see scripts/env.sh) picks which of the two # --node (default: $PROXMOX_HOST, see scripts/env.sh) picks which of the two
# LAN Proxmox nodes this runs against: production, pve1.sweet.home # LAN Proxmox nodes this runs against: production, pve1.sweet.home
@@ -497,27 +500,27 @@ sync_args=("$flake_target")
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run) [[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
bash "$sync_keys" "${sync_args[@]}" bash "$sync_keys" "${sync_args[@]}"
# If sync-host-keys.sh changed .sops.yaml or secrets/, those changes must be # If sync-host-keys.sh changed .sops.yaml, secrets/, or vars/per-machine/,
# committed and pushed before the remote `git pull` below picks them up -- # those changes must be committed and pushed before the remote `git pull`
# the PVE node builds from whatever HEAD is checked out there, not the local # below picks them up -- the PVE node builds from whatever HEAD is checked
# working tree. Detect uncommitted changes and block until the operator # out there, not the local working tree. Uncommitted clan vars or sops
# confirms they've pushed, so the build never runs against a stale flake. # recipients mean the image builds fine but the host cannot decrypt its
# secrets on first boot. Block until the operator confirms they've pushed.
if [[ "$dry_run" -eq 0 ]]; then if [[ "$dry_run" -eq 0 ]]; then
_sops_dirty="$(git -C "$repo_root" status --porcelain -- .sops.yaml secrets/ 2>/dev/null || true)" _dirty="$(git -C "$repo_root" status --porcelain -- .sops.yaml secrets/ vars/per-machine/ 2>/dev/null || true)"
if [[ -n "$_sops_dirty" ]]; then if [[ -n "$_dirty" ]]; then
echo echo
echo "==> COMMIT + PUSH REQUIRED before the remote build can succeed:" echo "==> COMMIT + PUSH REQUIRED before the remote build can succeed:"
echo " sync-host-keys.sh modified .sops.yaml / secrets/ to register the" echo " Uncommitted changes in .sops.yaml, secrets/, or vars/per-machine/."
echo " new host's sops recipient. The PVE node builds from the git-tracked" echo " The PVE node builds from the git-tracked flake, so these changes"
echo " flake, so these changes must be committed and pushed first -- otherwise" echo " must be committed and pushed first -- otherwise the image build will"
echo " the image build will succeed but the host cannot decrypt secrets on" echo " succeed but the host cannot decrypt its secrets on first boot."
echo " first boot (its age key isn't in the encrypted secrets files yet)."
echo echo
git -C "$repo_root" status --short -- .sops.yaml secrets/ || true git -C "$repo_root" status --short -- .sops.yaml secrets/ vars/per-machine/ || true
echo echo
read -rp " Commit and push those changes, then press Enter to continue (Ctrl-C to abort): " read -rp " Commit and push those changes, then press Enter to continue (Ctrl-C to abort): "
fi fi
unset _sops_dirty unset _dirty
fi fi
# --- VMID: pick one, and refuse to touch anything that already exists --- # --- VMID: pick one, and refuse to touch anything that already exists ---
@@ -643,36 +646,34 @@ ensure_remote_repo() {
} }
# --- sync host key to the node --------------------------------------------- # --- sync host key to the node ---------------------------------------------
# Clan-managed keys live in vars/per-machine/ (committed, sops-encrypted), # SSH host keys are stored as clan vars (vars/per-machine/<target>/openssh/).
# so they arrive on the node via `git pull`. But the build scripts expect a # Decrypt locally and scp just the two files for this target to the node's
# plaintext key file in host-keys/ (NIXOS_HOST_KEYS_DIR for LXC, or # host-keys/ directory, where the remote build script picks them up via
# --pre-format-files for VM). For clan keys, decrypt locally and scp just the # NIXOS_HOST_KEYS_DIR (LXC) or --pre-format-files (VM). A target with no
# two files for this target; for legacy host-keys/ entries, scp the whole dir. # clan var is an error -- generate one first with sync-host-keys.sh.
sync_remote_host_keys() { sync_remote_host_keys() {
echo echo
echo "==> Syncing host key for ${flake_target} to ${node}..." echo "==> Syncing host key for ${flake_target} to ${node}..."
if [[ "$dry_run" -eq 1 ]]; then if [[ "$dry_run" -eq 1 ]]; then
if clan_ssh_key_exists "$flake_target" "$repo_root"; then
echo "[dry-run] would decrypt clan SSH key for ${flake_target} and copy to ${ssh_target}:${remote_repo_dir}/host-keys/" echo "[dry-run] would decrypt clan SSH key for ${flake_target} and copy to ${ssh_target}:${remote_repo_dir}/host-keys/"
else
echo "[dry-run] would copy ${repo_root}/host-keys/ to ${ssh_target}:${remote_repo_dir}/host-keys/"
fi
return return
fi fi
ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'" if ! clan_ssh_key_exists "$flake_target" "$repo_root"; then
if clan_ssh_key_exists "$flake_target" "$repo_root"; then echo "ERROR: no clan SSH key found for ${flake_target}" >&2
echo " (expected: ${repo_root}/vars/per-machine/${flake_target}/openssh/ssh_host_ed25519_key/secret)" >&2
echo " Generate one first: bash scripts/secrets/sync-host-keys.sh ${flake_target}" >&2
exit 1
fi
local tmpdir local tmpdir
tmpdir="$(mktemp -d)" tmpdir="$(mktemp -d)"
# shellcheck disable=SC2064 # shellcheck disable=SC2064
trap "rm -rf '${tmpdir}'" RETURN trap "rm -rf '${tmpdir}'" RETURN
echo " Decrypting clan SSH key for ${flake_target}..." echo " Decrypting clan SSH key for ${flake_target}..."
clan_decrypt_ssh_key "$flake_target" "$repo_root" "$tmpdir" clan_decrypt_ssh_key "$flake_target" "$repo_root" "$tmpdir"
ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'"
scp -p "${tmpdir}/${flake_target}_ssh_host_ed25519_key" \ scp -p "${tmpdir}/${flake_target}_ssh_host_ed25519_key" \
"${tmpdir}/${flake_target}_ssh_host_ed25519_key.pub" \ "${tmpdir}/${flake_target}_ssh_host_ed25519_key.pub" \
"${ssh_target}:${remote_repo_dir}/host-keys/" "${ssh_target}:${remote_repo_dir}/host-keys/"
else
scp -pr "${repo_root}/host-keys/." "${ssh_target}:${remote_repo_dir}/host-keys/"
fi
} }
# --- build (or reuse an image already on the node) ------------------------ # --- build (or reuse an image already on the node) ------------------------