net-kingdom/sso-mfa/bootstrap/encrypt-secrets.sh
tegwick 4cf26a9bc8
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix: make custody input explicit and remove partial decryptions
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a12d13-27f2-7163-b8c0-f682af02772c
2026-10-12 10:53:30 +02:00

100 lines
3.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# encrypt-secrets.sh — encrypt secrets/ directory to secrets.enc/ using age
#
# Usage:
# ./encrypt-secrets.sh [SECRETS_DIR] [AGE_PUBLIC_RECIPIENT_OR_FILE]
#
# SECRETS_DIR plaintext secrets directory (default: ./secrets)
# AGE_PUBLIC_RECIPIENT_OR_FILE age public recipient or public-recipient file
# (default: registered keys/age.pub)
#
# Encrypts each *.env file (and pi.enc if present) to
# secrets.enc/<component>/<filename>.age. Prefer passing a public age recipient
# for normal bootstrap; the private key is needed only for decrypt/apply.
#
# After a successful encrypt, shreds the plaintext secrets directory unless
# --no-shred is passed.
#
# Run after gen-secrets.sh to store secrets safely in the repo.
# Commit secrets.enc/ to git.
set +x
set -euo pipefail
umask 077
SECRETS_DIR="${1:-./secrets}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AGE_RECIPIENT_OR_KEY="${2:-$SCRIPT_DIR/../../keys/age.pub}"
NO_SHRED=false
for arg in "$@"; do [[ "$arg" == "--no-shred" ]] && NO_SHRED=true; done
if [[ ! -d "$SECRETS_DIR" ]]; then
echo "ERROR: source secrets directory is unavailable." >&2
echo "Run gen-secrets.sh first." >&2
exit 1
fi
resolve_recipient() {
local source="$1"
if [[ "$source" == age1* ]]; then
printf '%s\n' "$source"
return 0
fi
if [[ ! -f "$source" ]]; then
echo "ERROR: public recipient file is unavailable." >&2
echo "Pass an age public recipient such as age1... or a file containing it." >&2
return 1
fi
# Never derive public metadata by loading private custody material.
if grep -q 'AGE-SECRET-KEY-' "$source"; then
echo "ERROR: encryption requires a public recipient, never a private-key file." >&2
return 1
fi
local recipient
recipient=$(grep -m1 '^age1' "$source" || true)
if [[ -n "$recipient" ]]; then
printf '%s\n' "$recipient"
return 0
fi
echo "ERROR: public recipient is missing from the supplied file." >&2
return 1
}
PUBKEY=$(resolve_recipient "$AGE_RECIPIENT_OR_KEY")
printf '' | age -r "$PUBKEY" >/dev/null 2>&1 || { echo "ERROR: invalid public recipient." >&2; exit 1; }
ENC_DIR="$(dirname "$SECRETS_DIR")/secrets.enc"
mkdir -p "$ENC_DIR"
echo "Encrypting secrets → $ENC_DIR/"
echo "Recipient: $PUBKEY"
echo ""
count=0
for component_dir in "$SECRETS_DIR"/*/; do
component=$(basename "$component_dir")
mkdir -p "$ENC_DIR/$component"
for f in "$component_dir"*; do
[[ -f "$f" ]] || continue
fname=$(basename "$f")
out="$ENC_DIR/$component/$fname.age"
age -r "$PUBKEY" -o "$out" "$f"
echo " encrypted: $component/$fname → secrets.enc/$component/$fname.age"
count=$((count + 1))
done
done
echo ""
echo "$count file(s) encrypted to $ENC_DIR/"
echo ""
if [[ "$NO_SHRED" == false ]]; then
echo "Shredding plaintext secrets..."
find "$SECRETS_DIR" -type f -exec shred -u {} \;
rm -rf "$SECRETS_DIR"
echo "Done. Plaintext secrets shredded."
else
echo "(--no-shred: plaintext kept at $SECRETS_DIR)"
fi
echo ""
echo "Next: commit secrets.enc/ to git."