#!/usr/bin/env bash # Clan vars helpers: manage SSH host keys stored as clan vars (sops-encrypted # binary files under vars/per-machine//openssh/) instead of the # gitignored host-keys/ directory. # # Layout (per clan's convention): # vars/per-machine//openssh/ssh_host_ed25519_key/secret -- sops binary (admin-encrypted) # vars/per-machine//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 # Returns 0 if clan vars hold a SSH host key for , 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 # 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 # Decrypts the sops-encrypted SSH host private key for into , # naming it _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 . 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 # 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" }