generated from coulomb/repo-seed
188 lines
5.7 KiB
Bash
Executable File
188 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# sops-custody-unlock.sh -- run SOPS/age operations with a temporary custody key.
|
|
#
|
|
# The private age key is supplied only for this invocation, validated against an
|
|
# expected public recipient, written to a 0600 temp file, and removed on exit.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
EXPECTED_RECIPIENT="${EXPECTED_AGE_RECIPIENT:-}"
|
|
SOURCE_FILE=""
|
|
INPUT_MODE=""
|
|
CHECK_ONLY=false
|
|
COMMAND=()
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
sops-custody-unlock.sh [options] [-- command ...]
|
|
|
|
Options:
|
|
--expected-recipient age1... Required recipient. Defaults to keys/age.pub.
|
|
--from-file PATH Read custody age key material from PATH.
|
|
--from-stdin Read custody age key material from stdin.
|
|
--prompt Prompt for one AGE-SECRET-KEY-1... line.
|
|
--check-only Validate the supplied key and exit.
|
|
-h, --help Show this help.
|
|
|
|
Examples:
|
|
# Run an inter-hub recovery drill after pasting the private key line.
|
|
sso-mfa/bootstrap/sops-custody-unlock.sh \
|
|
--expected-recipient age1aq8twfd78wvpra0had8cezcnj96tj4q0068edrz5jez8d6xwmflqdepsh4 \
|
|
-- make -C /home/worsch/inter-hub recovery-drill
|
|
|
|
# Pipe a password-manager field into the helper without printing it.
|
|
op read 'op://Platform/NetKingdom custodian age key/private-key' \
|
|
| sso-mfa/bootstrap/sops-custody-unlock.sh --from-stdin -- make recovery-drill
|
|
|
|
If no command is supplied, an interactive shell is opened with SOPS_AGE_KEY_FILE
|
|
set. Exit that shell to remove the temporary key file.
|
|
EOF
|
|
}
|
|
|
|
fail() {
|
|
printf 'ERROR: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--expected-recipient)
|
|
[[ $# -ge 2 ]] || fail "--expected-recipient requires a value"
|
|
EXPECTED_RECIPIENT="$2"
|
|
shift 2
|
|
;;
|
|
--expected-recipient=*)
|
|
EXPECTED_RECIPIENT="${1#*=}"
|
|
shift
|
|
;;
|
|
--from-file)
|
|
[[ $# -ge 2 ]] || fail "--from-file requires a path"
|
|
SOURCE_FILE="$2"
|
|
INPUT_MODE="file"
|
|
shift 2
|
|
;;
|
|
--from-file=*)
|
|
SOURCE_FILE="${1#*=}"
|
|
INPUT_MODE="file"
|
|
shift
|
|
;;
|
|
--from-stdin)
|
|
INPUT_MODE="stdin"
|
|
shift
|
|
;;
|
|
--prompt)
|
|
INPUT_MODE="prompt"
|
|
shift
|
|
;;
|
|
--check-only)
|
|
CHECK_ONLY=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
COMMAND=("$@")
|
|
break
|
|
;;
|
|
*)
|
|
fail "unknown argument: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$EXPECTED_RECIPIENT" && -f "$REPO_ROOT/keys/age.pub" ]]; then
|
|
EXPECTED_RECIPIENT="$(grep -m1 '^age1' "$REPO_ROOT/keys/age.pub" || true)"
|
|
fi
|
|
[[ "$EXPECTED_RECIPIENT" == age1* ]] || fail "expected age recipient not set; pass --expected-recipient age1..."
|
|
|
|
command -v age-keygen >/dev/null 2>&1 || fail "age-keygen not found; install age before running custody unlock"
|
|
|
|
if [[ -z "$INPUT_MODE" ]]; then
|
|
if [[ -t 0 ]]; then
|
|
INPUT_MODE="prompt"
|
|
else
|
|
INPUT_MODE="stdin"
|
|
fi
|
|
fi
|
|
|
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/netkingdom-sops-age.XXXXXX")"
|
|
KEY_FILE="$TMP_DIR/keys.txt"
|
|
cleanup() {
|
|
if [[ -f "$KEY_FILE" ]]; then
|
|
if command -v shred >/dev/null 2>&1; then
|
|
shred -u "$KEY_FILE" 2>/dev/null || rm -f "$KEY_FILE"
|
|
else
|
|
rm -f "$KEY_FILE"
|
|
fi
|
|
fi
|
|
rmdir "$TMP_DIR" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
umask 077
|
|
case "$INPUT_MODE" in
|
|
file)
|
|
[[ -f "$SOURCE_FILE" ]] || fail "custody age key file not found: $SOURCE_FILE"
|
|
cp "$SOURCE_FILE" "$KEY_FILE"
|
|
chmod 600 "$KEY_FILE"
|
|
;;
|
|
stdin)
|
|
cat > "$KEY_FILE"
|
|
chmod 600 "$KEY_FILE"
|
|
;;
|
|
prompt)
|
|
[[ -t 0 ]] || fail "cannot prompt for key because stdin is not a terminal; use --from-stdin or --from-file"
|
|
printf 'Paste custody age private key line (input hidden): ' >&2
|
|
IFS= read -r -s key_line
|
|
printf '\n' >&2
|
|
[[ -n "$key_line" ]] || fail "no key material supplied"
|
|
printf '%s\n' "$key_line" > "$KEY_FILE"
|
|
unset key_line
|
|
chmod 600 "$KEY_FILE"
|
|
;;
|
|
*)
|
|
fail "unsupported input mode: $INPUT_MODE"
|
|
;;
|
|
esac
|
|
|
|
[[ -s "$KEY_FILE" ]] || fail "no key material supplied"
|
|
if ! grep -q 'AGE-SECRET-KEY-1' "$KEY_FILE"; then
|
|
fail "supplied material does not contain an AGE-SECRET-KEY-1 private key"
|
|
fi
|
|
|
|
DERIVED_RECIPIENT="$(age-keygen -y "$KEY_FILE" 2>/dev/null || true)"
|
|
[[ "$DERIVED_RECIPIENT" == age1* ]] || fail "could not derive an age public recipient from supplied key"
|
|
|
|
if [[ "$DERIVED_RECIPIENT" != "$EXPECTED_RECIPIENT" ]]; then
|
|
printf 'ERROR: custody key does not match expected recipient\n' >&2
|
|
printf ' expected: %s\n' "$EXPECTED_RECIPIENT" >&2
|
|
printf ' derived : %s\n' "$DERIVED_RECIPIENT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'OK: custody age key matches expected recipient %s\n' "$EXPECTED_RECIPIENT" >&2
|
|
|
|
if [[ "$CHECK_ONLY" == true ]]; then
|
|
printf 'OK: check complete; temporary key file removed on exit\n' >&2
|
|
exit 0
|
|
fi
|
|
|
|
export SOPS_AGE_KEY_FILE="$KEY_FILE"
|
|
export NETKINGDOM_CUSTODY_AGE_KEY_FILE="$KEY_FILE"
|
|
|
|
if [[ ${#COMMAND[@]} -gt 0 ]]; then
|
|
printf 'Running command with temporary SOPS_AGE_KEY_FILE. Key file will be removed after command exit.\n' >&2
|
|
"${COMMAND[@]}"
|
|
else
|
|
printf 'Opening custody shell with temporary SOPS_AGE_KEY_FILE=%s\n' "$SOPS_AGE_KEY_FILE" >&2
|
|
printf 'Run recovery or lockdown commands, then exit the shell to remove the temp key.\n' >&2
|
|
bash -i
|
|
fi
|