Files
net-kingdom/sso-mfa/bootstrap/emergency-bundle.sh
Bernd Worsch 95656f2324 feat(creds): NK-WP-0005 — agent-driven credential bootstrap
Implements all 7 tasks from NK-WP-0005:

T01: creds-state.yaml → schema_version: 2, agent_mode: true
     Replaces keepass_confirmed with emergency_bundle_delivered,
     adds phase tracking fields for fully automated flow.

T02: creds-bootstrap-agent.sh — single entrypoint for autonomous
     bootstrap. 10 phases, idempotent re-runs via state file.
     Only human touchpoint: emergency bundle confirmation gate.

T03: emergency-bundle.sh — assembles and displays emergency bundle
     (age key + break-glass passwords + ops bundle location).
     Writes temp file, shreds on confirmation, clears screen.
     Supports --reprint for re-delivery.

T04: ~/.claude/commands/creds-init.md — /creds-init skill replaces
     /creds-bootstrap. Fully autonomous execution via the agent.

T05: Makefile — creds-agent-init, creds-agent-status,
     creds-emergency-reprint targets.

T06: creds-rotate.sh — --non-interactive flag for agent-driven
     rotation. Auto-confirms all gates; tracks last_rotated_<key>
     in creds-state.yaml. LLDAP web UI step prints warning in
     non-interactive mode.

T07: canon/standards/credential-management_v0.2.md — updated
     standard: KeePassXC removed from operational path, agent
     bootstrap as Phase 0, emergency bundle section, prohibited
     patterns updated.

Also: creds-status.sh handles both schema v1 (legacy) and v2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 08:38:52 +00:00

148 lines
7.8 KiB
Bash
Executable File

#!/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 ""