180 lines
5.6 KiB
Bash
180 lines
5.6 KiB
Bash
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
|
|
### UX helpers
|
|
|
|
function red() {
|
|
echo -e "\x1B[31m[!] $1 \x1B[0m"
|
|
if [ -n "${2-}" ]; then
|
|
echo -e "\x1B[31m[!] $($2) \x1B[0m"
|
|
fi
|
|
}
|
|
|
|
function green() {
|
|
echo -e "\x1B[32m[+] $1 \x1B[0m"
|
|
if [ -n "${2-}" ]; then
|
|
echo -e "\x1B[32m[+] $($2) \x1B[0m"
|
|
fi
|
|
}
|
|
|
|
function blue() {
|
|
echo -e "\x1B[34m[*] $1 \x1B[0m"
|
|
if [ -n "${2-}" ]; then
|
|
echo -e "\x1B[34m[*] $($2) \x1B[0m"
|
|
fi
|
|
}
|
|
|
|
function yellow() {
|
|
echo -e "\x1B[33m[*] $1 \x1B[0m"
|
|
if [ -n "${2-}" ]; then
|
|
echo -e "\x1B[33m[*] $($2) \x1B[0m"
|
|
fi
|
|
}
|
|
|
|
# Ask yes or no, with yes being the default
|
|
function yes_or_no() {
|
|
echo -en "\x1B[34m[?] $* [y/n] (default: y): \x1B[0m"
|
|
while true; do
|
|
read -rp "" yn
|
|
yn=${yn:-y}
|
|
case $yn in
|
|
[Yy]*) return 0 ;;
|
|
[Nn]*) return 1 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Ask yes or no, with no being the default
|
|
function no_or_yes() {
|
|
echo -en "\x1B[34m[?] $* [y/n] (default: n): \x1B[0m"
|
|
while true; do
|
|
read -rp "" yn
|
|
yn=${yn:-n}
|
|
case $yn in
|
|
[Yy]*) return 0 ;;
|
|
[Nn]*) return 1 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
### SOPS helpers
|
|
nix_secrets_dir=${NIX_SECRETS_DIR:-"$(dirname "${BASH_SOURCE[0]}")/../../nix-secrets"}
|
|
SOPS_FILE="${nix_secrets_dir}/.sops.yaml"
|
|
|
|
# Updates the .sops.yaml file with a new host or user age key.
|
|
function sops_update_age_key() {
|
|
field="$1"
|
|
keyname="$2"
|
|
key="$3"
|
|
|
|
if [ ! "$field" == "hosts" ] && [ ! "$field" == "users" ]; then
|
|
red "Invalid field passed to sops_update_age_key. Must be either 'hosts' or 'users'."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n $(yq ".keys.${field}[] | select(anchor == \"$keyname\")" "${SOPS_FILE}") ]]; then
|
|
green "Updating existing ${keyname} key"
|
|
yq -i "(.keys.${field}[] | select(anchor == \"$keyname\")) = \"$key\"" "$SOPS_FILE"
|
|
else
|
|
green "Adding new ${keyname} key"
|
|
yq -i ".keys.$field += [\"$key\"] | .keys.${field}[-1] anchor = \"$keyname\"" "$SOPS_FILE"
|
|
fi
|
|
}
|
|
|
|
# Adds the user and host to the shared.yaml creation rules
|
|
function sops_add_shared_creation_rules() {
|
|
u="\"$1_$2\"" # quoted user_host for yaml
|
|
h="\"$2\"" # quoted hostname for yaml
|
|
|
|
shared_selector='.creation_rules[] | select(.path_regex == "shared\.yaml$")'
|
|
if [[ -n $(yq "$shared_selector" "${SOPS_FILE}") ]]; then
|
|
echo "BEFORE"
|
|
cat "${SOPS_FILE}"
|
|
if [[ -z $(yq "$shared_selector.key_groups[].age[] | select(alias == $h)" "${SOPS_FILE}") ]]; then
|
|
green "Adding $u and $h to shared.yaml rule"
|
|
# NOTE: Split on purpose to avoid weird file corruption
|
|
yq -i "($shared_selector).key_groups[].age += [$u, $h]" "$SOPS_FILE"
|
|
yq -i "($shared_selector).key_groups[].age[-2] alias = $u" "$SOPS_FILE"
|
|
yq -i "($shared_selector).key_groups[].age[-1] alias = $h" "$SOPS_FILE"
|
|
fi
|
|
else
|
|
red "shared.yaml rule not found"
|
|
fi
|
|
}
|
|
|
|
# Adds the user and host to the host.yaml creation rules
|
|
function sops_add_host_creation_rules() {
|
|
host="$2" # hostname for selector
|
|
h="\"$2\"" # quoted hostname for yaml
|
|
u="\"$1_$2\"" # quoted user_host for yaml
|
|
w="\"$(whoami)_$(hostname)\"" # quoted whoami_hostname for yaml
|
|
n="\"$(hostname)\"" # quoted hostname for yaml
|
|
|
|
host_selector=".creation_rules[] | select(.path_regex | contains(\"${host}\.yaml\"))"
|
|
if [[ -z $(yq "$host_selector" "${SOPS_FILE}") ]]; then
|
|
green "Adding new host file creation rule"
|
|
yq -i ".creation_rules += {\"path_regex\": \"${host}\\.yaml$\", \"key_groups\": [{\"age\": [$u, $h]}]}" "$SOPS_FILE"
|
|
# Add aliases one by one
|
|
yq -i "($host_selector).key_groups[].age[0] alias = $u" "$SOPS_FILE"
|
|
yq -i "($host_selector).key_groups[].age[1] alias = $h" "$SOPS_FILE"
|
|
yq -i "($host_selector).key_groups[].age[2] alias = $w" "$SOPS_FILE"
|
|
yq -i "($host_selector).key_groups[].age[3] alias = $n" "$SOPS_FILE"
|
|
fi
|
|
}
|
|
|
|
# Adds the user and host to the shared.yaml and host.yaml creation rules
|
|
function sops_add_creation_rules() {
|
|
user="$1"
|
|
host="$2"
|
|
|
|
sops_add_shared_creation_rules "$user" "$host"
|
|
sops_add_host_creation_rules "$user" "$host"
|
|
}
|
|
|
|
age_secret_key=""
|
|
# Generate a user age key, update the .sops.yaml entries, and return the key in age_secret_key
|
|
# args: user, hostname
|
|
function sops_generate_user_age_key() {
|
|
target_user="$1"
|
|
target_hostname="$2"
|
|
key_name="${target_user}_${target_hostname}"
|
|
green "Age key does not exist. Generating."
|
|
user_age_key=$(age-keygen)
|
|
readarray -t entries <<<"$user_age_key"
|
|
age_secret_key=${entries[2]}
|
|
public_key=$(echo "${entries[1]}" | rg key: | cut -f2 -d: | xargs)
|
|
green "Generated age key for ${key_name}"
|
|
# Place the anchors into .sops.yaml so other commands can reference them
|
|
sops_update_age_key "users" "$key_name" "$public_key"
|
|
sops_add_creation_rules "${target_user}" "${target_hostname}"
|
|
|
|
# "return" key so it can be used by caller
|
|
export age_secret_key
|
|
}
|
|
|
|
function sops_setup_user_age_key() {
|
|
target_user="$1"
|
|
target_hostname="$2"
|
|
|
|
secret_file="${nix_secrets_dir}/sops/${target_hostname}.yaml"
|
|
config="${nix_secrets_dir}/.sops.yaml"
|
|
# If the secret file doesn't exist, it means we're generating a new user key as well
|
|
if [ ! -f "$secret_file" ]; then
|
|
green "Host secret file does not exist. Creating $secret_file"
|
|
sops_generate_user_age_key "${target_user}" "${target_hostname}"
|
|
mkdir -p "$(dirname "$secret_file")"
|
|
echo "{}" >"$secret_file"
|
|
sops --config "$config" -e "$secret_file" >"$secret_file.enc"
|
|
mv "$secret_file.enc" "$secret_file"
|
|
fi
|
|
if ! sops --config "$config" -d --extract '["keys]["age"]' "$secret_file" >/dev/null 2>&1; then
|
|
if [ -z "$age_secret_key" ]; then
|
|
sops_generate_user_age_key "${target_user}" "${target_hostname}"
|
|
fi
|
|
# shellcheck disable=SC2116,SC2086
|
|
sops --config "$config" --set "$(echo '["keys"]["age"] "'$age_secret_key'"')" "$secret_file"
|
|
else
|
|
green "Age key already exists for ${target_hostname}"
|
|
fi
|
|
}
|