Archived
Lift duplicated sops/age and confirm-prompt logic into scripts/lib/
scripts/backup-admin-key.sh, rotate-admin-key.sh, and sync-host-keys.sh each independently resolved sops/age's default key-file path, derived an age pubkey from an identity file, and (two of them) ran `sops updatekeys` the same way -- now shared via scripts/lib/sops-age.sh. Also extracted the "type X to confirm" prompt duplicated across create-proxmox-resource.sh and sync-host-keys.sh into scripts/lib/confirm.sh. Pure extraction, no behavior change -- each call site produces identical commands/output to before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ sops_yaml="${repo_root}/.sops.yaml"
|
|||||||
|
|
||||||
# shellcheck source=env.sh
|
# shellcheck source=env.sh
|
||||||
source "${repo_root}/scripts/env.sh"
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=lib/sops-age.sh
|
||||||
|
source "${repo_root}/scripts/lib/sops-age.sh"
|
||||||
|
|
||||||
# Pin cwd for the same reason rotate-admin-key.sh does: age/sops calls
|
# Pin cwd for the same reason rotate-admin-key.sh does: age/sops calls
|
||||||
# below should never depend on wherever the caller's shell happened to be.
|
# below should never depend on wherever the caller's shell happened to be.
|
||||||
@@ -43,7 +45,7 @@ EOF
|
|||||||
|
|
||||||
dry_run=0
|
dry_run=0
|
||||||
force=0
|
force=0
|
||||||
key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
|
||||||
args=()
|
args=()
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -103,13 +105,13 @@ scratch="$(mktemp)"
|
|||||||
trap 'rm -f "$scratch"' EXIT
|
trap 'rm -f "$scratch"' EXIT
|
||||||
( umask 077; printf '%s\n' "$src_content" > "$scratch" )
|
( umask 077; printf '%s\n' "$src_content" > "$scratch" )
|
||||||
|
|
||||||
src_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$scratch'")" || {
|
src_pub="$(age_pubkey_from_identity_file "$scratch")" || {
|
||||||
echo "ERROR: source doesn't look like a valid age identity (age-keygen -y failed)." >&2
|
echo "ERROR: source doesn't look like a valid age identity (age-keygen -y failed)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
echo " public key: ${src_pub}"
|
echo " public key: ${src_pub}"
|
||||||
|
|
||||||
current_admin_pub="$(grep -E '^ - &admin age1' "$sops_yaml" 2>/dev/null | awk '{print $NF}' || true)"
|
current_admin_pub="$(sops_yaml_admin_pubkey "$sops_yaml")"
|
||||||
if [[ -n "$current_admin_pub" && "$current_admin_pub" != "$src_pub" ]]; then
|
if [[ -n "$current_admin_pub" && "$current_admin_pub" != "$src_pub" ]]; then
|
||||||
echo "NOTE: this key does not match .sops.yaml's current &admin entry (${current_admin_pub})."
|
echo "NOTE: this key does not match .sops.yaml's current &admin entry (${current_admin_pub})."
|
||||||
echo " Backing it up anyway -- this script doesn't require it to be the admin key."
|
echo " Backing it up anyway -- this script doesn't require it to be the admin key."
|
||||||
@@ -131,7 +133,7 @@ fi
|
|||||||
mkdir -p "$(dirname "$dest")"
|
mkdir -p "$(dirname "$dest")"
|
||||||
install -m 600 "$scratch" "$dest"
|
install -m 600 "$scratch" "$dest"
|
||||||
|
|
||||||
dest_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$dest'")"
|
dest_pub="$(age_pubkey_from_identity_file "$dest")"
|
||||||
if [[ "$dest_pub" != "$src_pub" ]]; then
|
if [[ "$dest_pub" != "$src_pub" ]]; then
|
||||||
echo "ERROR: ${dest} was written but its public key doesn't match the source -- investigate before relying on this backup." >&2
|
echo "ERROR: ${dest} was written but its public key doesn't match the source -- investigate before relying on this backup." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
|||||||
source "${repo_root}/scripts/env.sh"
|
source "${repo_root}/scripts/env.sh"
|
||||||
# shellcheck source=lib/nix-eval.sh
|
# shellcheck source=lib/nix-eval.sh
|
||||||
source "${repo_root}/scripts/lib/nix-eval.sh"
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
|
# shellcheck source=lib/confirm.sh
|
||||||
|
source "${repo_root}/scripts/lib/confirm.sh"
|
||||||
|
|
||||||
sync_keys="${repo_root}/scripts/sync-host-keys.sh"
|
sync_keys="${repo_root}/scripts/sync-host-keys.sh"
|
||||||
|
|
||||||
@@ -230,8 +232,7 @@ cmd_modify() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
read -rp "Type the VMID (${vmid}) to confirm these changes: " confirm
|
if ! confirm_typed "$vmid" "Type the VMID (${vmid}) to confirm these changes: "; then
|
||||||
if [[ "$confirm" != "$vmid" ]]; then
|
|
||||||
echo "Cancelled -- input didn't match ${vmid}."
|
echo "Cancelled -- input didn't match ${vmid}."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -429,8 +430,7 @@ REMOTE_SCRIPT
|
|||||||
echo " - ${kind} VMID ${id} (${n})"
|
echo " - ${kind} VMID ${id} (${n})"
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
read -rp "Type the hostname (${host}) to confirm destroying the above and replacing it: " confirm
|
if ! confirm_typed "$host" "Type the hostname (${host}) to confirm destroying the above and replacing it: "; then
|
||||||
if [[ "$confirm" != "$host" ]]; then
|
|
||||||
echo "Cancelled -- input didn't match ${host}." >&2
|
echo "Cancelled -- input didn't match ${host}." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared "type X to confirm" prompt for scripts/create-proxmox-resource.sh
|
||||||
|
# (--modify, and replacing an existing --allow-duplicate-host resource) and
|
||||||
|
# scripts/sync-host-keys.sh (--regenerate-all-keys) -- three destructive
|
||||||
|
# confirmations that all work the same way (echo the expected value back
|
||||||
|
# exactly), kept in one place so the prompt/comparison logic can't drift.
|
||||||
|
# Source alongside env.sh:
|
||||||
|
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/confirm.sh"
|
||||||
|
#
|
||||||
|
# Deliberately does NOT print anything on mismatch or decide exit-vs-return
|
||||||
|
# -- callers vary on both (a top-level script exits, a subcommand function
|
||||||
|
# returns; wording differs too), so that stays at the call site.
|
||||||
|
|
||||||
|
# confirm_typed <expected> <prompt>
|
||||||
|
# Prints <prompt> via `read -rp`, then reports (via exit status) whether the
|
||||||
|
# typed input matched <expected> exactly.
|
||||||
|
confirm_typed() {
|
||||||
|
local expected="$1" prompt="$2" input
|
||||||
|
read -rp "$prompt" input
|
||||||
|
[[ "$input" == "$expected" ]]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared sops/age helpers for scripts/backup-admin-key.sh,
|
||||||
|
# scripts/rotate-admin-key.sh, and scripts/sync-host-keys.sh -- all three
|
||||||
|
# derive an age public key from a private identity file the same way, two
|
||||||
|
# of them resolve the same sops/age default key-file path, and two of them
|
||||||
|
# run `sops updatekeys` the same way. Kept in one place so they can't drift
|
||||||
|
# apart. Source alongside env.sh:
|
||||||
|
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/sops-age.sh"
|
||||||
|
#
|
||||||
|
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
|
||||||
|
# nix_extra_opts) if the caller has already set it, same convention as
|
||||||
|
# lib/ssh-host-keys.sh. Falls back to no extra options if the caller never
|
||||||
|
# sourced env.sh.
|
||||||
|
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
|
||||||
|
declare -a NIX_OPTS=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
# sops/age's own default identity-file resolution order, minus $SOPS_AGE_KEY
|
||||||
|
# itself (an inline identity, not a path -- callers that accept it check it
|
||||||
|
# separately, before falling back to this).
|
||||||
|
: "${DEFAULT_SOPS_AGE_KEY_FILE:=${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}}"
|
||||||
|
|
||||||
|
# age_pubkey_from_identity_file <identity-file>
|
||||||
|
# Prints the age public key for a private identity file (age-keygen -y).
|
||||||
|
age_pubkey_from_identity_file() {
|
||||||
|
local identity_file="$1"
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '${identity_file}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# sops_yaml_admin_pubkey <sops-yaml-path>
|
||||||
|
# Prints .sops.yaml's current &admin age public key, or empty (not an error
|
||||||
|
# under set -e) if no such anchor line exists -- callers that need to treat
|
||||||
|
# "missing" as fatal check for an empty result themselves.
|
||||||
|
sops_yaml_admin_pubkey() {
|
||||||
|
local sops_yaml="$1"
|
||||||
|
grep -E '^ - &admin age1' "$sops_yaml" 2>/dev/null | awk '{print $NF}' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# sops_updatekeys <secrets-file> [key-file]
|
||||||
|
# Re-encrypts <secrets-file> for .sops.yaml's current recipient set. If
|
||||||
|
# <key-file> is given, decrypts with that identity (SOPS_AGE_KEY_FILE)
|
||||||
|
# instead of whatever's ambient -- needed when the ambient default key
|
||||||
|
# doesn't match yet (e.g. mid-rotation, decrypting with the outgoing key).
|
||||||
|
sops_updatekeys() {
|
||||||
|
local secrets_file="$1" key_file="${2:-}"
|
||||||
|
if [[ -n "$key_file" ]]; then
|
||||||
|
SOPS_AGE_KEY_FILE="$key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||||
|
"sops updatekeys --yes '${secrets_file}'"
|
||||||
|
else
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p sops --run "sops updatekeys --yes '${secrets_file}'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
+19
-16
@@ -24,6 +24,8 @@ sops_yaml="${repo_root}/.sops.yaml"
|
|||||||
|
|
||||||
# shellcheck source=env.sh
|
# shellcheck source=env.sh
|
||||||
source "${repo_root}/scripts/env.sh"
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=lib/sops-age.sh
|
||||||
|
source "${repo_root}/scripts/lib/sops-age.sh"
|
||||||
|
|
||||||
# sops resolves .sops.yaml by walking up from the process's cwd, not from
|
# sops resolves .sops.yaml by walking up from the process's cwd, not from
|
||||||
# the target file's own path -- if this script were invoked from somewhere
|
# the target file's own path -- if this script were invoked from somewhere
|
||||||
@@ -53,7 +55,7 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
dry_run=0
|
dry_run=0
|
||||||
new_key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
new_key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
|
||||||
args=()
|
args=()
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -93,13 +95,9 @@ backup_key="${args[0]}"
|
|||||||
|
|
||||||
nix_extra_opts
|
nix_extra_opts
|
||||||
|
|
||||||
age_pub() {
|
|
||||||
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$1'"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "==> Deriving public keys..."
|
echo "==> Deriving public keys..."
|
||||||
old_pub="$(age_pub "$backup_key")"
|
old_pub="$(age_pubkey_from_identity_file "$backup_key")"
|
||||||
new_pub="$(age_pub "$new_key_file")"
|
new_pub="$(age_pubkey_from_identity_file "$new_key_file")"
|
||||||
echo " backup (old admin) key: ${old_pub}"
|
echo " backup (old admin) key: ${old_pub}"
|
||||||
echo " new admin key: ${new_pub}"
|
echo " new admin key: ${new_pub}"
|
||||||
|
|
||||||
@@ -108,12 +106,11 @@ if [[ "$old_pub" == "$new_pub" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
current_admin_line="$(grep -E '^ - &admin age1' "$sops_yaml" || true)"
|
current_admin_pub="$(sops_yaml_admin_pubkey "$sops_yaml")"
|
||||||
if [[ -z "$current_admin_line" ]]; then
|
if [[ -z "$current_admin_pub" ]]; then
|
||||||
echo "ERROR: couldn't find a '&admin age1...' line in ${sops_yaml}." >&2
|
echo "ERROR: couldn't find a '&admin age1...' line in ${sops_yaml}." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
current_admin_pub="$(awk '{print $NF}' <<<"$current_admin_line")"
|
|
||||||
|
|
||||||
if [[ "$current_admin_pub" != "$old_pub" ]]; then
|
if [[ "$current_admin_pub" != "$old_pub" ]]; then
|
||||||
echo "ERROR: ${backup_key} doesn't match the current &admin key in .sops.yaml." >&2
|
echo "ERROR: ${backup_key} doesn't match the current &admin key in .sops.yaml." >&2
|
||||||
@@ -129,9 +126,17 @@ if [[ "${#secrets_files[@]}" -eq 0 ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# sops_can_decrypt <key-file> <secrets-file>: used both to confirm the
|
||||||
|
# backup key still works before touching anything, and again after
|
||||||
|
# rotation to confirm the new key does too.
|
||||||
|
sops_can_decrypt() {
|
||||||
|
local key_file="$1" secrets_file="$2"
|
||||||
|
SOPS_AGE_KEY_FILE="$key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||||
|
"sops -d '${secrets_file}'" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
echo "==> Confirming the backup key can actually decrypt..."
|
echo "==> Confirming the backup key can actually decrypt..."
|
||||||
if ! SOPS_AGE_KEY_FILE="$backup_key" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
if ! sops_can_decrypt "$backup_key" "${secrets_files[0]}"; then
|
||||||
"sops -d '${secrets_files[0]}'" >/dev/null; then
|
|
||||||
echo "ERROR: backup key failed to decrypt $(basename "${secrets_files[0]}") -- aborting." >&2
|
echo "ERROR: backup key failed to decrypt $(basename "${secrets_files[0]}") -- aborting." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -162,14 +167,12 @@ echo " Updated."
|
|||||||
echo "==> Re-encrypting secrets/*.yaml for the new recipient set..."
|
echo "==> Re-encrypting secrets/*.yaml for the new recipient set..."
|
||||||
for f in "${secrets_files[@]}"; do
|
for f in "${secrets_files[@]}"; do
|
||||||
echo "==> $(basename "$f")"
|
echo "==> $(basename "$f")"
|
||||||
SOPS_AGE_KEY_FILE="$backup_key" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
sops_updatekeys "$f" "$backup_key"
|
||||||
"sops updatekeys --yes '${f}'"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "==> Verifying the new key can decrypt everything..."
|
echo "==> Verifying the new key can decrypt everything..."
|
||||||
for f in "${secrets_files[@]}"; do
|
for f in "${secrets_files[@]}"; do
|
||||||
if ! SOPS_AGE_KEY_FILE="$new_key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
if ! sops_can_decrypt "$new_key_file" "$f"; then
|
||||||
"sops -d '${f}'" >/dev/null; then
|
|
||||||
echo "ERROR: new key failed to decrypt $(basename "$f") after rotation -- investigate before committing." >&2
|
echo "ERROR: new key failed to decrypt $(basename "$f") after rotation -- investigate before committing." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ source "${repo_root}/scripts/env.sh"
|
|||||||
source "${repo_root}/scripts/lib/nix-eval.sh"
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
# shellcheck source=lib/ssh-host-keys.sh
|
# shellcheck source=lib/ssh-host-keys.sh
|
||||||
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
||||||
|
# shellcheck source=lib/sops-age.sh
|
||||||
|
source "${repo_root}/scripts/lib/sops-age.sh"
|
||||||
|
# shellcheck source=lib/confirm.sh
|
||||||
|
source "${repo_root}/scripts/lib/confirm.sh"
|
||||||
|
|
||||||
mkdir -p "$keydir"
|
mkdir -p "$keydir"
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@ ensure_admin_decrypt_key() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
local key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
|
||||||
|
|
||||||
if [[ -s "$key_file" ]]; then
|
if [[ -s "$key_file" ]]; then
|
||||||
echo "Found existing sops age key at ${key_file}."
|
echo "Found existing sops age key at ${key_file}."
|
||||||
@@ -97,7 +101,7 @@ ensure_admin_decrypt_key() {
|
|||||||
mkdir -p "$(dirname "$key_file")"
|
mkdir -p "$(dirname "$key_file")"
|
||||||
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -o '${key_file}'" 2>&1 | grep -v "^Public key:" || true
|
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -o '${key_file}'" 2>&1 | grep -v "^Public key:" || true
|
||||||
local new_pub
|
local new_pub
|
||||||
new_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '${key_file}'")"
|
new_pub="$(age_pubkey_from_identity_file "$key_file")"
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
@@ -239,7 +243,7 @@ apply_edit_plan() {
|
|||||||
while IFS= read -r basename; do
|
while IFS= read -r basename; do
|
||||||
[[ -z "$basename" ]] && continue
|
[[ -z "$basename" ]] && continue
|
||||||
echo "==> secrets/${basename}"
|
echo "==> secrets/${basename}"
|
||||||
nix-shell "${NIX_OPTS[@]}" -p sops --run "sops updatekeys --yes '${repo_root}/secrets/${basename}'"
|
sops_updatekeys "${repo_root}/secrets/${basename}"
|
||||||
done <<<"$changed"
|
done <<<"$changed"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -359,8 +363,7 @@ cmd_regenerate_all() {
|
|||||||
echo "image/tarball before it can decrypt secrets again."
|
echo "image/tarball before it can decrypt secrets again."
|
||||||
|
|
||||||
if [[ "$dry_run" -ne 1 ]]; then
|
if [[ "$dry_run" -ne 1 ]]; then
|
||||||
read -rp "Type REGENERATE to confirm: " confirm
|
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
|
||||||
if [[ "$confirm" != "REGENERATE" ]]; then
|
|
||||||
echo "Cancelled."
|
echo "Cancelled."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user