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
# 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
# the build tooling (Nix, etc.). Every run after that just `git pull`s it and
# copies over the locally-managed host-keys/ (gitignored, so a git pull
# alone wouldn't carry it) before building.
# the build tooling (Nix, etc.). Every run after that just `git pull`s it.
# SSH host keys are stored as clan vars (vars/per-machine/<target>/openssh/,
# 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
# 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)
bash "$sync_keys" "${sync_args[@]}"
# If sync-host-keys.sh changed .sops.yaml or secrets/, those changes must be
# committed and pushed before the remote `git pull` below picks them up --
# the PVE node builds from whatever HEAD is checked out there, not the local
# working tree. Detect uncommitted changes and block until the operator
# confirms they've pushed, so the build never runs against a stale flake.
# If sync-host-keys.sh changed .sops.yaml, secrets/, or vars/per-machine/,
# those changes must be committed and pushed before the remote `git pull`
# below picks them up -- the PVE node builds from whatever HEAD is checked
# out there, not the local working tree. Uncommitted clan vars or sops
# 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
_sops_dirty="$(git -C "$repo_root" status --porcelain -- .sops.yaml secrets/ 2>/dev/null || true)"
if [[ -n "$_sops_dirty" ]]; then
_dirty="$(git -C "$repo_root" status --porcelain -- .sops.yaml secrets/ vars/per-machine/ 2>/dev/null || true)"
if [[ -n "$_dirty" ]]; then
echo
echo "==> COMMIT + PUSH REQUIRED before the remote build can succeed:"
echo " sync-host-keys.sh modified .sops.yaml / secrets/ to register the"
echo " new host's sops recipient. The PVE node builds from the git-tracked"
echo " flake, so these changes must be committed and pushed first -- otherwise"
echo " the image build will succeed but the host cannot decrypt secrets on"
echo " first boot (its age key isn't in the encrypted secrets files yet)."
echo " Uncommitted changes in .sops.yaml, secrets/, or vars/per-machine/."
echo " The PVE node builds from the git-tracked flake, so these changes"
echo " must be committed and pushed first -- otherwise the image build will"
echo " succeed but the host cannot decrypt its secrets on first boot."
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
read -rp " Commit and push those changes, then press Enter to continue (Ctrl-C to abort): "
fi
unset _sops_dirty
unset _dirty
fi
# --- VMID: pick one, and refuse to touch anything that already exists ---
@@ -643,36 +646,34 @@ ensure_remote_repo() {
}
# --- sync host key to the node ---------------------------------------------
# Clan-managed keys live in vars/per-machine/ (committed, sops-encrypted),
# so they arrive on the node via `git pull`. But the build scripts expect a
# plaintext key file in host-keys/ (NIXOS_HOST_KEYS_DIR for LXC, or
# --pre-format-files for VM). For clan keys, decrypt locally and scp just the
# two files for this target; for legacy host-keys/ entries, scp the whole dir.
# SSH host keys are stored as clan vars (vars/per-machine/<target>/openssh/).
# Decrypt locally and scp just the two files for this target to the node's
# host-keys/ directory, where the remote build script picks them up via
# NIXOS_HOST_KEYS_DIR (LXC) or --pre-format-files (VM). A target with no
# clan var is an error -- generate one first with sync-host-keys.sh.
sync_remote_host_keys() {
echo
echo "==> Syncing host key for ${flake_target} to ${node}..."
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/"
else
echo "[dry-run] would copy ${repo_root}/host-keys/ to ${ssh_target}:${remote_repo_dir}/host-keys/"
fi
return
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
tmpdir="$(mktemp -d)"
# shellcheck disable=SC2064
trap "rm -rf '${tmpdir}'" RETURN
echo " Decrypting clan SSH key for ${flake_target}..."
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" \
"${tmpdir}/${flake_target}_ssh_host_ed25519_key.pub" \
"${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) ------------------------