You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
8.8 KiB
Bash
245 lines
8.8 KiB
Bash
#!/usr/bin/env bash
|
|
# Purpose: manage .sops.yaml based on gpg keys in the same dir _and_ verify correct configuration
|
|
set -euo pipefail
|
|
|
|
function fn_gpg_extract_fpr(){
|
|
# PURPOSE: get fingerprint from gpg keyfile
|
|
gpgkeyfile=$1;shift;
|
|
# fingerprint
|
|
# caveat: restrict to netgo.de email, use-case:
|
|
# uid ... <...@mehrwerk.net>
|
|
# uid ... <...@netgo.de>
|
|
# fancy gpg src: https://unix.stackexchange.com/a/731872
|
|
fpr="$(gpg --show-keys --list-options show-only-fpr-mbox "$(readlink -f "${gpgkeyfile}")" | grep '@netgo.de' | awk "{print \$1}")"
|
|
echo "${fpr}"
|
|
}
|
|
|
|
function fn_gpg_extract_uid(){
|
|
# PURPOSE: get user-id from gpg keyfile
|
|
gpgkeyfile=$1;shift;
|
|
# user id
|
|
# caveat: restrict to netgo.de email, use-case:
|
|
# uid ... <...@mehrwerk.net>
|
|
# uid ... <...@netgo.de>
|
|
# fancy gpg src: https://unix.stackexchange.com/a/731872
|
|
uid="$(gpg --show-keys --with-colons "$(readlink -f "${gpgkeyfile}")" | awk -F':' '$1=="uid" {print $10}' | grep '@netgo.de')"
|
|
echo "${uid}"
|
|
}
|
|
|
|
function fn_sops_locate_config_in_git_repo(){
|
|
# PURPOSE: locate sops config
|
|
# Returns path sops config to be updated; defaults to returning "$(git rev-parse --show-toplevel)/.sops.yaml"
|
|
# sops locates config by recursively walking _up_ the tree from the execeution dir context,
|
|
# + _but_ does not have a mechanism to update the sops config
|
|
# This function does the same in order to locate the correct sops config to update
|
|
|
|
# starting dir, default: PWD. Note: 'realpath' to normalise the dir
|
|
start_dir="$(realpath "${1:-"${PWD}"}")";
|
|
stop_dir="$(git rev-parse --show-toplevel)"
|
|
>&2 echo "# ---"
|
|
>&2 echo "# start_dir: "${start_dir}""
|
|
>&2 echo "# stop_dir: "${stop_dir}""
|
|
|
|
# BEGIN
|
|
search_dir="${start_dir}"
|
|
contender="${search_dir}/.sops.yaml"
|
|
# base case - located the file OR stopping condition - at top of repo
|
|
if [[ -e "${contender}" ]]; then
|
|
>&2 echo "# BASE CASE: found ${contender}"
|
|
echo "${contender}"
|
|
elif [[ "${search_dir}" == "${stop_dir}" ]]; then
|
|
>&2 echo "# STOPPING CONDITION: no sops config found, suggesting: ${contender}"
|
|
echo "${contender}"
|
|
else
|
|
>&2 echo "# walk up one dir..."
|
|
fn_sops_locate_config_in_git_repo "$(dirname "${search_dir}")"
|
|
fi
|
|
}
|
|
|
|
function fn_sops_generate_config(){
|
|
# PURPOSE: generate sops config based on keyfiles
|
|
# sops.yaml doc: https://github.com/getsops/sops?tab=readme-ov-file#using-sops-yaml-conf-to-select-kms-pgp-and-age-for-new-files
|
|
# CAVEAT: dirty hacks, as DRY as feasible within bash
|
|
|
|
>&2 echo "# RUN: generate SOPS config"
|
|
# hack: 2D list workaround, i.e. difficult to have list-of-lists
|
|
fpr_list=()
|
|
uid_list=()
|
|
type_list=()
|
|
for gpgkeyfile in *automation*gpg.pub; do
|
|
type_list+=( "autom" )
|
|
fpr_list+=( "$(fn_gpg_extract_fpr "${gpgkeyfile}")" )
|
|
uid_list+=( "$(fn_gpg_extract_uid "${gpgkeyfile}")" )
|
|
done
|
|
for gpgkeyfile in $(ls *gpg.pub | grep -v automation); do
|
|
type_list+=( "human" )
|
|
fpr_list+=( "$(fn_gpg_extract_fpr "${gpgkeyfile}")" )
|
|
uid_list+=( "$(fn_gpg_extract_uid "${gpgkeyfile}")" )
|
|
done
|
|
|
|
|
|
# header
|
|
echo "# Fingerprint | User Type | User ID"
|
|
# entries/rows
|
|
for ind in "${!fpr_list[@]}"; do
|
|
printf "# %s | %s | %s\n" \
|
|
"${fpr_list[${ind}]}" \
|
|
"${type_list[${ind}]}" \
|
|
"${uid_list[${ind}]}"
|
|
done
|
|
echo "# keys in https://git.dev-at.de/smardigo-hetzner/communication-keys"
|
|
|
|
cat <<EOM
|
|
creation_rules:
|
|
# list of keys for encryption in stage
|
|
- pgp: >-
|
|
EOM
|
|
|
|
# all but last line get comma
|
|
ind_2nd_last=$((${#fpr_list[@]} - 1))
|
|
for fpr in ${fpr_list[@]:0:${ind_2nd_last}}; do
|
|
echo " ${fpr},"
|
|
done
|
|
# last line no comma
|
|
# echo " ${fpr_list[-1]}," # requires bash v4.1
|
|
echo " ${fpr_list[${ind_2nd_last}]}"
|
|
}
|
|
|
|
fn_sops_updatekeys_and_verify(){
|
|
# PURPOSE: call 'sops updatekeys' and dump contents of file so end user can visually verify functionality
|
|
sops_enc_file="${1}";shift;
|
|
# update keys in secrets file
|
|
test -e "${sops_enc_file}" || exit 1
|
|
|
|
# "update the keys of SOPS files using the config file"
|
|
>&2 echo "# RUN: sops updatekeys ${sops_enc_file}"
|
|
# HAAAACK: loop through all passed-in files, ignore any errors, always say "yes" -> rely on git diff to verify!
|
|
sops updatekeys -y "${sops_enc_file}" || echo "SKIPPING"
|
|
}
|
|
|
|
function main(){
|
|
if [[ ! -n "${@}" ]]; then
|
|
# if empty args, remove
|
|
shift
|
|
fi
|
|
# "anchor" for actions relevant to this script
|
|
repo_root="$(realpath $(dirname "${BASH_SOURCE[0]}")/..)"
|
|
|
|
# OPTIONS: ARGPARSING and VALIDATION
|
|
# assume location of script as running directly from repo with keys (instead of as a standalone packaged tool)
|
|
keyfiles_dir="${repo_root}"
|
|
# dir containing .sops.yaml
|
|
sops_config_dir=""
|
|
# path to group definitions
|
|
groups_def_dir="${repo_root}/groups"
|
|
opt_list_groups=0
|
|
groups_list=()
|
|
opt_find_secrets=0
|
|
secrets_file_list=()
|
|
|
|
while (( $# >= 1 ));do
|
|
cur="${1}";
|
|
case $cur in
|
|
# ARGS: print this help
|
|
-h|--help) echo "# ARGUMENTS:"; grep -A 1 '# ARGS:' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
# ARGS: [optional] dir containing gpg keyfiles. defaults to git repo root, var: ${repo_root}
|
|
-k|--key|--keyfiles) keyfiles_dir="${2}"; shift ;;
|
|
# ARGS: [optional] defines dir for sops config file (.sops.yaml), create if needed. defaults to git repo root, var: ${repo_root}
|
|
-c|--config_dir) sops_config_dir="${2}"; shift ;;
|
|
# ARGS: [optional] show list of groups and exit
|
|
-lg|--list_groups) opt_list_groups=1 ;;
|
|
# ARGS: [optional] [list] specify "groups" which correspond to e.g. job groups, projects, etc
|
|
-g|--group) groups_list+=( "${2}" ); shift ;;
|
|
# ARGS: [optional] update all "secrets.yaml" files found below .sops.yaml location
|
|
-f|--find_secrets) opt_find_secrets=1;;
|
|
# ARGS: [optional] [list] specify files containing sops-encrypted secrets
|
|
-s|--secrets_file|-f|--file) secrets_file_list+=( "${2}" ); shift ;;
|
|
# ARGS: [optional] [list] specify files containing sops-encrypted secrets
|
|
*) secrets_file_list+=( "${cur}" )
|
|
esac
|
|
shift;
|
|
done
|
|
|
|
# Resolve Parameters
|
|
# ... i.e. combine,override,etc options which interact
|
|
if [[ "${#groups_list[@]}" -eq 1 ]]; then
|
|
# simply change keyfiles_dir to the "groups" dir
|
|
keyfiles_dir="${groups_def_dir}/${groups_list[0]}"
|
|
elif [[ "${#groups_list[@]}" -gt 1 ]]; then
|
|
>&2 echo "# ERROR: only specify one group"
|
|
exit 1
|
|
fi
|
|
|
|
# VALIDATE INPUTS
|
|
keyfiles_dir="$(realpath "${keyfiles_dir}")"
|
|
test -d "${keyfiles_dir}" || (echo "E: specify dir containing keyfiles; invalid dir: '${keyfiles_dir}'" && exit 1)
|
|
# define sops config location
|
|
sops_config=""
|
|
if [[ -n "${sops_config_dir:-}" ]]; then
|
|
# user-specified
|
|
sops_config_dir="$(realpath "${sops_config_dir}")"
|
|
# vvv possibly redundant, since the 'realpath' will fail if dir not valid
|
|
test -d "${sops_config_dir}" || (echo "E: specify dir containing .sops.yaml, invalid dir: '${sops_config_dir}'" && exit 1)
|
|
sops_config="${sops_config_dir}/.sops.yaml"
|
|
else
|
|
# locate appropriate sops config if default assumption not found
|
|
# dev note: '2> /dev/null' to disable debug output
|
|
sops_config="$(fn_sops_locate_config_in_git_repo 2> /dev/null)"
|
|
sops_config_dir="$(dirname "${sops_config}")"
|
|
fi
|
|
|
|
# Paths to Secrets Files
|
|
if [[ "${#secrets_file_list[@]}" != "0" ]]; then
|
|
for secrets_file in "${secrets_file_list[@]}"; do
|
|
test -e "${secrets_file}" || (echo "E: could not locate file with secrets, tried: ${secrets_file}" && exit 1)
|
|
done
|
|
fi
|
|
if [[ "${opt_find_secrets}" -eq 1 ]]; then
|
|
# DEV NOTE: this is far too complicated
|
|
# loop through find, src: https://stackoverflow.com/questions/9612090/how-to-loop-through-file-names-returned-by-find
|
|
while IFS= read -r -d $'\0'; do
|
|
secrets_file_list+=("${REPLY}")
|
|
done < <( find "${sops_config_dir}" -name secrets.yaml -print0 )
|
|
fi
|
|
# /VALIDATE INPUTS
|
|
# /OPTIONS: ARGPARSING and VALIDATION
|
|
|
|
# BEGIN
|
|
if [[ "${opt_list_groups}" -eq 1 ]]; then
|
|
# list available groups and exit
|
|
pushd "${groups_def_dir}" > /dev/null 2>&1
|
|
>&2 echo "# INFO: listing groups"
|
|
ls -1d *
|
|
exit 0
|
|
popd > /dev/null 2>&1
|
|
fi
|
|
|
|
# UPDATE SOPS CONFIG
|
|
|
|
# update sops config
|
|
# TODO: remove the 'pushd;popd' workaround and make the functions aware of the dir being read
|
|
pushd "${keyfiles_dir}" > /dev/null 2>&1
|
|
(fn_sops_generate_config) > "${sops_config}"
|
|
popd > /dev/null 2>&1
|
|
|
|
# VERIFY
|
|
if [[ "${#secrets_file_list[@]}" != "0" ]]; then
|
|
# import keys
|
|
pushd "${keyfiles_dir}" > /dev/null 2>&1
|
|
>&2 echo "# RUN: gpg --import *.gpg.pub"
|
|
gpg_out="$(gpg --import *.gpg.pub 2>&1)"
|
|
popd > /dev/null 2>&1
|
|
# update
|
|
for secrets_file in "${secrets_file_list[@]}"; do
|
|
fn_sops_updatekeys_and_verify "${secrets_file}"
|
|
done
|
|
echo "# SUCCESS: all users with keys in this dir should have functional keys"
|
|
else
|
|
echo "# WARN: no secrets file passed in, make sure to call 'sops updatekeys' on secrets files"
|
|
fi
|
|
}
|
|
|
|
# pass-through, set default value
|
|
main "${@-}"
|
|
exit
|