lab: host restore drill — prove sealed backup restores (M2 prereq)
The CLI test proves export→import logic against a fake gopass; this proves the real recovery path end-to-end against real gopass in an isolated throwaway store: seed → seal → destroy every entry → import → assert byte-equal restoration and no plaintext in the bundle. It is the safety net for no-op-RevokeOld drivers (e.g. appsecret), where the sealed .age backup is the only rollback — the prerequisite for the first real rotation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-2
@@ -86,9 +86,11 @@ credential, verified backup + restore-drill first, verify-new-before-revoke-old.
|
|||||||
6. **aws** — last: real IAM user with minimal policy, console as recovery path.
|
6. **aws** — last: real IAM user with minimal policy, console as recovery path.
|
||||||
- [ ] Each rung: runbook entry (local ROTATION-REAL-CRED-RUNBOOK.md), audit log
|
- [ ] Each rung: runbook entry (local ROTATION-REAL-CRED-RUNBOOK.md), audit log
|
||||||
retained, proofs.go + ROTATION-PROOFS.md updated in the same commit.
|
retained, proofs.go + ROTATION-PROOFS.md updated in the same commit.
|
||||||
- [ ] Backup restore drill: before the first real rotation, prove
|
- [x] Backup restore drill: before the first real rotation, prove
|
||||||
`import` of a sealed backup actually restores the store (not just that the
|
`import` of a sealed backup actually restores the store (not just that the
|
||||||
bundle verifies).
|
bundle verifies). Done two ways: `TestExportImportRoundTripCLI` (fake gopass,
|
||||||
|
in CI) + `lab/lab-restore-drill.sh` (real gopass on the host, isolated store —
|
||||||
|
seed → seal → destroy → restore → assert byte-equal, no plaintext leak).
|
||||||
|
|
||||||
Exit criteria: ≥5 drivers at LIVE-REAL, zero incidents, execute-gate enforces
|
Exit criteria: ≥5 drivers at LIVE-REAL, zero incidents, execute-gate enforces
|
||||||
proof levels.
|
proof levels.
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ multipass exec incredigo-sbx -- bash -lc '
|
|||||||
|
|
||||||
## Custody / smoke
|
## Custody / smoke
|
||||||
|
|
||||||
|
- `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
|
||||||
|
bundle. The safety net for no-op-`RevokeOld` drivers (e.g. `appsecret`), where the
|
||||||
|
backup is the only rollback. Prerequisite for the first real rotation (ROADMAP M2).
|
||||||
- `lab-store.sh` — throwaway key + gopass store + fake `.env`/`.aws`/consumer files.
|
- `lab-store.sh` — throwaway key + gopass store + fake `.env`/`.aws`/consumer files.
|
||||||
- `lab-run.sh` — scan→status→migrate→export→import→`rotate --dry-run --blast`→worklist.
|
- `lab-run.sh` — scan→status→migrate→export→import→`rotate --dry-run --blast`→worklist.
|
||||||
- `lab-harness.sh`, `incredigo-lab-setup.sh` — older combined harness variants.
|
- `lab-harness.sh`, `incredigo-lab-setup.sh` — older combined harness variants.
|
||||||
|
|||||||
Executable
+111
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lab-restore-drill.sh — prove the sealed backup is a REAL recovery path, on the host,
|
||||||
|
# against REAL gopass, before any real rotation trusts it as the only rollback.
|
||||||
|
#
|
||||||
|
# The CLI test (cmd/incredigo TestExportImportRoundTripCLI) already proves the export→
|
||||||
|
# import LOGIC against a FAKE gopass binary. This drill proves the same thing end-to-end
|
||||||
|
# against a genuinely installed gopasspw/gopass in an ISOLATED throwaway store: seed →
|
||||||
|
# seal → DESTROY the entries → restore from the sealed bundle → assert every value is
|
||||||
|
# back byte-for-byte. It is the safety net for drivers whose RevokeOld is a no-op (e.g.
|
||||||
|
# appsecret), where the sealed .age backup is the ONLY way back.
|
||||||
|
#
|
||||||
|
# Safety: isolated GOPASS_HOMEDIR + GNUPGHOME (no-protection throwaway key) + FAKE creds.
|
||||||
|
# It CANNOT touch the real ~/.password-store or a real GPG key. Nothing real is rotated.
|
||||||
|
set -euo pipefail
|
||||||
|
export PATH=/usr/local/bin:$PATH
|
||||||
|
export GOPASS_HOMEDIR="${GOPASS_HOMEDIR:-$HOME/.drill-gopass}"
|
||||||
|
export GNUPGHOME="${GNUPGHOME:-$HOME/.drill-gnupg}"
|
||||||
|
export GOPASS_NO_NOTIFY=true
|
||||||
|
export INCREDIGO_PASSPHRASE="${INCREDIGO_PASSPHRASE:-drill-seal-pass}"
|
||||||
|
|
||||||
|
# --- locate incredigo (prefer an installed/staged binary, 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 (install gopasspw/gopass)"; exit 1; }
|
||||||
|
|
||||||
|
# --- clean isolated store + throwaway key ---
|
||||||
|
rm -rf "$GOPASS_HOMEDIR" "$GNUPGHOME"
|
||||||
|
mkdir -p "$GNUPGHOME"; chmod 700 "$GNUPGHOME"
|
||||||
|
mkdir -p "$GOPASS_HOMEDIR"
|
||||||
|
|
||||||
|
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 Drill
|
||||||
|
Name-Email: drill@incredigo.local
|
||||||
|
Expire-Date: 0
|
||||||
|
%commit
|
||||||
|
EOF
|
||||||
|
gpg --batch --generate-key "$GNUPGHOME/key.batch" 2>/dev/null
|
||||||
|
FPR=$(gpg --list-keys --with-colons drill@incredigo.local | awk -F: '/^fpr:/{print $10; exit}')
|
||||||
|
echo "key: $FPR"
|
||||||
|
|
||||||
|
echo "== gopass init (isolated) =="
|
||||||
|
gopass init --crypto gpg --storage fs "$FPR" </dev/null 2>&1 | tail -2 || gopass init "$FPR" </dev/null 2>&1 | tail -2
|
||||||
|
|
||||||
|
# --- seed FAKE entries and remember their values ---
|
||||||
|
declare -A SEED=(
|
||||||
|
[imported/aws/default]='wJalrFAKEsecretKEYwJalrFAKEsecretKEY0001'
|
||||||
|
[imported/env/api]='tok-末-value-FAKE-0001'
|
||||||
|
[imported/db/postgres]='lab-db-PASSWORD-0001'
|
||||||
|
)
|
||||||
|
echo "== seed store =="
|
||||||
|
for p in "${!SEED[@]}"; do
|
||||||
|
printf '%s\n' "${SEED[$p]}" | gopass insert --multiline=false -f "$p"
|
||||||
|
done
|
||||||
|
gopass ls --flat
|
||||||
|
|
||||||
|
# --- seal (the same bundle the Snapshot backup gate produces) ---
|
||||||
|
BUNDLE="$(mktemp -d)/restore-drill.age"
|
||||||
|
echo "== export → sealed bundle: $BUNDLE =="
|
||||||
|
"$INC" export --out "$BUNDLE" --prefix imported/
|
||||||
|
[ -s "$BUNDLE" ] || { echo "FAIL: bundle missing/empty"; exit 1; }
|
||||||
|
|
||||||
|
# the sealed bundle must NOT contain any plaintext secret
|
||||||
|
for p in "${!SEED[@]}"; do
|
||||||
|
if grep -aqF "${SEED[$p]}" "$BUNDLE"; then
|
||||||
|
echo "FAIL: sealed bundle leaked plaintext for $p"; exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo " bundle holds no plaintext secret ✓"
|
||||||
|
|
||||||
|
# --- simulate LOSS: destroy every entry ---
|
||||||
|
echo "== DESTROY all entries (simulate loss/corruption) =="
|
||||||
|
for p in "${!SEED[@]}"; do gopass rm -f "$p" >/dev/null; done
|
||||||
|
REMAIN=$(gopass ls --flat | grep -c 'imported/' || true)
|
||||||
|
[ "$REMAIN" -eq 0 ] || { echo "FAIL: store not empty after destroy ($REMAIN left)"; exit 1; }
|
||||||
|
echo " store emptied ✓"
|
||||||
|
|
||||||
|
# --- restore from the sealed bundle ---
|
||||||
|
echo "== import → restore =="
|
||||||
|
"$INC" import --in "$BUNDLE" --force
|
||||||
|
|
||||||
|
# --- assert every value is back, byte-for-byte ---
|
||||||
|
echo "== verify restoration =="
|
||||||
|
fail=0
|
||||||
|
for p in "${!SEED[@]}"; do
|
||||||
|
got="$(gopass show -o "$p" 2>/dev/null || true)"
|
||||||
|
if [ "$got" = "${SEED[$p]}" ]; then
|
||||||
|
echo " $p restored ✓"
|
||||||
|
else
|
||||||
|
echo " $p MISMATCH: got '$got' want '${SEED[$p]}'"; fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$fail" -eq 0 ] || { echo "RESTORE_DRILL_FAIL"; exit 1; }
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "RESTORE_DRILL_OK — sealed backup is a proven recovery path (real gopass, isolated)"
|
||||||
Reference in New Issue
Block a user