Archived
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
107 lines
4.3 KiB
Bash
107 lines
4.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Clan vars helpers: manage SSH host keys stored as clan vars (sops-encrypted
|
|
# binary files under vars/per-machine/<target>/openssh/) instead of the
|
|
# gitignored host-keys/ directory.
|
|
#
|
|
# Layout (per clan's convention):
|
|
# vars/per-machine/<target>/openssh/ssh_host_ed25519_key/secret -- sops binary (admin-encrypted)
|
|
# vars/per-machine/<target>/openssh/ssh_host_ed25519_key.pub/value -- plaintext SSH pubkey
|
|
#
|
|
# Sourced by create-proxmox-resource.sh and sync-host-keys.sh.
|
|
# Depends on sops-age.sh and ssh-host-keys.sh being sourced first (for
|
|
# sops_yaml_admin_pubkey, ssh_pubkey_to_age, and NIX_OPTS).
|
|
|
|
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
|
|
declare -a NIX_OPTS=()
|
|
fi
|
|
|
|
# clan_ssh_key_exists <target> <repo_root>
|
|
# Returns 0 if clan vars hold a SSH host key for <target>, 1 otherwise.
|
|
clan_ssh_key_exists() {
|
|
local target="$1" repo_root="$2"
|
|
[[ -f "${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key/secret" ]]
|
|
}
|
|
|
|
# clan_ssh_pubkey_path <target> <repo_root>
|
|
# Prints the path to the plaintext SSH public key value file.
|
|
clan_ssh_pubkey_path() {
|
|
local target="$1" repo_root="$2"
|
|
echo "${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key.pub/value"
|
|
}
|
|
|
|
# clan_decrypt_ssh_key <target> <repo_root> <dest_dir>
|
|
# Decrypts the sops-encrypted SSH host private key for <target> into <dest_dir>,
|
|
# naming it <target>_ssh_host_ed25519_key (to match NIXOS_HOST_KEYS_DIR
|
|
# conventions that lxc.nix and the disko build already expect). Also copies
|
|
# the plaintext public key. The caller is responsible for protecting and
|
|
# cleaning up <dest_dir>.
|
|
clan_decrypt_ssh_key() {
|
|
local target="$1" repo_root="$2" dest_dir="$3"
|
|
local secret="${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key/secret"
|
|
local pubval="${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key.pub/value"
|
|
local dest_priv="${dest_dir}/${target}_ssh_host_ed25519_key"
|
|
local dest_pub="${dest_dir}/${target}_ssh_host_ed25519_key.pub"
|
|
|
|
nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
|
"sops -d --output-type binary '${secret}'" > "$dest_priv"
|
|
chmod 0600 "$dest_priv"
|
|
cp "$pubval" "$dest_pub"
|
|
}
|
|
|
|
# clan_generate_ssh_key <target> <repo_root>
|
|
# Generates a new SSH host key pair and stores it in clan vars format:
|
|
# - private key: sops binary-encrypted for the admin age key
|
|
# - public key: plaintext value file
|
|
# Idempotent: if the secret already exists, prints a note and returns 0.
|
|
# Requires sops_yaml_admin_pubkey (from sops-age.sh) to be available.
|
|
clan_generate_ssh_key() {
|
|
local target="$1" repo_root="$2"
|
|
local var_base="${repo_root}/vars/per-machine/${target}/openssh"
|
|
local secret_dir="${var_base}/ssh_host_ed25519_key"
|
|
local pubval_dir="${var_base}/ssh_host_ed25519_key.pub"
|
|
|
|
if [[ -f "${secret_dir}/secret" ]]; then
|
|
echo "Clan SSH host key for ${target} already exists -- skipping generation."
|
|
return 0
|
|
fi
|
|
|
|
# Resolve admin age public key from .sops.yaml
|
|
local admin_pubkey
|
|
admin_pubkey="$(sops_yaml_admin_pubkey "${repo_root}/.sops.yaml")"
|
|
if [[ -z "$admin_pubkey" ]]; then
|
|
echo "ERROR: Could not find &admin age key in ${repo_root}/.sops.yaml" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Generate the SSH key pair in a secure temp directory
|
|
local tmpdir
|
|
tmpdir="$(mktemp -d)"
|
|
local priv_tmp="${tmpdir}/ssh_host_ed25519_key"
|
|
|
|
# shellcheck disable=SC2064
|
|
trap "rm -rf '${tmpdir}'" RETURN
|
|
|
|
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
|
|
"ssh-keygen -t ed25519 -N '' -C '${target}' -f '${priv_tmp}'" >/dev/null
|
|
|
|
# Create a minimal sops config that uses only the admin age key -- this
|
|
# prevents sops from merging in ALL recipients from .sops.yaml (which
|
|
# would unnecessarily encrypt for every host's key, not just admin).
|
|
local sops_cfg="${tmpdir}/sops-config.json"
|
|
printf '{"creation_rules":[{"key_groups":[{"age":["%s"]}]}]}\n' \
|
|
"$admin_pubkey" > "$sops_cfg"
|
|
|
|
# Encrypt the private key in sops binary format (admin-only recipient)
|
|
mkdir -p "$secret_dir" "$pubval_dir"
|
|
nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
|
"sops -e --config '${sops_cfg}' --input-type binary '${priv_tmp}'" \
|
|
> "${secret_dir}/secret"
|
|
|
|
# Store the public key as a plaintext value file
|
|
cp "${priv_tmp}.pub" "${pubval_dir}/value"
|
|
|
|
echo "Generated and stored clan SSH host key for ${target}."
|
|
echo " Private key: ${secret_dir}/secret (sops binary, admin-key encrypted)"
|
|
echo " Public key: ${pubval_dir}/value"
|
|
}
|