diff --git a/docs/secrets.md b/docs/secrets.md index 09f09d3..194056d 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -30,3 +30,12 @@ This decrypts `creds/secrets.enc.env` to `creds/secrets.env` if needed (requires - Generate a new age key: `age-keygen -o creds/age-key.txt` (keep old backup if you need to reencrypt). - Update `.sops.yaml` recipient to the new public key. - Re-encrypt: `SOPS_AGE_KEY_FILE=creds/age-key.txt sops --encrypt --in-place creds/secrets.enc.env`. + +## Per-user age keys +- Keys live under `creds/age/.key` (git-ignored) and carry a public key in the header. +- Helper: `./scripts/manage-age-key.sh add alice` generates a key and appends the recipient to `.sops.yaml`. +- Remove: `./scripts/manage-age-key.sh remove alice` deletes the key file and strips the recipient (re-encrypt afterwards). +- List: `./scripts/manage-age-key.sh list`. +- After adding/removing recipients, re-encrypt secrets: `sops --encrypt --in-place creds/secrets.enc.env`. + +Share each user’s private key securely (password manager). Multiple recipients in `.sops.yaml` allow any listed user to decrypt. diff --git a/scripts/manage-age-key.sh b/scripts/manage-age-key.sh new file mode 100755 index 0000000..6e360e8 --- /dev/null +++ b/scripts/manage-age-key.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Manage per-user age keys and sops recipients. +# - Creates a new age keypair under creds/age/.key +# - Updates .sops.yaml recipients list for creds/secrets.enc.env +# - Can delete a user (removes key file and recipient entry) +# +# Usage: +# ./scripts/manage-age-key.sh add +# ./scripts/manage-age-key.sh remove +# ./scripts/manage-age-key.sh list +# +# Notes: +# - Keep private keys out of git; creds/age/ is git-ignored. +# - After add/remove, re-encrypt secrets: sops --encrypt --in-place creds/secrets.enc.env + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +AGE_DIR="$ROOT_DIR/creds/age" +SOPS_CONFIG="$ROOT_DIR/.sops.yaml" + +usage() { + sed -n '1,12p' "${BASH_SOURCE[0]}" + exit 1 +} + +list_keys() { + if [[ -d "$AGE_DIR" ]]; then + find "$AGE_DIR" -maxdepth 1 -type f -name '*.key' -printf '%f\n' + fi +} + +ensure_age_dir() { + mkdir -p "$AGE_DIR" +} + +add_user() { + local user="$1" + ensure_age_dir + local key_file="$AGE_DIR/${user}.key" + if [[ -f "$key_file" ]]; then + echo "Key already exists: $key_file" + exit 1 + fi + echo "Generating age key for ${user} -> ${key_file}" + age-keygen -o "$key_file" >/dev/null + local pub + pub=$(grep '^# public key:' "$key_file" | awk '{print $4}') + echo "Public key: $pub" + + if [[ ! -f "$SOPS_CONFIG" ]]; then + echo "sops config not found at $SOPS_CONFIG; skipping recipient update." + echo "Add this recipient manually: $pub" + exit 0 + fi + + if grep -q "$pub" "$SOPS_CONFIG"; then + echo "Recipient already present in $SOPS_CONFIG" + else + echo "Adding recipient to $SOPS_CONFIG" + # Append under the first age key list. + tmp=$(mktemp) + awk -v pub="$pub" ' + /^ *- age:/ { in_age=1; print; next } + in_age && /^ *- age1/ { print " - " pub; in_age=0 } + { print } + ' "$SOPS_CONFIG" >"$tmp" + mv "$tmp" "$SOPS_CONFIG" + fi + + echo "Done. Re-encrypt secrets after updating recipients:" + echo " sops --encrypt --in-place creds/secrets.enc.env" +} + +remove_user() { + local user="$1" + local key_file="$AGE_DIR/${user}.key" + if [[ -f "$key_file" ]]; then + rm -f "$key_file" + echo "Removed key $key_file" + else + echo "No key file for $user at $key_file" + fi + if [[ -f "$SOPS_CONFIG" ]]; then + local pub + pub=$(grep '^# public key:' "$key_file" 2>/dev/null | awk '{print $4}') + if [[ -n "${pub:-}" ]]; then + tmp=$(mktemp) + awk -v pub="$pub" '!($0 ~ pub)' "$SOPS_CONFIG" >"$tmp" + mv "$tmp" "$SOPS_CONFIG" + echo "Removed recipient from $SOPS_CONFIG (if present)." + fi + fi + echo "Re-encrypt secrets to drop removed recipients if needed:" + echo " sops --encrypt --in-place creds/secrets.enc.env" +} + +cmd="${1:-}" +case "$cmd" in + add) + [[ $# -eq 2 ]] || usage + add_user "$2" + ;; + remove|rm|delete) + [[ $# -eq 2 ]] || usage + remove_user "$2" + ;; + list|ls) + list_keys + ;; + *) + usage + ;; +esac