generated from coulomb/repo-seed
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>
This commit is contained in:
147
sso-mfa/bootstrap/emergency-bundle.sh
Executable file
147
sso-mfa/bootstrap/emergency-bundle.sh
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/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 ""
|
||||
Reference in New Issue
Block a user