net-kingdom/sso-mfa/bootstrap/emergency-bundle.sh

148 lines
7.8 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# emergency-bundle.sh — assemble and display the net-kingdom emergency credential bundle
#
# Usage:
# bash sso-mfa/bootstrap/emergency-bundle.sh \
# --age-key <path> (default: ~/.config/sops/age/keys.txt)
# --secrets-dir <path> (default: ./secrets)
# --ops-bundle <path> (path to the .tar.age ops bundle)
# [--reprint] (reprint a previously-delivered bundle without
# requiring all secrets to be present)
#
# Displays the emergency bundle on stdout, optionally writes a temp file
# for 60 seconds, then prompts for human confirmation.
# Sets emergency_bundle_delivered: true in creds-state.yaml after confirmation.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
STATE_FILE="$SCRIPT_DIR/creds-state.yaml"
AGE_KEY="$HOME/.config/sops/age/keys.txt"
SECRETS_DIR="$SCRIPT_DIR/secrets"
OPS_BUNDLE=""
REPRINT=false
# ── Argument parsing ──────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--age-key) AGE_KEY="$2"; shift 2 ;;
--secrets-dir) SECRETS_DIR="$2"; shift 2 ;;
--ops-bundle) OPS_BUNDLE="$2"; shift 2 ;;
--reprint) REPRINT=true; shift ;;
*) echo "Unknown argument: $1" >&2; exit 1 ;;
esac
done
# ── Read helpers ──────────────────────────────────────────────────────────────
read_env_val() {
local file="$1" key="$2"
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | sed "s/^${key}=//" || echo "<not found>"
}
read_env() {
local component="$1" key="$2"
local f="$SECRETS_DIR/$component/secrets.env"
if [[ -f "$f" ]]; then
read_env_val "$f" "$key"
else
echo "<secrets not available>"
fi
}
# ── Collect values ────────────────────────────────────────────────────────────
# Age private key
if [[ -f "$AGE_KEY" ]]; then
AGE_PRIVATE_KEY=$(cat "$AGE_KEY")
AGE_PUBKEY=$(grep 'public key:' "$AGE_KEY" | awk '{print $NF}' || echo "")
else
AGE_PRIVATE_KEY="<age key not found at $AGE_KEY>"
AGE_PUBKEY=""
fi
# Break-glass passwords
PI_ADMIN_PASS="$(read_env privacyidea PI_ADMIN_PASSWORD)"
LLDAP_ADMIN_PASS="$(read_env lldap LLDAP_LDAP_USER_PASS)"
PG_ROOT_PASS="$(read_env postgres PG_ROOT_PASSWORD)"
BG_PASS="$(read_env breakglass BREAKGLASS_PASSWORD)"
# Ops bundle info
OPS_BUNDLE_LOCATION="${OPS_BUNDLE:-<not created yet>}"
if [[ -n "$AGE_PUBKEY" ]]; then
DECRYPT_CMD="age -d -i <age-key-path> ops-bundle-<date>.tar.age"
else
DECRYPT_CMD="age -d -i <age-key-path> ops-bundle-<date>.tar.age"
fi
GENERATED_DATE="$(date -Iseconds)"
# ── Temp file (shredded automatically) ───────────────────────────────────────
TMPFILE="$(mktemp --suffix=-emergency-bundle.txt)"
cleanup_tmp() { shred -u "$TMPFILE" 2>/dev/null || rm -f "$TMPFILE"; }
trap cleanup_tmp EXIT
# ── Assemble bundle text ──────────────────────────────────────────────────────
BUNDLE_TEXT="$(cat <<BUNDLE
╔══════════════════════════════════════════════════════════════════╗
║ NET-KINGDOM EMERGENCY CREDENTIAL BUNDLE ║
║ Generated: ${GENERATED_DATE}
║ Store this. Nothing else ever needs human storage. ║
╠══════════════════════════════════════════════════════════════════╣
║ AGE PRIVATE KEY (decrypt all SOPS/age secrets from git)
║ ────────────────────────────────────────────────────────────── ║
${AGE_PRIVATE_KEY}
╠══════════════════════════════════════════════════════════════════╣
║ BREAK-GLASS PASSWORDS (direct service access, cluster-bypass)
║ ────────────────────────────────────────────────────────────── ║
║ privacyIDEA admin : ${PI_ADMIN_PASS}
║ LLDAP admin : ${LLDAP_ADMIN_PASS}
║ PostgreSQL root : ${PG_ROOT_PASS}
║ break-glass user : ${BG_PASS}
╠══════════════════════════════════════════════════════════════════╣
║ OPS BUNDLE (age-encrypted point-in-time secret snapshot)
║ ────────────────────────────────────────────────────────────── ║
║ Location : ${OPS_BUNDLE_LOCATION}
║ Decrypt : ${DECRYPT_CMD}
╠══════════════════════════════════════════════════════════════════╣
║ RECOVERY INSTRUCTIONS ║
║ ────────────────────────────────────────────────────────────── ║
║ 1. Restore age key to ~/.config/sops/age/keys.txt ║
║ 2. Clone net-kingdom repo + run: make creds-apply ║
║ 3. Use break-glass passwords for direct service access if needed ║
╚══════════════════════════════════════════════════════════════════╝
BUNDLE
)"
# Write to temp file for 60s window
echo "$BUNDLE_TEXT" > "$TMPFILE"
chmod 600 "$TMPFILE"
# ── Display ───────────────────────────────────────────────────────────────────
echo ""
echo "$BUNDLE_TEXT"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " A copy has been written to: $TMPFILE"
echo " It will be shredded automatically when you confirm below."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " Store the above in your personal password manager now."
echo " (1Password, Bitwarden, KeePassXC, paper — your choice.)"
echo ""
read -rp " Press Enter when you have stored the bundle (this clears the screen and shreds the temp file): "
# Shred temp file (trap handles it but do it explicitly here first)
shred -u "$TMPFILE" 2>/dev/null || rm -f "$TMPFILE"
trap - EXIT
# Clear screen to remove secrets from terminal scrollback
clear 2>/dev/null || printf '\033c'
echo ""
echo " ✔ Emergency bundle confirmed. Temp file shredded."
echo ""