generated from coulomb/repo-seed
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>
148 lines
7.8 KiB
Bash
Executable File
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 ""
|