Skip to main content
RunBook Academy

← All runbooks in Backup & DR

low riskinformational~90 min

Run a restore test

1 · Prerequisites

Confirm every item is in place before any state change.

  • A scheduled window and a named owner. A restore test performed when somebody had a spare afternoon produces a result nobody can compare against the previous one.
  • The service tier and testing cadence for the system under test, so the level chosen is the level the tier is owed rather than the level that fits the afternoon.
  • Read access to the repository or snapshot source, and a credential that is separate from the one the backup job uses. A test that only works with the job's own identity has not tested the recovery identity.
  • A target that is not the original: a fresh host, a fresh namespace, a fresh cluster, or a fresh account. Not the production system, not a machine that already has the application's packages on it.
  • A manifest recorded at backup time — paths and checksums — held outside the repository being tested.
  • A business invariant recorded before the recovery point, supplied by the data owner: a row count, a sum over a money column, a file checksum. In the measured k3s capture this was the orders.csv md5 9eb4e2ad8e08e1dcaaf87ababab964b0.
  • The previous test record for this system, so elapsed times and gap counts are a trend rather than a first observation.
  • Authority to destroy the test target when the exercise finishes, and somewhere to file the evidence that is not a personal directory.

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · Write down the system, the level and the date before touching anything. Every object created below is prefixed rbdr- so the cleanup at the end can be scoped and asserted, and so nothing collides with the reader's own workloads.
  • · State in advance what this test will prove and what it will not. A level-four test rebuilds a service on clean infrastructure; it says nothing about the copy at the second site, nothing about failback, and nothing about a hostile cause. Writing the limits down before the result exists is what stops the report from overclaiming afterwards.
  • · Confirm the target is genuinely not the original. A "clean" host that was imaged from the production host, or a namespace on the production cluster, hides exactly the dependencies the exercise exists to surface.
  • · Confirm the test cannot write to production. Check the credentials the target holds, the DNS names it will answer to, the queues it will consume from and the replication it might join. This is the single check that keeps the exercise low risk.
  • · Confirm the manifest and the invariant both predate the recovery point, and confirm who supplied them. A property computed from the restored data proves only that the restored data equals itself.
  • · Confirm the repository is opened read-only, or that the test path cannot prune. The exercise reads recovery points; it never manages them.
  • · Confirm capacity on the target before starting, not at 70% through the copy. An out-of-space failure halfway through a retrieval is a wasted window, not a finding.
  • · Note the wall-clock start time and the person on the keyboard. Both are part of the result. A test run by the author of the procedure measures the procedure less honestly than one run by whoever is on shift.

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1Choose the system from the tier schedule, not from convenience. The system that is easiest to restore is the one that least needs testing, and an estate that keeps testing it reports a coverage figure that is true and meaningless.
  2. 2Choose the level from the maturity progression and record it. Reading one object proves reachability; restoring a dataset onto an isolated target proves the chain resolves; rebuilding the service on clean infrastructure is the first level indexed by the service rather than by the repository, so it is the first that can find something never backed up at all.
  3. 3Write the two sentences that bound the result: what this level proves, and what it leaves unproved. Both go in the record. The second sentence is the one that gets omitted, and its absence is how a level-four result gets read as a disaster recovery capability.
  4. 4Select the recovery point deliberately and name it before restoring. Never "the latest". The latest is the one the nightly job already exercises by existing, and it sits at the shallow end of every incremental chain.
  5. 5Choose an old recovery point on some scheduled fraction of tests, and say which one and why. A recovery point near the retention horizon is the case nobody exercises: the longest chain, the coldest storage class, the oldest encryption key, the schema the current application no longer writes.
  6. 6Provision the target from nothing and start the clock on the first stage. Do not reuse yesterday's test host. Reuse is how a dependency that the target already happened to have stops being discoverable.
  7. 7Time each stage separately: provision, retrieve, restore, assemble, validate. A single total hides which stage is the problem, and it is almost never the one people assume. Backup duration is not a predictor of restore duration, because backup is incremental and restore is not.
  8. 8Record every item the clean target turned out to need that did not come from the recovery point. A package no upstream still carries, a certificate installed by hand, a DNS record created outside the repository, a kernel module, a locale, a user id. This list is the primary output of a level-four test and is worth more than the elapsed time.
  9. 9Reconcile the restored tree against the manifest before looking at the application. Count what matched, and list what did not, path by path. A restore that finishes with the right shape and the wrong contents passes every plausibility check a human performs.
  10. 10Check the business invariant recorded before the recovery point. The k3s capture compared recovered md5 : 9eb4e2ad8e08e1dcaaf87ababab964b0 against the original and got byte identity. A count without a checksum survives a restore that lost the values; a checksum without a count survives one that lost rows in equal and opposite directions.
  11. 11Have the data owner look at records they chose in advance. This is the step that converts "the numbers reconcile" into "the business would have accepted this", and it is not yours to perform.
  12. 12Decide explicitly: continue, or stop and convert the test into an incident. A failed restore test is a production finding about a system that is currently unprotected, and the decision belongs in the record with its reasoning.
  13. 13Destroy the test target, then assert it is gone. A half-decommissioned restore target with production data on it and no monitoring is the most likely thing this exercise leaves behind.
  14. 14File the result against the previous one for the same system, and publish the gap list. The report the estate makes is the date of the last successful test per tier, the measured stage timings, and the open findings — not a percentage.

4 · Verification

Confirm the procedure actually fixed the problem.

  • The record names the system, the level, the date, the operator, and the two sentences bounding what the test proves and does not prove — and the second sentence was written before the result existed.
  • The recovery point is named by identifier and timestamp, with the reason it was chosen, and the reason is not "it was the newest".
  • The target can be shown not to be the original: a build log or provisioning record that starts from an unconfigured image, account or namespace.
  • Five stage timings exist, not one total, and each is attributable to a stage a reader could repeat. grep -c '^stage=' "$RBDR_LOG" prints 5 and exits 0; any smaller number means two stages were timed as one.
  • The manifest reconciliation lists matched and unmatched paths. grep -v ': OK$' "$RBDR_RESULT" prints nothing and exits 1, or every line it does print has a written explanation. An unexplained line means the test did not pass.
  • The business invariant matches the value recorded before the recovery point, and the recorded value is traceable to the data owner who supplied it.
  • The gap list exists as a file with one line per item, even when it is empty — an empty gap list that was deliberately produced is evidence, and a missing one is not.
  • The data owner has confirmed by name and at a recorded time, against records they chose in advance.
  • The test target no longer exists, and the assertion is a command output rather than a recollection: rbdr-assert-absent looked for the target by name, reported that it found nothing, and exited 0.
  • The repository ended the exercise with the recovery points it started with: the closing restic snapshots listing in the log names the same identifiers as the listing captured at selection time.

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • Destroy the test target first and assert the destruction. Restored production data sitting on an unmonitored host is a data exposure the exercise created, and it outlives the exercise by default.
  • Remove the restored dataset, the extracted archives and the temporary manifests from any staging area, including the operator's own scratch directory.
  • Revoke the credential, token, client certificate or firewall exception opened so the target could reach the repository or the identity provider.
  • Reverse the reflex mutations. Alert silences opened so the test would not page anyone, monitor edits, thresholds relaxed "just for the window", and any check repointed at the test hostname.
  • Remove the test target from DNS, from service discovery, from the load balancer and from the configuration management inventory, in that order. The DNS record is the one that gets left.
  • Confirm the repository is unchanged: same recovery points, same retention, no new locks held by the test identity. The exercise reads; if it wrote, say so and say what.
  • Leave the evidence in place even when the test failed. The stage timings and the gap list from a failed exercise are the only honest input to the next recovery time estimate.

6 · Escalation

When the runbook isn't enough, contact:

  • · The selected recovery point cannot be opened at all — the endpoint refuses, the credential is rejected, the passphrase does not work: escalate immediately and treat the system as unprotected until proven otherwise. This is not a test failure, it is a live finding.
  • · The chain is incomplete: a parent increment is missing and the restore still produced a directory that looks complete. Escalate to the backup platform owner with the recovery point and the missing member, because every recovery point downstream of it is now suspect.
  • · The manifest reconciliation shows unmatched content that nobody can explain within the window: escalate to the data owner and the platform owner together, and stop the exercise rather than continuing to the application checks.
  • · The gap list contains something that cannot be reconstructed at all — a key, a certificate authority, a licence, an artefact whose upstream is gone: escalate to the service owner the same day. A gap that has no source is an unrecoverable service, not a test finding.
  • · The measured stage timings put the recovery beyond the recovery time objective this tier was signed off against: escalate to whoever owns the objective. Whether to fund the gap or restate the objective is a business decision, not an engineering one.
  • · The test target has taken writes from, or sent writes to, a production system: escalate at once and treat it as a production incident. Nothing else in this runbook matters until that path is closed.
  • · The exercise cannot be completed because the owner of the target, the credential or the window is unavailable: escalate rather than substituting a level-three test and reporting it as level four.

This is the scheduled exercise, not the incident. Nothing here is urgent, which is precisely why it is the only opportunity anyone gets to find out what the recovery actually costs before somebody needs it. The output is not a pass mark. It is a set of numbers and a list of things that were missing.

Declare the level and its limits before you start

LevelWhat it is the first to proveWhat it still leaves unproved
Read one object from a recovery pointThe source answers now: endpoint, credential, passphrase, catalogueAnything you did not read
Restore one file against a recorded checksumSilent content damage, by comparing against something the backup system did not produceEvery object you did not touch
Restore a dataset onto an isolated targetThe whole chain resolves, and a real restore duration for real dataWhether a service can be assembled around it
Rebuild the service on clean infrastructureSomething that was never backed up at allThe site the primary occupied
Recover at the second siteThe far copy, the transfer, the DNS and the identity pathGetting back
Full exercise with failbackThe return, and the state written during the exerciseThat the estate survives a hostile cause

Write the level down, and write the row on its right down with it. That second sentence is what stops a rebuilt service being reported as a disaster recovery capability three months later, when the person who ran it has moved on.

RBDR_TEST_ID="rbdr-restore-test-$(date -u +%Y%m%dT%H%MZ)"
RBDR_SYSTEM=rbdr-orders
RBDR_LEVEL=4
RBDR_LOG="/srv/rbdr-evidence/$RBDR_TEST_ID.log"
mkdir -p "$(dirname "$RBDR_LOG")"
{
  printf 'test_id=%s system=%s level=%s\n' "$RBDR_TEST_ID" "$RBDR_SYSTEM" "$RBDR_LEVEL"
  printf 'proves=service rebuilt on clean infrastructure from a named recovery point\n'
  printf 'does_not_prove=second-site retrieval, failback, hostile cause\n'
} | tee "$RBDR_LOG"

Select the recovery point, and sometimes select an old one

RBDR_CUTOFF=$(date -u -d '45 days ago' +%Y-%m-%d)
printf 'long-chain candidates are the points dated %s or earlier\n' "$RBDR_CUTOFF" \
  | tee -a "$RBDR_LOG"
restic snapshots | tee -a "$RBDR_LOG"

# Type the identifier and the reason yourself. A script that picks the point
# for you has made the one choice this step exists to record, and the listing
# above is also the baseline the repository is compared against at the end.
read -r -p 'recovery point id: ' RBDR_SNAP
read -r -p 'why this one: ' RBDR_REASON
printf 'recovery_point=%s reason=%s\n' "$RBDR_SNAP" "$RBDR_REASON" | tee -a "$RBDR_LOG"

The newest recovery point is the one the nightly job exercises simply by existing. An old one is a different test: the longest incremental chain, the coldest storage class, the oldest encryption key, and a schema the current application may no longer write. Chain length is the risk that goes unnoticed, because a chain with a hole in it does not announce itself. In the measured GNU tar capture, replaying level 0 and level 2 with level 1 missing produced this: “The restore did not fail. It produced a directory that looks complete, with the newest app.conf and a silently incomplete orders.csv.”

Restore onto infrastructure that is not the original

Configuration changeevery object came back Running, and the service still had nothing to serve
$ rebuild the namespace from its complete declared state, then read the business data
  pod/rbdr-orders-writer   1/1   Running   0     6s

--- and the business data? ---
total 8
drwxrwxrwx    2 root     root          4096 Aug 28 14:34 .
drwxr-xr-x    1 root     root          4096 Aug 28 14:34 ..
cat: can't open '/data/orders.csv': No such file or directory
command terminated with exit code 1

Everything the repository described came back, and the thing the business cared about did not, because it had never been in the repository. That gap is only visible on a target that starts with nothing. Rebuild the host, the namespace or the account from an unconfigured image; do not reuse the target from last quarter, because anything it already happens to have stops being discoverable.

Time each stage, not the exercise

Read-only / Safethe number the nightly job reports, and the number the incident needs
$ compare first backup, unchanged backup and restore of the same 400 MiB
  first backup of 400 MiB : 1.49s
second backup, unchanged: .73s
restore of 400 MiB      : 1.07s

The middle number is the one the schedule reports every night, and it is the least useful of the three. Record five timings — provision, retrieve, restore, assemble, validate — because a single total hides which stage is the problem and it is rarely the one people expect.

rbdr_stage() {
  RBDR_NAME=$1
  shift
  RBDR_T0=$(date +%s)
  "$@"
  RBDR_RC=$?
  RBDR_T1=$(date +%s)
  printf 'stage=%s seconds=%s rc=%s\n' "$RBDR_NAME" "$((RBDR_T1 - RBDR_T0))" "$RBDR_RC" \
    | tee -a "$RBDR_LOG"
  return "$RBDR_RC"
}
rbdr_stage provision /usr/local/bin/rbdr-provision-target "$RBDR_TEST_ID"
rbdr_stage retrieve /usr/local/bin/rbdr-fetch-repository "$RBDR_SNAP"
rbdr_stage restore restic restore "$RBDR_SNAP" --target /srv/rbdr-restore
rbdr_stage assemble /usr/local/bin/rbdr-assemble-service "$RBDR_TEST_ID"
rbdr_stage validate /usr/local/bin/rbdr-invariant-check "$RBDR_TEST_ID"

The four rbdr- helpers stand for whatever your site already uses to build a host, bring the repository within reach, assemble the service and check the invariant; the wrapper around them is the part worth copying. Keep retrieve and restore apart even when one command appears to do both. Retrieval is the stage that pays for cold storage class, offsite bandwidth and media handling, and it is the stage that disappears from an estimate when the two are timed together.

Reconcile against a manifest, then against the business

RBDR_MANIFEST="/srv/rbdr-manifests/$RBDR_SNAP.sha256"
RBDR_RESULT="/srv/rbdr-evidence/$RBDR_TEST_ID-manifest.txt"
# The subshell keeps the directory change local, so a missing restore tree
# fails this command instead of leaving the responder somewhere unexpected.
( cd /srv/rbdr-restore && sha256sum -c "$RBDR_MANIFEST" ) > "$RBDR_RESULT" 2>&1
printf 'manifest_matched=%s\n' "$(grep -c ': OK$' "$RBDR_RESULT")" | tee -a "$RBDR_LOG"
grep -v ': OK$' "$RBDR_RESULT" | tee -a "$RBDR_LOG"
Read-only / Safea property recorded before the recovery point, compared afterwards
$ read the recovered file and compare its digest with the one recorded at backup time
  ORDER-1001,4500.00
ORDER-1002,1250.00
recovered md5 : 9eb4e2ad8e08e1dcaaf87ababab964b0
original md5  : 9eb4e2ad8e08e1dcaaf87ababab964b0
RECOVERED - the application data is back, byte-identical

The manifest answers whether the bytes arrived. The invariant answers whether the business would recognise them. Both were recorded before the recovery point, which is the only reason either means anything.

Record what the clean target turned out to need

RBDR_GAPS="/srv/rbdr-evidence/$RBDR_TEST_ID-gaps.tsv"
: > "$RBDR_GAPS"
rbdr_gap() { printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$RBDR_GAPS"; }
rbdr_gap certificate /etc/ssl/private/rbdr-orders.key 'installed by hand, outside every protected path'
rbdr_gap package rbdr-legacy-driver-1.4 'no upstream mirror still carries this version'
rbdr_gap dns rbdr-orders.example.internal 'created by ticket, not by the infrastructure repository'
wc -l < "$RBDR_GAPS" | tee -a "$RBDR_LOG"

This file is the primary output of the exercise. Produce it even when it is empty, because an empty gap list that somebody deliberately created is evidence and a missing one is not.

Decision point: continue, or stop

What happenedDecisionWhy
A stage failed and the cause is in the test harnessFix and continue, note the lost time separatelyThe harness is not the system under test
The recovery point will not open at allStop, raise a finding todayThe system is unprotected right now
The chain has a missing memberStop, raise a finding, keep the partial restoreEvery recovery point downstream is suspect
The manifest has unexplained mismatchesStop before the application checksLater checks would describe the wrong data
A gap has no source anywhereContinue, but raise it the same dayThe exercise found an unrecoverable component
The timings exceed the tier objectiveContinue, then escalate the numberThe objective, not the test, is what has to change

Abort criteria

Stop the exercise, destroy the target and escalate when any of these hold.

  • The target has written to, or been written to by, a production system.
  • The test path has modified the repository: a lock, a prune, a retention change.
  • The restored dataset has reached a network the data classification does not allow, or a host outside the agreed boundary.
  • The window has expired and continuing would run into a change freeze or a business-critical period.
  • The restore is consuming capacity that a production system shares.

Destroy the target, then prove it

/usr/local/bin/rbdr-destroy-target "$RBDR_TEST_ID"
/usr/local/bin/rbdr-assert-absent "$RBDR_TEST_ID" | tee -a "$RBDR_LOG"
rm -rf /srv/rbdr-restore
restic snapshots | tee -a "$RBDR_LOG"

The assertion matters more than the destruction. “I deleted it” is a recollection; a command that looks for the target and reports nothing is a record. The closing snapshot listing is compared against the one captured at selection time, and the two must name the same recovery points: the exercise reads the repository and is finished only when it can show it changed nothing there.

What the test feeds

The estate report is not a percentage. It is, per service tier: the date of the last completed test, the level it reached, the five stage timings, the number of open gaps, and the age of the oldest unresolved one. A business validation sits alongside it — the data owner’s confirmation, by name and time, against records they chose in advance. That sentence is what turns an engineering exercise into something a service owner can sign.

What to record

The system, level, date and operator. The two bounding sentences. The recovery point identifier, its timestamp, and why that one. The five stage timings. The manifest result, matched and unmatched. Both halves of the invariant, expected and actual. The gap list. The decision taken at each decision point and its reason. The destruction assertion. Finally the delta against the previous test for the same system, because a single result is an anecdote and the second one is the first piece of information.

Cross-course references

  • Linux for Production Sysadmins, Part XLIX (Restore) — the file-level restore mechanics that the retrieve and restore stages are timing.
  • Kubernetes for Production Sysadmins, Part XCV (Backup Strategy) — why the declared state and the volume contents are two separate protected sets, which is the gap the level-four capture above exposes.
  • Observability for Production Sysadmins, Part LXII (Business Metrics) — the standing measurement that makes a business invariant something already recorded rather than something reconstructed during the exercise.

References

  1. NIST SP 800-84, Guide to Test, Training, and Exercise Programs for IT Plans and Capabilities
  2. NIST SP 800-34 Rev. 1, Contingency Planning Guide for Federal Information Systems
  3. restic documentation, Restoring from backup
  4. GNU tar manual
  5. k3s documentation, Backup and Restore
  6. GNU coreutils manual