Archived
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14c0ad8125 | ||
|
|
a9f20e1229 |
@@ -117,6 +117,16 @@ Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` also has:
|
|||||||
for the manual steps `sync-host-keys.sh`/`create-proxmox-resource.sh`
|
for the manual steps `sync-host-keys.sh`/`create-proxmox-resource.sh`
|
||||||
print when they bootstrap a brand-new, not-yet-trusted key on a machine
|
print when they bootstrap a brand-new, not-yet-trusted key on a machine
|
||||||
with no prior admin access.
|
with no prior admin access.
|
||||||
|
- `scripts/backup-admin-key.sh <dest-path> [--key-file <path>] [--force]
|
||||||
|
[--dry-run]` — copies the local sops age key (source resolution matches
|
||||||
|
sops/age itself: `$SOPS_AGE_KEY` inline, then `--key-file`, then
|
||||||
|
`$SOPS_AGE_KEY_FILE`, then the XDG default) to an arbitrary destination
|
||||||
|
path with `0600` permissions, validating it's a real age identity and
|
||||||
|
round-tripping the public key before and after the write. Refuses to
|
||||||
|
overwrite an existing `<dest-path>` without `--force`. Purely a local
|
||||||
|
filesystem copy — never touches `.sops.yaml`/`secrets/*.yaml` or the
|
||||||
|
repo at all. The resulting file is exactly what `rotate-admin-key.sh`
|
||||||
|
expects as its backup-key argument.
|
||||||
|
|
||||||
`sync-host-keys.sh`, `create-proxmox-resource.sh`, and
|
`sync-host-keys.sh`, `create-proxmox-resource.sh`, and
|
||||||
`rotate-admin-key.sh` genuinely mutate real state when run for real (not
|
`rotate-admin-key.sh` genuinely mutate real state when run for real (not
|
||||||
@@ -125,6 +135,9 @@ containers, real revocation of decrypt access. They require the
|
|||||||
operator's own SSH/sops access, which an agent session doesn't have — but
|
operator's own SSH/sops access, which an agent session doesn't have — but
|
||||||
don't suggest running any of them non-dry-run without the operator's
|
don't suggest running any of them non-dry-run without the operator's
|
||||||
explicit go-ahead even if it becomes technically reachable.
|
explicit go-ahead even if it becomes technically reachable.
|
||||||
|
`backup-admin-key.sh` only writes a key copy to a path the operator gives
|
||||||
|
it — lower-stakes than the others, but it still handles a real private
|
||||||
|
key, so treat its destination path choice as the operator's call too.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
Executable
+148
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Backs up the local sops age key (the private key that decrypts
|
||||||
|
# secrets/*.yaml -- normally the one trusted as &admin) to an arbitrary
|
||||||
|
# destination path, e.g. a USB drive or other offline storage, so it can
|
||||||
|
# later be restored and handed to rotate-admin-key.sh if this machine's
|
||||||
|
# copy is ever lost, or to run either script from a different machine.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/backup-admin-key.sh <dest-path> [--key-file <path>] [--force] [--dry-run]
|
||||||
|
#
|
||||||
|
# Source key resolution matches sops/age's own default order:
|
||||||
|
# $SOPS_AGE_KEY (inline identity text) if set, else
|
||||||
|
# --key-file if given, else
|
||||||
|
# $SOPS_AGE_KEY_FILE if set, else
|
||||||
|
# ${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
sops_yaml="${repo_root}/.sops.yaml"
|
||||||
|
|
||||||
|
# shellcheck source=env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 <dest-path> [--key-file <path>] [--force] [--dry-run]
|
||||||
|
|
||||||
|
<dest-path> Where to write the backup. Parent directories are
|
||||||
|
created as needed. Written with 0600 permissions.
|
||||||
|
--key-file <path> Read the key from here instead of the default
|
||||||
|
sops/age resolution (\$SOPS_AGE_KEY_FILE, then
|
||||||
|
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt).
|
||||||
|
Ignored if \$SOPS_AGE_KEY is set (that always wins,
|
||||||
|
same precedence sops/age itself uses).
|
||||||
|
--force Overwrite <dest-path> if it already exists.
|
||||||
|
--dry-run Print what would happen; write nothing.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
force=0
|
||||||
|
key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
||||||
|
args=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
dry_run=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--force)
|
||||||
|
force=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--key-file)
|
||||||
|
key_file="${2:?--key-file requires a path}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
args+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${#args[@]}" -ne 1 ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
dest="${args[0]}"
|
||||||
|
|
||||||
|
nix_extra_opts
|
||||||
|
|
||||||
|
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
|
||||||
|
echo "==> Source: \$SOPS_AGE_KEY (inline identity from the environment)."
|
||||||
|
src_content="$SOPS_AGE_KEY"
|
||||||
|
else
|
||||||
|
[[ -s "$key_file" ]] || {
|
||||||
|
echo "ERROR: no key found. \$SOPS_AGE_KEY is unset and ${key_file} doesn't exist or is empty." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "==> Source: ${key_file}"
|
||||||
|
src_content="$(cat "$key_file")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Round-trip through a private scratch file (rather than trusting the
|
||||||
|
# source string as-is) so age-keygen -y validates it's a real identity
|
||||||
|
# before anything is written to <dest-path>.
|
||||||
|
scratch="$(mktemp)"
|
||||||
|
trap 'rm -f "$scratch"' EXIT
|
||||||
|
( umask 077; printf '%s\n' "$src_content" > "$scratch" )
|
||||||
|
|
||||||
|
src_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$scratch'")" || {
|
||||||
|
echo "ERROR: source doesn't look like a valid age identity (age-keygen -y failed)." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " public key: ${src_pub}"
|
||||||
|
|
||||||
|
current_admin_pub="$(grep -E '^ - &admin age1' "$sops_yaml" 2>/dev/null | awk '{print $NF}' || true)"
|
||||||
|
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 " Backing it up anyway -- this script doesn't require it to be the admin key."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "$dest" && "$force" -ne 1 ]]; then
|
||||||
|
echo "ERROR: ${dest} already exists. Pass --force to overwrite." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
echo "[dry-run] would write $(wc -c <"$scratch" | tr -d ' ') bytes to ${dest} (mode 0600)"
|
||||||
|
[[ -e "$dest" ]] && echo "[dry-run] would overwrite existing file (--force given)"
|
||||||
|
echo "[dry-run] Nothing was written. Re-run without --dry-run to apply this."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$dest")"
|
||||||
|
install -m 600 "$scratch" "$dest"
|
||||||
|
|
||||||
|
dest_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$dest'")"
|
||||||
|
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
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done. Backed up to: ${dest}
|
||||||
|
public key: ${dest_pub}
|
||||||
|
|
||||||
|
This is a private key -- store it somewhere offline/secure, not in this
|
||||||
|
repo or anywhere it'd get committed. Restore it with:
|
||||||
|
scripts/rotate-admin-key.sh ${dest}
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user