diff --git a/lab/README.md b/lab/README.md index 1d1278e..8088b89 100644 --- a/lab/README.md +++ b/lab/README.md @@ -61,6 +61,14 @@ multipass exec incredigo-sbx -- bash -lc ' ## Custody / smoke +- `lab-rung1-appsecret-host.sh` — **safe-candidate ladder rung 1**, the host dress + rehearsal: the first end-to-end run of the real `rotate --execute` spine on the host + toolchain (real gopass, backup gate, audit, seal flow) against a **throwaway scratch + app** (dummy `.env` + `config.yml`, fake secret). Walks dry-run → blast → execute → + asserts the in-place cutover (old gone, one new value in both files, stored blob + updated) → shows the old value is recoverable from the sealed backup. Isolated store, + zero real-cred risk. `appsecret` stays LIVE-VM — a real-cred rotation is what earns + LIVE-REAL. (ROADMAP M2 rung 1.) - `lab-restore-drill.sh` — proves the sealed `.age` backup is a **real recovery path** on the host against real gopass (isolated throwaway store, fake creds): seed → seal → destroy every entry → `import` → assert byte-equal restoration + no plaintext in the diff --git a/lab/lab-rung1-appsecret-host.sh b/lab/lab-rung1-appsecret-host.sh new file mode 100755 index 0000000..ca791a8 --- /dev/null +++ b/lab/lab-rung1-appsecret-host.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# lab-rung1-appsecret-host.sh — RUNG 1 of the safe-candidate ladder, on the HOST. +# +# The first end-to-end run of the REAL rotate --execute spine on the host toolchain +# (real gopass, real backup gate, real audit log, real INCREDIGO_PASSPHRASE seal flow) +# — but against a THROWAWAY "scratch app" (dummy .env + config.yml) so no credential +# that matters is at risk. This is the dress rehearsal before a real dev DB / Gitea PAT. +# +# What it walks (docs/ROTATION-REAL-CRED-RUNBOOK.md Part B): +# 1. stage a self-contained appsecret credential in an ISOLATED gopass store +# 2. --dry-run (noop spine, touches nothing) +# 3. --blast (map which files consume the secret, read-only) +# 4. --execute (backup gate → rotate files in place → verify → store → revoke-noop) +# 5. assert: old value gone from both files, one identical NEW value in both, +# the stored blob carries the new value, a sealed backup exists +# 6. recovery: import the sealed backup and show the OLD signing value is recoverable +# +# Safety: isolated GOPASS_HOMEDIR + GNUPGHOME (no-protection throwaway key), a scratch +# app dir under $HOME, FAKE secret. It CANNOT touch the real store or a real GPG key. +set -euo pipefail +export PATH=/usr/local/bin:$PATH +export GOPASS_HOMEDIR="${GOPASS_HOMEDIR:-$HOME/.rung1-gopass}" +export GNUPGHOME="${GNUPGHOME:-$HOME/.rung1-gnupg}" +export GOPASS_NO_NOTIFY=true +export INCREDIGO_PASSPHRASE="${INCREDIGO_PASSPHRASE:-rung1-seal-pass}" + +SCRATCH="${SCRATCH:-$HOME/incredigo-scratch-app}" +PREFIX="rotation-test/" +ENTRY="rotation-test/appsecret/scratchapp" + +# --- locate incredigo (prefer installed/staged, else build from the repo) --- +if [ -n "${INCREDIGO_BIN:-}" ] && [ -x "${INCREDIGO_BIN}" ]; then + INC="$INCREDIGO_BIN" +elif command -v incredigo >/dev/null 2>&1; then + INC="$(command -v incredigo)" +else + ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "$(dirname "$0")/..")" + INC="$(mktemp -d)/incredigo" + echo "== building incredigo from $ROOT ==" + ( cd "$ROOT" && CGO_ENABLED=0 go build -o "$INC" ./cmd/incredigo ) +fi +echo "== incredigo: $INC" +command -v gopass >/dev/null 2>&1 || { echo "FAIL: gopass not on PATH"; exit 1; } + +# --- clean isolated store + throwaway key --- +rm -rf "$GOPASS_HOMEDIR" "$GNUPGHOME" "$SCRATCH" +mkdir -p "$GNUPGHOME"; chmod 700 "$GNUPGHOME" +mkdir -p "$GOPASS_HOMEDIR" "$SCRATCH" + +echo "== throwaway GPG key ==" +cat > "$GNUPGHOME/key.batch" <<'EOF' +%no-protection +Key-Type: eddsa +Key-Curve: ed25519 +Subkey-Type: ecdh +Subkey-Curve: cv25519 +Name-Real: Incredigo Rung1 +Name-Email: rung1@incredigo.local +Expire-Date: 0 +%commit +EOF +gpg --batch --generate-key "$GNUPGHOME/key.batch" 2>/dev/null +FPR=$(gpg --list-keys --with-colons rung1@incredigo.local | awk -F: '/^fpr:/{print $10; exit}') +gopass init --crypto gpg --storage fs "$FPR" &1 | tail -1 || gopass init "$FPR" &1 | tail -1 + +# --- scratch app: two files sharing one signing secret (FAKE) --- +OLD="$(openssl rand -hex 32)" # 64 hex chars — stands in for a Django SECRET_KEY etc. +cat > "$SCRATCH/.env" < "$SCRATCH/config.yml" </dev/null +RECOVERED_BLOB="$(gopass show -o "$ENTRY")" +case "$RECOVERED_BLOB" in + *"val=$OLD"*) echo " ✓ restored blob carries the OLD value — file rewrite is reversible from backup" ;; + *) echo " ✗ backup did not carry the old value"; fail=1 ;; +esac + +echo +[ "$fail" -eq 0 ] && echo "RUNG1_APPSECRET_OK — real host rotate --execute spine walked on a scratch app" \ + || { echo "RUNG1_APPSECRET_FAIL"; exit 1; }