secrets/rotate.sh (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
#!/bin/bash
set -euo pipefail
if [[ "$#" -gt 0 && ("$1" == "-h" || "$1" == "--help") ]]; then
cat <<EOF
rotate: Generate a new age key and re-encrypt the global secret store
Creates a new age keypair, re-encrypts every secret in the global store with
the new public key, updates the public key file, and stores the new
private key in the key directory.
Usage:
rotate
rotate --yes
rotate -h|--help
Options:
--yes Skip interactive confirmation
The command will:
1. Confirm the operation (unless --yes)
2. Decrypt each secret in the store using the current private key
3. Generate a new age keypair
4. Re-encrypt all secrets with the new public key
5. Update the public key file and install the new private key
Note: Interactive command. Commit the updated store files and public key
wherever the store is tracked,
and update the private key backup in 1Password when complete.
EOF
exit 0
fi
this_dir="$(cd "$(dirname "$0")" && pwd)"
source "${this_dir}/common.sh"
assume_yes=false
if [[ "${1:-}" == "--yes" ]]; then
assume_yes=true
elif [[ -n "${1:-}" ]]; then
>&2 echo "ERROR: Unknown argument: $1"
exit 1
fi
ensure_age
ensure_private_key
old_private_key="${private_key}"
old_public_key="${key_id}"
shopt -s nullglob
store_files=("${store}"/*.age)
shopt -u nullglob
secret_count="${#store_files[@]}"
if [[ "${assume_yes}" != "true" ]]; then
if [[ ! -t 1 ]]; then
>&2 echo "ERROR: rotate requires interactive confirmation (use --yes to skip)"
exit 1
fi
>&2 echo "This will generate a new age key and re-encrypt ${secret_count} secret(s)."
>&2 echo "Current public key: ${old_public_key}"
>&2 printf "Continue? [y/N] "
local_confirm=""
read -r local_confirm </dev/tty
if [[ "${local_confirm}" != [yY] ]]; then
>&2 echo "Aborted."
exit 1
fi
fi
staging="$(mktemp -d "${TMPDIR:-/tmp}/szecret-rotate.XXXXXX")"
temp_private_key="$(mktemp -u "${TMPDIR:-/tmp}/szecret-rotate-key.XXXXXX")"
temp_public_key="$(mktemp "${TMPDIR:-/tmp}/szecret-rotate-pub.XXXXXX")"
cleanup() {
rm -rf "${staging}"
rm -f "${temp_private_key}" "${temp_public_key}"
}
trap cleanup EXIT
age-keygen -o "${temp_private_key}" 2>/dev/null
new_public_key="$(age-keygen -y "${temp_private_key}")"
echo "${new_public_key}" > "${temp_public_key}"
for secret_file in "${store_files[@]}"; do
secret_name="$(basename "${secret_file}" .age)"
age_decrypt "${old_private_key}" "${secret_file}" \
| age_encrypt "${temp_public_key}" "${staging}/${secret_name}.age"
done
for secret_file in "${store_files[@]}"; do
secret_name="$(basename "${secret_file}" .age)"
mv "${staging}/${secret_name}.age" "${secret_file}"
done
echo "${new_public_key}" > "${public_key}"
installed_private_key="${private_key_dir}/${new_public_key}"
mv "${temp_private_key}" "${installed_private_key}"
temp_private_key=""
chmod 600 "${installed_private_key}" 2>/dev/null || true
cat <<EOF
Rotation complete.
Old public key: ${old_public_key}
New public key: ${new_public_key}
Secrets re-encrypted: ${secret_count}
New private key: ${installed_private_key}
Next steps:
1. Commit the updated store and $(basename "${public_key}")
2. Update the szecret private key in 1Password
The old private key remains at ${old_private_key} until you remove it.
EOF
|