Skip to main content
RunBook Academy

← All labs in Backup & DR

Lab · advanced · ~75 min

Prove the encryption key still opens the repository after losing production

B · Nested virtualisation

Objectives

  • Reproduce the failing case first: an encrypted repository that survives the site intact, and cannot be opened, because the only passphrase lived on the host that was lost
  • Record the exact failure signature — "Fatal: wrong password or no key found" and exit code 12 — so it is recognisable during an incident
  • Inspect the repository key directory and explain why possessing the repository is not the same as possessing the ability to open it
  • Add a second, independent unlock path with `restic key add` and confirm with `restic key list` that two keys now open one repository
  • Export borg key material as a printable block with `borg key export --paper` and place it in an escrow location off the protected host
  • Remove every production credential and configuration, then restore using escrowed material alone and prove the recovered bytes match the source checksum
  • Produce a written custody record naming who holds what, where it is held, and which unlock path it opens

Prerequisites

  • A disposable Linux host or container where roughly 100 MB of scratch space under the home directory can be destroyed
  • restic and borg installed and on PATH
  • GNU coreutils and GNU find (md5sum, stat, find -printf, base64)
  • Comfort exporting shell variables and reading a restic snapshot listing
  • Lab 11 (build a restic repository) completed, or equivalent familiarity with `restic init` and `restic backup`

Objective

Encryption moves the recovery question from “do I still have the bytes?” to “can I still read them?” Those are different questions with different failure modes, and only one of them is visible on a capacity graph.

You will build the ordinary arrangement — an encrypted repository whose passphrase file sits inside the directory being backed up — and then remove the host. The repository survives entirely. Nothing is corrupt, nothing is missing, and nothing will open it. That is the failing case, and it comes first because a drill that starts from the working configuration never tests the thing that actually goes wrong.

Then you will build the arrangement that survives: a second key on the same repository, exported key material held somewhere the estate does not host, a written custody record, and a restore performed with escrowed material alone.

Architecture

One master key encrypts the data. Two independently derived keys wrap that master key, and each wrapping is a separate door into the same room.

flowchart TD
    MK["master key\nencrypts every blob in the repository"]
    K1["key 4bc6f61a\nmaster key wrapped by\nthe production passphrase"]
    K2["key 66c34166\nmaster key wrapped by\nthe recovery passphrase"]
    MK --> K1
    MK --> K2
    P["production host\npassphrase file inside\nthe tree being backed up"] --> K1
    E["escrow custodian\nrecovery passphrase, and paper key\nheld off the protected estate"] --> K2
    P -.->|"site loss removes this path"| F["restic snapshots\nFatal: wrong password or no key found\nexit code 12"]
    K2 --> R["restic --password-file recovery restore\nSummary: Restored 3 files-dirs 38 B\nexit code 0"]

Copying the repository offsite copies both doors and neither key. That is the whole problem, and the lower path is the whole answer.

Requirements

  • Mode B-nested. A container or throwaway VM with local filesystem repositories. No object storage, no second host, no network backend.
  • restic and borg on PATH. Everything quoted below came from these builds:
Read-only / Safethe two builds every capture in this lab came from
$ restic version; borg --version
restic 0.19.1 compiled with go1.26.4 on linux/amd64
borg 1.4.0
  • Roughly 100 MB of scratch space. Every object is prefixed rbdr-, so Cleanup can be scoped and asserted.
  • The capture ran under /work as root, over a 10 MiB source tree. This lab runs under your home directory over a two-line CSV, so paths, sizes, key IDs, snapshot IDs and hostnames will all be your own. The messages, the exit codes and the md5 comparison are the point, and those reproduce.

Scenario

A small application host has been backed up nightly to a repository on separate storage for two years. The repository is replicated offsite. The passphrase is in a file on the host, readable by the backup job, and it has been there since the day the repository was created because that is what made the cron entry work without a prompt.

The building the host lives in is gone. The offsite repository is untouched. You are the person asked how long the restore will take.

Tasks

Task 1 — Record the pre-lab state

Cleanup is diffed against this file, so record it before anything exists.

LAB="$HOME/rbdr-lab-23"
ESCROW="$HOME/rbdr-escrow-23"
mkdir -p "$LAB" "$ESCROW"

{
  restic version
  borg --version
  ls -d "$LAB"/rbdr-* "$ESCROW"/rbdr-* 2>&1
  find "$HOME" -maxdepth 1 -name 'rbdr-*' -printf '%f\n' | sort
} | tee "$LAB/state.pre-lab"

The two ls arguments are expected to fail here. That failure text is part of the baseline: at the end it has to fail in exactly the same way.

Task 2 — Build the ordinary arrangement

LAB="$HOME/rbdr-lab-23"
SRC="$LAB/rbdr-prod"
mkdir -p "$SRC"
printf 'ORDER-1001,4500.00\nORDER-1002,1250.00\n' > "$SRC/orders.csv"

export RESTIC_REPOSITORY="$LAB/rbdr-repo"
export RESTIC_PASSWORD_FILE="$SRC/.restic-pass"
head -c 32 /dev/urandom | base64 > "$RESTIC_PASSWORD_FILE"
chmod 0600 "$RESTIC_PASSWORD_FILE"

restic init
restic backup "$SRC"
restic snapshots

One snapshot, one passphrase, one copy of that passphrase. Everything about this works, and everything about it is normal.

Task 3 — The failing case: lose the site

LAB="$HOME/rbdr-lab-23"
export RESTIC_REPOSITORY="$LAB/rbdr-repo"
rm -rf "$LAB/rbdr-prod"
unset RESTIC_PASSWORD_FILE RESTIC_PASSWORD
printf 'rbdr-not-the-passphrase\n' > "$LAB/rbdr-wrong.pass"

du -sh "$RESTIC_REPOSITORY"
find "$RESTIC_REPOSITORY" -type f | wc -l
restic --password-file "$LAB/rbdr-wrong.pass" snapshots
echo "lost-passphrase exit code: $?" | tee "$LAB/escrow-drill.txt"

The real passphrase went with the directory it lived in, so every candidate you can still supply is the wrong one. Supplying a wrong but non-empty passphrase from a file is how you make that deterministic and non-interactive, and it produces the signature the capture recorded.

Data-loss riskan intact repository that cannot be opened
$ restic snapshots
--- attempting a restore with no passphrase ---
$ restic snapshots
Fatal: wrong password or no key found
>>> exit code: 12

Every byte is present and readable and permanently useless. This is not corruption and not media failure: the data survived the disaster, and the ability to read it did not.

Read-only / Safethe key file is already in the repository, and that changes nothing
$ ls /work/repo/keys/
$ ls /work/repo/keys/
d5f39ef5517fab2e4d1e6dc1d9d5b4b1f1ca4eeacecde523af8b5a0c3c3d120e

That file is the master key, encrypted with a key derived from the passphrase. The lock shipped offsite along with the door.

Task 4 — Add a second, independent unlock path

Start clean, because the first repository is now unopenable by design.

LAB="$HOME/rbdr-lab-23"
SRC="$LAB/rbdr-prod2"
mkdir -p "$SRC"
printf 'ORDER-1001,4500.00\nORDER-1002,1250.00\n' > "$SRC/orders.csv"
md5sum "$SRC/orders.csv" | tee "$LAB/md5.source2"

export RESTIC_REPOSITORY="$LAB/rbdr-repo2"
export RESTIC_PASSWORD_FILE="$SRC/.restic-pass"
head -c 32 /dev/urandom | base64 > "$RESTIC_PASSWORD_FILE"
chmod 0600 "$RESTIC_PASSWORD_FILE"
restic init
restic backup "$SRC"

The checksum is written outside the tree it describes, because a hash stored beside its data proves nothing once the data is gone.

ESCROW="$HOME/rbdr-escrow-23"
head -c 32 /dev/urandom | base64 > "$ESCROW/rbdr-recovery.pass"
chmod 0600 "$ESCROW/rbdr-recovery.pass"

restic key add --new-password-file "$ESCROW/rbdr-recovery.pass"
echo "key add exit code: $?" | tee -a "$HOME/rbdr-lab-23/escrow-drill.txt"
restic key list
Configuration changea second passphrase added to an existing repository
$ restic key add --new-password-file /work/recovery-pass
$ restic key add --new-password-file /work/recovery-pass
saved new key with ID 66c34166d8443d16e8c5899fe3f792e749cb24fa3a90ac90bbe8348979b57d90
>>> exit code: 0
Read-only / Safetwo keys, one repository, one master key
$ restic key list
$ restic key list
 ID        User  Host          Created
--------------------------------------------------
 66c34166  root  17dffded9807  2026-08-28 14:04:55
*4bc6f61a  root  17dffded9807  2026-08-28 14:04:52
--------------------------------------------------

The asterisk marks the key the current passphrase opened. Neither passphrase can derive the other; both decrypt the same master key.

Task 5 — Export key material to escrow

The second mechanism escrows the key material itself rather than a passphrase.

LAB="$HOME/rbdr-lab-23"
ESCROW="$HOME/rbdr-escrow-23"
export BORG_REPO="$LAB/rbdr-brepo"
export BORG_PASSPHRASE='rbdr-lab-23-throwaway'

borg init --encryption=repokey-blake2 "$BORG_REPO"
borg key export --paper "$BORG_REPO" | tee "$ESCROW/rbdr-borg-paper-key.txt"
Read-only / Safeprintable key material that survives every system holding the repository
$ borg key export --paper /work/brepo
$ borg key export --paper /work/brepo
To restore key use borg key import --paper /path/to/repo

BORG PAPER KEY v1
id: 31 / 6ece3c b2eaf0 547454 / ab77e5 a39843 - ce
 1: 86a961 6c676f 726974 686da6 736861 323536 - 14
 2: a46461 7461da 019ee5 d18e4e 246dfa 8e1bb1 - 59
 3: 620691 f2dea2 918fcf ef0e59 4e1923 ca1ffa - 5d
 4: 9156ef de13d2 0406ff a3be7b f4edec b27562 - 67
 5: 49a3c5 f22540 1b9be8 bfa8a5 5ed425 fab040 - 3f
 6: cb83da a857c9 e5bc97 32f394 452c1a b9c48b - e3
 7: 72cc14 df7270 948df1 bed610 b7e152 81f4c4 - ed
 8: 0e1d11 f617f9 3baa03 8b1189 f111e8 a2bec3 - 06

The first line of that output names the command that reads the block back. The borg key documentation cited above describes the trailing pair on each line as a checksum, so a mistyped digit is caught on import rather than producing a key that silently fails to open anything. The block is still useless without the passphrase, which is exactly why the two go to different custodians.

Task 6 — Remove everything production held

LAB="$HOME/rbdr-lab-23"
rm -rf "$LAB/rbdr-prod2"
unset RESTIC_PASSWORD_FILE RESTIC_PASSWORD

ls -d "$LAB/rbdr-prod2" 2>&1
env | grep -c '^RESTIC_PASSWORD' || echo "0 production credentials remain in this shell"

The production passphrase file went with the directory it lived in. The repository, the escrow directory and the paper key are all that remain.

Task 7 — Open it with escrowed material alone, and time it

LAB="$HOME/rbdr-lab-23"
ESCROW="$HOME/rbdr-escrow-23"
export RESTIC_REPOSITORY="$LAB/rbdr-repo2"

restic --password-file "$ESCROW/rbdr-recovery.pass" snapshots
T0=$(date +%s)
restic --password-file "$ESCROW/rbdr-recovery.pass" restore latest --target "$LAB/rbdr-rec"
RC=$?
T1=$(date +%s)
printf 'escrow restore exit code: %s\nActual restore time (s): %s\n' "$RC" "$((T1 - T0))" \
  | tee -a "$LAB/escrow-drill.txt"
Read-only / Safethe same disaster, with escrow in place
$ restic --password-file /work/recovery-pass snapshots
$ restic --password-file /work/recovery-pass snapshots
ID        Time                 Host          Tags        Paths        Size
--------------------------------------------------------------------------
b96ba7cf  2026-08-28 14:04:52  17dffded9807              /work/prod2  38 B
--------------------------------------------------------------------------
Timestamps shown in local time
1 snapshots
>>> exit code: 0
LAB="$HOME/rbdr-lab-23"
SRC="$LAB/rbdr-prod2"
REC=$(md5sum < "$LAB/rbdr-rec$SRC/orders.csv" | cut -d' ' -f1)
ORIG=$(cut -d' ' -f1 "$LAB/md5.source2")
printf 'recovered md5 : %s\noriginal md5  : %s\n' "$REC" "$ORIG" \
  | tee -a "$LAB/escrow-drill.txt"
[ "$REC" = "$ORIG" ] && echo "RECOVERED - byte-identical"
Read-only / Safea restore performed with a passphrase production never held
$ restic --password-file /work/recovery-pass restore latest --target /work/rec
$ restic --password-file /work/recovery-pass restore latest --target /work/rec
restoring snapshot b96ba7cf of [/work/prod2] at 2026-08-28 14:04:52.481565631 +0000 UTC by root@17dffded9807 to /work/rec
Summary: Restored 3 files/dirs (38 B) in 0:00
>>> exit code: 0

recovered md5 : 9eb4e2ad8e08e1dcaaf87ababab964b0
original md5  : 9eb4e2ad8e08e1dcaaf87ababab964b0
RECOVERED - byte-identical, using a passphrase production never held

Task 8 — Write the custody record

LAB="$HOME/rbdr-lab-23"
cat > "$LAB/custody-record.txt" <<'EOF'
material,form,custodian,location,unlock path it opens
restic production passphrase,file,platform on-call,production host (destroyed in Task 6),rbdr-repo2 key 4bc6f61a
restic recovery passphrase,file,recovery custodian A,escrow store off the protected estate,rbdr-repo2 key 66c34166
borg paper key,printed block,recovery custodian B,sealed envelope in a different building,rbdr-brepo key material
borg passphrase,written note,recovery custodian A,escrow store off the protected estate,rbdr-brepo passphrase
EOF
cat "$LAB/custody-record.txt"

The two key IDs above are the capture’s. Replace them with the ones your own restic key list printed in Task 4 before you treat this file as a record — a custody document carrying somebody else’s identifiers is the failure this lab is about, written down.

Two custodians, and neither one alone can open the borg repository. The restic rows show why the drill mattered: the top row named a custodian who no longer exists, and no document would have told you that.

Validation

CommandExpected outputExit code
restic backup "$SRC" (Task 2)snapshot saved, one snapshot listed0
restic --password-file "$LAB/rbdr-wrong.pass" snapshots (Task 3)Fatal: wrong password or no key found12
find "$RESTIC_REPOSITORY" -type f | wc -l (Task 3)a non-zero count; the repository is intact0
restic key add --new-password-file "$ESCROW/rbdr-recovery.pass" (Task 4)saved new key with ID followed by 64 hex characters0
restic key list (Task 4)two rows, one marked with *0
borg key export --paper "$BORG_REPO" (Task 5)To restore key use borg key import --paper, then BORG PAPER KEY v1, an id: line and a numbered block (eight lines in the capture)0
env | grep -c '^RESTIC_PASSWORD' (Task 6)01
restic --password-file "$ESCROW/rbdr-recovery.pass" snapshots (Task 7)one snapshot row, 1 snapshots0
restic ... restore latest --target "$LAB/rbdr-rec" (Task 7)Summary: Restored 3 files/dirs0
md5 comparison (Task 7)RECOVERED - byte-identical0
diff of pre- and post-lab state (Cleanup)no output0

The row that carries the lab is the second. If it returns a snapshot listing, a passphrase is still reachable from this shell and the failing case did not happen.

Expected Outcome

One repository, two outcomes, decided entirely by where the key material was held.

MeasureValue
Repository state after site lossintact; the capture measured 11M and 6 files
Open attempt without the production passphraseFatal: wrong password or no key found, exit code 12
Keys present in the repositorythe encrypted master key was there the whole time and did not help
Unlock paths after restic key add2, listed as 66c34166 and 4bc6f61a
Open attempt with escrowed passphraseone snapshot listed, exit code 0
Restore resultSummary: Restored 3 files/dirs (38 B), exit code 0
Recovered md59eb4e2ad8e08e1dcaaf87ababab964b0, matching the source
Actual restore timeread the Actual restore time (s) line from escrow-drill.txt. The capture reported in 0:00 for a 38-byte snapshot on local disk and did not time the wall clock separately; your number is your own and it is the one to record
Actual RPO observedzero for the escrow case: the restored file matched the source byte for byte. For the failing case there is no recovery point at all — the data exists and cannot be read, so the observed RPO is unbounded rather than a number of minutes

Write both rows down. An RPO in minutes assumes a readable recovery point exists, and the first half of this lab produced a repository where one did not.

Troubleshooting

SymptomCause
Fatal: wrong password or no key found in Task 7The escrowed file is not the one that was passed to restic key add, or an editor appended a newline when it was copied. Compare md5sum of the file against the one used in Task 4.
Task 3 prompts enter password for repository instead of failingNo passphrase reached restic: --password-file was omitted, or the path it names does not exist. Press Ctrl-C and re-run with --password-file "$LAB/rbdr-wrong.pass".
Task 3 exits 0 and lists a snapshotA working passphrase is still reachable from this shell — most often RESTIC_PASSWORD or RESTIC_PASSWORD_FILE survived the unset. env | grep RESTIC and start Task 3 again; until it exits 12, the failing case has not happened.
Fatal: unable to open config file from any restic commandRESTIC_REPOSITORY is unset in this shell, or names a directory that was never initialised. Re-export it.
Fatal: wrong password or no key found in Task 4, from restic key addThe command authenticates with the CURRENT passphrase before adding a new one, so RESTIC_PASSWORD_FILE must still point at the production passphrase at that moment. This is why a second key cannot be added after the first one is lost.
restic key list shows only one keyThe key add ran against a different repository. Echo $RESTIC_REPOSITORY and re-run inside the same shell.
borg key export --paper reports Repository ... does not existBORG_REPO is unset or the borg init in Task 5 did not run. Both commands must share one shell.
borg key export produces nothing for a keyfile repositoryWith keyfile modes the key lives in the home directory rather than in the repository; this lab uses repokey-blake2, where it is in the repository.
The md5 comparison in Task 7 reports No such file or directoryrestic nests the absolute source path beneath the target, so the file is at $LAB/rbdr-rec$SRC/orders.csv. Echo that path before comparing.
Task 6 prints a non-zero countA password variable is still exported. unset it and re-run; leaving it set means Task 7 proves nothing about escrow.

Cleanup

LAB="$HOME/rbdr-lab-23"
ESCROW="$HOME/rbdr-escrow-23"
rm -rf "$LAB/rbdr-repo" "$LAB/rbdr-repo2" "$LAB/rbdr-brepo" \
       "$LAB/rbdr-rec" "$LAB/rbdr-prod" "$LAB/rbdr-prod2"
rm -f "$LAB/rbdr-wrong.pass" "$ESCROW/rbdr-recovery.pass" \
      "$ESCROW/rbdr-borg-paper-key.txt"
unset RESTIC_REPOSITORY RESTIC_PASSWORD_FILE BORG_REPO BORG_PASSPHRASE

{
  restic version
  borg --version
  ls -d "$LAB"/rbdr-* "$ESCROW"/rbdr-* 2>&1
  find "$HOME" -maxdepth 1 -name 'rbdr-*' -printf '%f\n' | sort
} | tee "$LAB/state.post-lab"

diff "$LAB/state.pre-lab" "$LAB/state.post-lab" \
  && echo "CLEAN: post-lab state matches the baseline recorded in Task 1"

The diff must print nothing and exit 0. state.pre-lab, state.post-lab, md5.source2, escrow-drill.txt and custody-record.txt are deliverables and are kept; none of them matches the rbdr-* glob, which is why the assertion still holds. The recovery passphrase and the paper key are deleted rather than kept — they are live key material for repositories that no longer exist, and both match the glob, so keeping them would break the baseline as well. The two lab directories themselves are named rbdr-lab-23 and rbdr-escrow-23, so remove them last if you want the home directory clean.

Production notes

  • The drill is the only observation that answers the question. A key inventory, a wiki page and a vault entry all describe an unlock path. Only an attempted open with the production credentials removed demonstrates one. Schedule it, and record the exit code and the elapsed time each run.
  • Two unlock paths, two custodians, one repository. restic key add costs seconds and removes the single point of failure a lone passphrase creates. Adding it after an incident is not possible: the command authenticates with a key that already works.
  • Escrow the passphrase and the exported key material separately. The paper block and the passphrase that opens it should never share a safe, an envelope or a custodian. Either one alone is inert.
  • Rehearse the removal, not just the restore. Task 6 is the step teams skip. A drill run in a shell that still has the production variables exported is a test of the repository, not of the escrow.
  • Record who holds what, and re-check it when people leave. The custody record in Task 8 is a four-column file for a reason: material, form, custodian, location. A custodian who has changed roles is a broken unlock path that no monitoring system will report.
  • Rotation is the same drill in reverse. Adding a replacement key and removing the old one is safe only while an unlock path you have tested still works, so test first, then rotate.

What You Learned

  • Survival of the data and survival of the ability to read it are separate outcomes. An intact repository returned Fatal: wrong password or no key found at exit code 12, and no capacity graph, checksum or replication monitor would have shown that in advance.
  • Possessing the repository is not possessing the ability to open it. The encrypted master key sits in the repository’s keys/ directory. Copying the repository offsite copies the lock along with the door.
  • A second key is an independent path to the same master key. After restic key add, restic key list showed 66c34166 and 4bc6f61a. Neither passphrase derives the other, and either one opens every snapshot.
  • Exported key material outlives the estate. borg key export --paper produced a printable block that survives the loss of every system holding the repository — and remains inert without the passphrase, which is why the two are escrowed to different custodians.
  • The proof is the restore, not the inventory. The recovered md5 9eb4e2ad8e08e1dcaaf87ababab964b0 matched the source, using a passphrase the production host never held. That sentence is what a custody record is trying to be evidence for.

Deliverables

  • · state.pre-lab and state.post-lab — the baseline recorded in Task 1 and the assertion Cleanup diffs against it
  • · md5.source2 — the source checksum recorded before the second repository existed
  • · escrow-drill.txt — both exit codes from the failing case and the escrow case, the measured restore seconds, and the two md5 values
  • · custody-record.txt — one row per piece of key material: form, custodian, location, and the unlock path it opens
  • · rbdr-borg-paper-key.txt — the printable key block, written to the escrow location rather than beside the repository, and destroyed in Cleanup along with the repository it opens

Verification status

Last reviewed
2026-08-28
Executed end to end
2026-08-29