blob: 4286f4c96692879f08f3b00978ce957701eccb7d (
plain)
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
|