Skip to main content
RunBook Academy

← All labs in Linux

Lab · advanced · ~90 min

Lab: Restore drill - validate the backup strategy end-to-end

B · Nested virtualisationC · Simulation

Objectives

  • Capture a pre-restore baseline of content and metadata
  • Run a complete restore drill
  • Time each step against real work
  • Validate ownership, permissions, ACLs and extended attributes after the restore
  • Document findings
  • Improve the runbook based on findings

Prerequisites

This lab runs a complete restore drill: from backup to service running. By the end you will have a validated restore procedure.

Objective

Restore a production application onto a clean host from backup alone, measure how long each step really takes, and prove the restored data is identical to the original — content and metadata. A drill that cannot fail has verified nothing, so this one has a validation gate that fails on ownership, permission, ACL or extended-attribute drift.

Architecture

One host, three roles:

  • Source of truth — the pre-restore baseline captured from the live application host (or from a snapshot of it).
  • Backup store — the repository holding the most recent backup, plus its encryption key in the vault.
  • Target — a clean test host with the application package installed but no application data.

The drill only proves anything if the target is genuinely clean. A target with leftover data from an earlier drill will pass validation for reasons that have nothing to do with the backup.

Requirements

  • A test host with the application installed and the service stopped.
  • Read access to the backup repository and its encryption key in the vault. Retrieving the key is part of the drill — do not pre-stage it.
  • getfacl, getfattr (package attr), sha256sum, systemctl, curl.
  • On RHEL-family targets, ls -Z and a working SELinux policy.
  • A maintenance window on the target. The drill destroys and rebuilds the application data directory.

Scenario

The application myapp stores its data in /var/lib/myapp, runs as the myapp user, and has an RTO of 4 hours. Backups run nightly to an encrypted repository. Nobody has restored from it in production. Your job is to find out, today, whether it works.

Tasks

Task 1: Capture the pre-restore baseline

Everything the validation gate compares against comes from here. Take it from the live host, before you touch the target.

# Content and layout, with mode and ownership
sudo find /var/lib/myapp -printf '%p %m %U:%G\n' | sort > /tmp/pre-restore.txt

# POSIX ACLs
sudo getfacl -R -p /var/lib/myapp > /tmp/pre-restore.acl

# Extended attributes (includes security.selinux on RHEL family)
sudo getfattr -R -d -m - /var/lib/myapp > /tmp/pre-restore.xattr

# Content hashes
sudo find /var/lib/myapp -type f -exec sha256sum {} + | sort > /tmp/pre-restore.sha256

# Copy all four off the host - they must survive the target being wiped
scp /tmp/pre-restore.* operator@jump-host:/srv/drills/

Task 2: Prepare the target

sudo systemctl stop myapp
sudo mv /var/lib/myapp /var/lib/myapp.pre-drill
sudo mkdir -p /var/lib/myapp

Keep /var/lib/myapp.pre-drill until the drill is signed off. It is your undo.

Task 3: Execute the restore, timing each step

Time each step by bracketing real work. A timestamp taken immediately after another timestamp measures nothing.

#!/usr/bin/env bash
set -euo pipefail

step() {                       # step <label> <command...>
  local label="$1"; shift
  local t0 t1
  t0=$(date +%s)
  "$@"
  t1=$(date +%s)
  printf '%-24s %6ss\n' "$label" "$((t1 - t0))" | tee -a /tmp/drill-timings.txt
}

DRILL_START=$(date +%s)

step 'vault key retrieval'  vault kv get -field=key secret/myapp/backup
step 'repository mount'     sudo mount /dev/sdb1 /mnt/backup
step 'data extract'         sudo borg extract --numeric-ids /mnt/backup/repo::myapp-latest
step 'ownership + contexts' sudo restorecon -R /var/lib/myapp
step 'service start'        sudo systemctl start myapp
step 'health check'         curl -fsS http://localhost/health

printf '%-24s %6ss\n' 'TOTAL' "$(( $(date +%s) - DRILL_START ))" \
  | tee -a /tmp/drill-timings.txt

Task 4: Smoke-test the service

# Service running
systemctl status myapp

# Smoke test
curl -I http://localhost/health

# Data verification
psql -c "SELECT count(*) FROM users;"

# Compare with expected state
diff /etc/myapp/config.yml /backup/expected-config.yml

These checks prove the service came up. They do not prove the restore was correct — a service will happily start on a data directory whose permissions are wrong in ways that only bite on the next write. The Validation section is what proves the restore.

Task 5: Document the drill

Fill the step timings from /tmp/drill-timings.txt and the metadata results from the Validation section. Do not write numbers you did not measure — an RTO figure that came from an estimate rather than a stopwatch is the thing this drill exists to replace.

RESTORE DRILL REPORT
Date: 2026-08-09
Backup used: backup-2026-08-02
Target: test-host-01
RTO target: 4 hours
Actual time: 38 minutes          <- TOTAL line from /tmp/drill-timings.txt

Steps (measured, not estimated):
1. Vault key retrieval: 2 min
2. Repository mount: 1 min
3. Data extract: 18 min
4. Ownership + contexts: 2 min
5. Service start: 5 sec
6. Health check: 3 min

Validation gate:
- Ownership/mode diff: PASS
- ACL diff: PASS
- xattr/SELinux diff: FAIL - 412 files restored as default_t
- Content hashes: PASS
- Service health: PASS
RESULT: FAIL (metadata)

Findings:
- Restore time well within RTO
- No content corruption detected
- Service starts cleanly, but the backup job omits --xattrs, so
  SELinux contexts do not survive the round trip

Issues found:
- The backup list was slow; investigate
- The encryption key is in a different vault than the runbook
  says; update the runbook
- The backup job runs tar without --acls --xattrs --selinux

Improvements to make:
1. Add --acls --xattrs --selinux to the backup job and re-drill
2. Update the runbook to point to the correct vault
3. Investigate slow backup list
4. Add a test for "find the vault" to the drill

A drill that reports FAIL is a successful drill. It found a real defect in the backup job, in a maintenance window, on a test host. The alternative was finding it during a real restore.

Task 6: Update the runbook

Based on findings, update the runbook:

  • Correct the vault reference.
  • Add notes about backup list timing.
  • Add the new “find the vault” test.

Task 7: Schedule the next drill

Quarterly cadence means four fields in the month column, not one.

# Restore drill: 02:00 on the 9th of Feb, May, Aug, Nov
0 2 9 2,5,8,11 * root /opt/scripts/restore-drill.sh

Check the expression before you trust it. 0 2 9 11 * is minute 0, hour 2, day-of-month 9, month 11 — that fires once a year, on 9 November. A schedule labelled quarterly that runs annually is worse than no schedule, because the calendar entry stops anyone asking.

# Confirm the next few firings
sudo systemd-analyze calendar --iterations=4 '*-02,05,08,11-09 02:00:00'

Validation

The drill passes only if all five gates pass. Run them on the target, against the baseline captured in Task 1.

# 1. Ownership and mode match the pre-restore baseline
sudo find /var/lib/myapp -printf '%p %m %U:%G\n' | sort > /tmp/post-restore.txt
diff /tmp/pre-restore.txt /tmp/post-restore.txt \
  && echo 'PASS: ownership/mode intact'

# 2. POSIX ACLs survived the round trip
sudo getfacl -R -p /var/lib/myapp > /tmp/post-restore.acl
diff /tmp/pre-restore.acl /tmp/post-restore.acl \
  && echo 'PASS: ACLs intact'

# 3. Extended attributes and SELinux contexts survived
sudo getfattr -R -d -m - /var/lib/myapp > /tmp/post-restore.xattr
diff /tmp/pre-restore.xattr /tmp/post-restore.xattr \
  && echo 'PASS: xattrs intact'
ls -Z /var/lib/myapp | head        # RHEL family: must not be default_t

# 4. Content is complete and byte-identical, not merely present
sudo find /var/lib/myapp -type f -exec sha256sum {} + | sort > /tmp/post-restore.sha256
diff /tmp/pre-restore.sha256 /tmp/post-restore.sha256 \
  && echo 'PASS: content identical'

# 5. The service starts and serves
systemctl is-active myapp && curl -fsS http://localhost/health \
  && echo 'PASS: service healthy'

Expected Outcome

At the end of the drill:

  • /var/lib/myapp on the target is byte-identical to the baseline, with matching ownership, modes, ACLs and extended attributes.
  • /tmp/drill-timings.txt holds a per-step breakdown measured with a stopwatch, and the total is comfortably inside the 4-hour RTO.
  • The report records a PASS or FAIL per gate, with the FAILs traced to a specific backup-job option.
  • The runbook has been corrected where the drill contradicted it.
  • The next drill is scheduled with a cron expression you verified fires quarterly.
  • /var/lib/myapp.pre-drill has been removed, once and only once the gates pass.

Deliverables

  • · Pre- and post-restore baseline files for content, ACLs and xattrs
  • · Measured per-step timings
  • · Completed restore drill report with a pass/fail per validation gate
  • · Updated restore runbook
  • · Improvements to the backup system

Verification status

Last reviewed
2026-08-09
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.