feat(provision): Phase 2 — migrate SSH host keys to clan vars

Replaces the gitignored host-keys/ directory with clan vars as the
authoritative storage for SSH host keys. Keys are now generated as
sops-binary-encrypted clan var files (admin-key only) and checked into
vars/per-machine/<target>/openssh/, eliminating the plaintext private
key that previously had to live outside the repo.

Changes:
- modules/clan/ssh-host-key.nix: clan vars generator for the ed25519
  SSH host key pair (neededFor="activation" — not mapped to sops.secrets,
  delivered via tarball baking for LXC or --pre-format-files for VMs)
- flake.nix: add clanCore module + required settings to every mkTarget;
  deduplicate bundled disko/sops-nix via follows; all 27 hosts eval clean
- flake.lock: updated to reflect the new follows constraints
- scripts/lib/clan-vars.sh: new helper library with
  clan_ssh_key_exists / clan_ssh_pubkey_path / clan_decrypt_ssh_key /
  clan_generate_ssh_key for use by the provisioning and sync scripts
- scripts/secrets/sync-host-keys.sh: queue_host_sync() now checks clan
  vars first; generates via clan_generate_ssh_key if no key exists;
  derives age fingerprint from clan pub key for .sops.yaml registration
- scripts/proxmox/create-proxmox-resource.sh: key management simplified
  (sync-host-keys.sh now generates the key if missing, so the inline
  prepare-host-key.sh call is gone); sync_remote_host_keys() decrypts
  the clan key into a temp dir and scps just the two files to the node
  when a clan key exists, falling back to the old host-keys/ scp for
  any remaining legacy entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B2EJ4qTsM5KUqhS5c3GAwx
This commit is contained in:
2026-07-25 18:22:33 +10:00
co-authored by Claude Sonnet 4.6
parent a9745b594b
commit a63e1c70c3
6 changed files with 218 additions and 75 deletions
+31 -20
View File
@@ -59,6 +59,10 @@ source "${repo_root}/scripts/env.sh"
source "${repo_root}/scripts/lib/nix-eval.sh"
# shellcheck source=../lib/confirm.sh
source "${repo_root}/scripts/lib/confirm.sh"
# shellcheck source=../lib/sops-age.sh
source "${repo_root}/scripts/lib/sops-age.sh"
# shellcheck source=../lib/clan-vars.sh
source "${repo_root}/scripts/lib/clan-vars.sh"
sync_keys="${repo_root}/scripts/secrets/sync-host-keys.sh"
@@ -485,19 +489,10 @@ echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '
nix_extra_opts
# --- make sure this target has a registered host key --------------------
# sync-host-keys.sh is idempotent and generates the key (via clan vars) if
# no key exists yet -- the old inline prepare-host-key.sh call is gone.
echo
echo "==> Ensuring host key exists and is registered..."
_host_keyfile="${repo_root}/host-keys/${flake_target}_ssh_host_ed25519_key"
if [[ ! -f "$_host_keyfile" ]]; then
echo " No host key found for ${flake_target}; generating one via prepare-host-key.sh..."
_prepare_host_key="${repo_root}/scripts/secrets/prepare-host-key.sh"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would run: bash ${_prepare_host_key} ${flake_target}"
else
bash "$_prepare_host_key" "$flake_target"
fi
fi
unset _host_keyfile _prepare_host_key
sync_args=("$flake_target")
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
bash "$sync_keys" "${sync_args[@]}"
@@ -647,21 +642,37 @@ ensure_remote_repo() {
fi
}
# --- sync locally-managed host-keys/ to the node ---------------------------
# Gitignored (see .gitignore), so `git pull` above never carries it -- both
# build paths need it present as NIXOS_HOST_KEYS_DIR / --pre-format-files
# input on the node itself now that the build runs there. scp (not rsync,
# not already a dependency anywhere else in this repo) mirrors how this
# script already transfers the --image case below.
# --- 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.
sync_remote_host_keys() {
echo
echo "==> Syncing host-keys/ to ${node}..."
echo "==> Syncing host key for ${flake_target} to ${node}..."
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would copy ${repo_root}/host-keys/ to ${ssh_target}:${remote_repo_dir}/host-keys/"
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'"
scp -pr "${repo_root}/host-keys/." "${ssh_target}:${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/"
fi
}
# --- build (or reuse an image already on the node) ------------------------