From e8c4122460a774a953fbf5ad9d0855264cac19ec Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Sat, 25 Jul 2026 21:24:32 +1000 Subject: [PATCH] =?UTF-8?q?refactor(provision):=20Phase=203=20=E2=80=94=20?= =?UTF-8?q?remove=20legacy=20host-keys/=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All actively deployed lxc-* hosts now have clan vars. Remove the legacy scp -pr host-keys/ fallback in sync_remote_host_keys(): instead of silently copying the gitignored directory, error clearly if no clan var exists for the target and tell the operator how to generate one. Also extend the uncommitted-changes check to cover vars/per-machine/ in addition to .sops.yaml and secrets/, since clan vars must be committed before the remote build git-pulls them. Update the script header and sync_remote_host_keys comment to reflect the new clan-only key flow. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01B2EJ4qTsM5KUqhS5c3GAwx --- scripts/proxmox/create-proxmox-resource.sh | 81 +++++++++++----------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/scripts/proxmox/create-proxmox-resource.sh b/scripts/proxmox/create-proxmox-resource.sh index 6a231d6..6c9ce4e 100755 --- a/scripts/proxmox/create-proxmox-resource.sh +++ b/scripts/proxmox/create-proxmox-resource.sh @@ -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//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 ). # # --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//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 + echo "[dry-run] would decrypt clan SSH key for ${flake_target} and copy to ${ssh_target}:${remote_repo_dir}/host-keys/" return fi - ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'" - if clan_ssh_key_exists "$flake_target" "$repo_root"; then - 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" - 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/" + 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/" } # --- build (or reuse an image already on the node) ------------------------