Skip to main content
RunBook Academy

← All runbooks in Backup & DR

medium riskservice affecting~90 min

Restore a complete Linux service

1 · Prerequisites

Confirm every item is in place before any state change.

  • A named service and a named owner who can accept the recovery point that will be chosen, because that choice discards work and is not yours alone to make
  • An incident timeline with at least two timestamps: when the damage began, and when it was noticed. Without the first one there is no criterion for selecting a recovery point
  • Read access to the backup repository, and the passphrase or key from wherever it is escrowed rather than from the host being rebuilt
  • A target host with the operating system release, the mount points and the free capacity the service assumes, sized for the restored data rather than its stored size
  • The package manifest or configuration repository that records which versions were running, since "the current version" is a different system
  • The identity material the service presents: TLS certificate and private key, SSH host keys, service account credentials — sourced from the secret manager or escrow, not from the archive of application data
  • A scratch path with room for a staged copy of the restored tree, so the archive can be inspected before anything is placed in its final location
  • Somewhere to record timings, because the elapsed time of this procedure is the recovery time this service actually has

2 · Pre-checks

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

  • · Confirm the target has never run this service. systemctl list-unit-files | grep rbdr- and getent passwd for the service account. A host with history lends the restore a package set, an account number, a trusted CA and an open port, and every borrowed dependency is one the exercise cannot discover.
  • · Enumerate the dependency set before touching the repository. Packages and their versions, the service account and its numeric UID and GID, unit files and drop-ins, certificates and their trust chain, secrets, name resolution, and every path the service expects to be a mount rather than a directory.
  • · Confirm the repository is readable and lists the snapshots you expect. restic snapshots against the repository, with the credential taken from escrow. A repository that cannot be listed is an escalation, not a slow step.
  • · Verify the recovery point covers the incident window. Compare snapshot timestamps against the moment the damage began. If every available snapshot is later than that moment, stop: this procedure cannot produce clean data and the plan has to change.
  • · Read the archive's own record of ownership. Extract etc/passwd and etc/group from the recovery point into a scratch path and note the numeric UID and GID the service ran as. The fresh installation will allocate whatever number is free, which is very often not that one.
  • · Confirm the restore tool will carry metadata. ACLs, extended attributes, file capabilities, sparse allocation and hard links are all carried only when the tool is told to carry them, and their absence is invisible to a content checksum.
  • · Confirm the mount layout exists before the data lands. A path that was a separate filesystem on the original host and is a directory on the target will fill the root filesystem quietly and pass every check until it does.
  • · Record the start time and the chosen recovery point ID in the incident log before the first write, so the decision is auditable afterwards rather than reconstructed.

3 · Procedure

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

  1. 1Select the recovery point explicitly against the incident timeline, never "the latest". List the snapshots, identify the last one that predates the moment the damage began, and write its ID down. The newest snapshot is the one most likely to contain the damage you are recovering from.
  2. 2Have the service owner accept the data loss implied by that recovery point. Everything written between the snapshot and the incident is discarded. That is a business decision with a name attached, and it belongs in the incident log before the restore, not in the review afterwards.
  3. 3Decide: original host or clean host. Restoring onto the surviving original is faster and proves almost nothing, because the host supplies the dependencies. Restoring onto a host with no history is slower and is the only version that discovers what the backup is missing. Record which you chose and why.
  4. 4Provision the target and create the mount points before any data arrives. Same paths, same filesystem types where the service depends on them, capacity for the restored form of the data.
  5. 5Install the package set at the recorded versions, from the manifest or the configuration repository. Reconstructing the version list from memory during an incident is where reconstruction usually stalls.
  6. 6Decide: configuration management before or after the data restore. Running it first creates the accounts, units and paths the restore needs and is the safe order. Running it after the data restore can overwrite restored files with the templated version, and on a service whose configuration lives beside its data that overwrite is silent.
  7. 7Reconcile numeric identity before anything is extracted. Compare the UID and GID recorded in the archive against what the fresh installation allocated. Align the account to the archive, or plan to re-own the tree — decide once, before files exist with two ownerships in them.
  8. 8Restore into a staging path first, not into the final location. A staged tree can be inspected, checksummed and abandoned. A tree written straight into the service directory cannot.
  9. 9Restore the data with the metadata flags set, then confirm the restore tool reported success and a file count consistent with what you expected. A restore exit status of 1 with most files written is a real outcome and must be read, not assumed away.
  10. 10Prove the restored bytes against a checksum manifest taken before the incident, file by file. This is the step that separates a completed transfer from recovered data.
  11. 11Prove the metadata survived, not only the contents. ACL entries, extended attributes, file capabilities, sparse allocation and hard links. A checksum comparison passes on a tree that has lost all five.
  12. 12Install identity material from escrow: TLS certificate and private key, SSH host keys, service account credentials and API tokens. This material is not application data and is routinely absent from the archive that covers application data.
  13. 13Move the staged tree into place and apply ownership and security labels before anything is started, then relabel for SELinux or AppArmor where the platform uses them.
  14. 14Start the dependencies in order, checking each before starting the next: mounts, then the secret agent or credential helper, then the datastore, then the service, then anything that fronts it. Starting the service before its mount is present writes new data into the underlying directory.
  15. 15Validate with a transaction that exercises the whole path — a write followed by a read-back, plus a read of a record that predates the incident. A listening port proves a binary parsed a configuration file.
  16. 16Record the timings and the recovery point, then hand back deliberately. Time to first byte restored, time to service start, time to a validated transaction, and the total. That total is this service's measured recovery time.

4 · Verification

Confirm the procedure actually fixed the problem.

  • restic snapshots exits 0 and the row for the chosen ID carries a Time earlier than the damage time written in the incident log. The ID in the log and the ID passed to the restore command are the same string, compared character by character rather than recognised at a glance.
  • The restore ran with its status captured — restic restore "$SNAP" --target "$STAGE"; echo "restore exit: $?" — and the recorded line reads restore exit: 0. A Summary: line of the form Restored 6 / 7 files/dirs with a non-zero exit is a failed restore however calm the console looks.
  • md5sum -c run from inside the restored tree against the pre-incident manifest prints OK for every path and exits 0. One FAILED line, or exit 1, means the bytes are not the bytes whatever the restore reported.
  • Metadata survived, checked one command at a time: getfacl -p on the configuration file lists the extra entry rather than only the three base ones; getfattr -d on the indexed file prints the stored attribute rather than nothing; getcap on the capability-bearing binary prints cap_net_raw=ep rather than an empty line; du --apparent-size and du still disagree on the sparse image; and stat -c %h on a hard-linked path still returns a count above 1. Empty output at exit 0 is the failure mode here, not a non-zero status.
  • find "$STAGE" -nouser -o -nogroup prints nothing. find exits 0 whether or not it matched, so the empty output is the result and the exit status is not.
  • systemctl is-active prints active for each unit and exits 0, and it was run after each unit rather than once at the end, so the order the dependencies came up is recorded rather than assumed.
  • journalctl -u rbdr-orders.service over the restore window shows one start, not a repeating start-fail-start cycle. A unit that is restarting every thirty seconds still answers active to is-active in the moment you happen to ask.
  • The write probe returned HTTP 201 and the script did not take its abort branch: test "$CODE" = "201" exited 0. A response body on its own is not the check; the status code is.
  • The read-back of the probe record and the read of a pre-incident record both exited 0 through grep -q. The second is the one that matters: it proves the restored rows are behind the running service rather than an empty schema that answers politely.
  • The four timings are written down and compared against the recovery time objective, and the comparison is reported whether it was met or missed. An unreported overrun silently becomes the plan's number.

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • The service was started and is wrong: stop it before anything else, then unmount the data path. A partly started service writes as it runs, and every second it runs adds records that belong to no consistent recovery point.
  • Roll back in the reverse of the start order — dependents, service, datastore, credential agent, mounts — so nothing restarts the thing you just stopped through a dependency.
  • Keep the staged tree. It is the evidence of what the archive actually contained, and deleting it to reclaim space destroys the only record of a restore that went wrong.
  • If the service wrote into the final path before its mount was present, those writes are underneath the mount and invisible once it is mounted. Unmount, list the directory, and move what is there aside before remounting.
  • If a configuration management run overwrote restored files, do not re-run it to fix the result. Restore the affected paths again from the same recovery point and disable the run until the ordering decision has been made deliberately.
  • If DNS, a load balancer or a monitoring target list was pointed at the rebuilt host, reverse those changes before stopping it, or clients will fail against an address that answers nothing.
  • Reverse the reflex mutations too: silences raised on the alerts for this service, monitor thresholds edited during triage, and any speculative restart timers added while the service was flapping.
  • Record what was restored, to what point, what was discarded and what was rolled back. A restore is a decision about which data survives and needs a written record.

6 · Escalation

When the runbook isn't enough, contact:

  • · Every available recovery point is later than the moment the damage began: escalate to the service owner and the backup platform owner immediately. There is no clean recovery point, and the decision is now about partial recovery or reconstruction, not about this procedure.
  • · The repository reports damaged packs, or a read-data verification fails: escalate to the backup platform owner and switch to the second copy of the repository. Do not repair the only copy you have while it is the only copy you have.
  • · A restore completes with a non-zero exit status and missing files: escalate before starting the service. A partially restored dataset that starts is worse than one that does not, because it begins accepting writes on top of a gap.
  • · Identity material cannot be recovered — the certificate private key or the service credential is not in escrow: escalate to the security owner. New issuance is a human decision and its lead time belongs in the incident timeline now, not when the service is otherwise ready.
  • · The restore is on the critical path of an outage and elapsed time is exceeding the recovery time objective: escalate to the incident owner with the measured remaining work, so they can decide about degraded service instead of waiting.
  • · The original host is alive and may still be writing to shared storage or answering for the same name: escalate before starting the rebuilt service. Two instances believing they own the same data is not recoverable by restarting either of them.
  • · The validation transaction fails after every step reported success: escalate rather than iterating on the service. The failure is usually in the dependencies the rebuilt host was never given — trust store, resolver view, firewall policy, monitoring registration.

A service is not a directory. It is a package set at particular versions, an account with a particular number, a unit file, a certificate, a secret, a name that resolves, a path that is a mount rather than a directory, and — last — some data. On the host that has been serving traffic all week, all but the last of those is already present, so a restore there is forced to produce almost nothing. On a host that has never run the service, every one of them has to arrive from a source you can name, and the ones with no source announce themselves by their absence. That is the whole reason this procedure targets a clean host.

Enumerate before you restore

Work the list before the repository is written anywhere near the service, because each missing item is cheap to find now and expensive to find at the point where it blocks a start-up. Unpack the recovery point into a staging path — with the metadata flags set, so what you inspect is what would have landed — and read the archive’s own view of identity out of it. The numeric ownership recorded inside is the fact the fresh installation will contradict.

SVC=rbdr-orders
STAGE=/srv/rbdr-restore
install -d -m 0750 "$STAGE"
tar --acls --xattrs --xattrs-include='*' -xf /var/backups/rbdr-orders.tar -C "$STAGE"

awk -F: -v s="$SVC" '$1 == s {print "archive uid=" $3 " gid=" $4}' "$STAGE/etc/passwd"
id -u "$SVC" 2>/dev/null || echo "host: the service account does not exist yet"

Then ask the archive whether it contains the things a running host was supplying. Absence here is the finding, not an error:

STAGE=/srv/rbdr-restore
for want in etc/systemd/system etc/ssl/private etc/fstab etc/resolv.conf; do
  if [ -e "$STAGE/$want" ]; then
    printf 'present in archive: %s\n' "$want"
  else
    printf 'MISSING from archive: %s\n' "$want"
  fi
done

Choosing the recovery point

The catalogue does not know which snapshot is correct; it only knows which is newest. Read it against the incident timeline instead.

Read-only / Saferestic snapshots — the catalogue you choose from
$ restic snapshots
ID        Time                 Host          Tags        Paths       Size
-------------------------------------------------------------------------------
3fe43af4  2026-08-28 13:27:02  8211a08b55c3  daily       /work/prod  60.000 MiB
3e349a12  2026-08-28 13:27:03  8211a08b55c3  daily       /work/prod  60.000 MiB
-------------------------------------------------------------------------------
Timestamps shown in local time
2 snapshots

Both rows report the same size, and neither says anything about whether the data inside it is good. There was no incident in this capture; what it does hold is a change written between the two snapshots, and that is the shape of the decision. Apply the rule — take the last snapshot whose time precedes the moment the damage began — and if the change between these two rows were the damage, the correct choice is the older ID, 3fe43af4, which the capture records as the newer one’s parent, rather than the entry at the top of the list. Write the chosen ID into the incident log, and have the service owner accept what is discarded between that timestamp and now.

Decision points

DecisionChoose the first whenChoose the second when
Original host or clean hostThe original host survives, the fault is confined to data, and speed outranks discoveryThe host is gone, the fault is not understood, or this is a rehearsal whose purpose is to find the gaps
Configuration management before or after the data restoreAlmost always: the run creates the accounts, units and paths the restore lands intoOnly when the run is scoped to exclude every path the restore writes, and that scoping has been read

The second decision is the one that bites. A configuration management run after the data restore will happily template a configuration file back over the restored one, and on services that keep configuration beside data it takes the data with it. Running it first, then restoring, means the restore is the last writer and its result is what remains.

Restore, and prove the metadata came too

Content is the easy half. The measured archive-fidelity capture built a source tree carrying an ACL, a user extended attribute, a cap_net_raw=ep file capability, a setuid bit, a sparse file and a hard-linked pair, then compared what four different restores returned.

Configuration changersync -aAXH --sparse — the flags that carry the metadata
$ rsync -aAXH --sparse src/ r4/
  [restored from rsync -aAXH --sparse]
  ACL on app.conf        : 1 entr(y|ies)
  xattr on index.dat     : sha256:deadbeef
  capability on netcheck : cap_net_raw=ep
  setuid bit on admin-tool: present
  sparse.img apparent    : 200M
  sparse.img allocated   : 0
  payload.a link count   : 2
  payload a/b same inode : yes

That is the shape of a restored tree that will start as itself: the ACL granting a second account read access is intact, the capability that lets an unprivileged binary open a raw socket is intact, the sparse image still allocates nothing, and two paths still share an inode.

Prove the metadata deliberately, and prove that every numeric owner in the tree resolves to an account this host actually has:

STAGE=/srv/rbdr-restore
DATA="$STAGE/var/lib/rbdr-orders"
getfacl -p "$STAGE/etc/rbdr-orders/app.conf"
getfattr -d "$DATA/index.dat"
getcap "$STAGE/usr/local/bin/rbdr-netcheck"
du --apparent-size -h "$DATA/rbdr-sparse.img"
du -h "$DATA/rbdr-sparse.img"
stat -c '%n links=%h' "$DATA/payload.a"
find "$STAGE" -nouser -o -nogroup

Each of those exits 0 whether or not the metadata is there, so read the output rather than the status. An empty getcap line and a getcap line naming a capability are the same exit code and different systems.

Prove the bytes, not the job

A restore reports what it wrote. Only a comparison against a manifest taken before the incident reports whether what it wrote is correct.

Service impact possiblerestore, then verify against the pre-incident checksums
$ restic restore 3fe43af4 --target /work/restore
restoring snapshot 3fe43af4 of [/work/prod] at 2026-08-28 13:27:02.65376235 +0000 UTC by root@8211a08b55c3 to /work/restore
Summary: Restored 7 files/dirs (60.000 MiB) in 0:00
>>> exit code: 0

--- comparing the restored tree against the 09:00 checksums ---
./app/app.conf: OK
./app/orders.csv: OK
./db/data.bin: OK
>>> md5sum -c exit code: 0

Two exit statuses, read separately. In the same capture a damaged repository produced a restore that reported Restored 6 / 7 files/dirs and exited 1, and the verification that followed returned ./db/data.bin: FAILED. The restore status tells you the transfer’s outcome; the manifest comparison tells you whether the data is the data.

Start dependencies in order

for unit in srv-rbdr.mount rbdr-secret-agent.service rbdr-orders.service; do
  systemctl start "$unit"
  if ! systemctl is-active --quiet "$unit"; then
    printf 'abort: %s did not reach active\n' "$unit" >&2
    exit 1
  fi
done

Mounts first, because a service started before its data path is mounted writes into the underlying directory, where the writes become invisible the moment the mount succeeds. Credential helpers next, because a service that starts without its secret usually starts successfully and fails on its first outbound call.

Signatures and what they usually mean

What you seeWhere to look first
Restore exits 0, md5sum -c prints FAILED on one pathThe repository, not the restore. A pack read back wrong and the transfer still completed
Service starts, then permission errors the configuration does not explainThe ACL or the file capability did not travel. Content matched; metadata did not
The data path fills the root filesystemA path that was a separate filesystem at source is a plain directory here
Files written during start-up disappearThe service started before its mount; those writes are underneath it
Every unit is active, the transaction fails during the TLS handshakeIdentity material came from the archive instead of escrow, or the trust store was never populated
The restored tree lists numeric owners instead of namesThe fresh installation allocated a different UID and the archive’s numbers were never reconciled

Abort criteria

Stop the procedure and escalate, rather than continuing, when any of these is true: no snapshot predates the moment the damage began; the repository cannot be read with the escrowed credential; the restore exits non-zero with files missing; the checksum comparison fails on any file; the numeric owner check returns paths with no matching account and the account cannot be reconciled; the identity material is not in escrow; or the original instance is still writing to shared storage. Each of these makes the next step actively harmful, because from the moment the service starts it accepts writes that belong to no consistent recovery point.

Business validation

A running process proves that a binary parsed a configuration file. Validate with a transaction that traverses the whole path — certificate, trust store, route, account permissions on the data directory, and the restored rows:

BASE=https://rbdr-orders.restore.example.internal
CA=/etc/ssl/certs/rbdr-internal-ca.pem
NEW=/tmp/rbdr-probe.json
CODE=$(curl -sS --cacert "$CA" -X POST "$BASE/api/orders" \
  -H 'content-type: application/json' \
  -d '{"ref":"RBDR-PROBE-1","amount":1.00}' \
  -o "$NEW" -w '%{http_code}')
test "$CODE" = "201" || { printf 'abort: create returned HTTP %s\n' "$CODE" >&2; exit 1; }
curl -sS --cacert "$CA" "$BASE/api/orders/RBDR-PROBE-1" | grep -q 'RBDR-PROBE-1'
curl -sS --cacert "$CA" "$BASE/api/orders/ORDER-1001" | grep -q 'ORDER-1001'

The write proves the service can accept work. The read-back proves it persisted. The final line, reading a record that existed before the incident, proves the restored data is behind the running service rather than an empty schema that happens to answer.

What to record

The recovery point ID and its timestamp; who accepted the discarded window; which host type was chosen and why; whether configuration management ran before or after; every dependency that was missing from the archive and where it was sourced from instead; the two exit statuses from the restore and the manifest comparison; and four timings — first byte restored, service started, transaction validated, total. That total is the recovery time this service has, as opposed to the one written in the plan, and the two are frequently different numbers.

References

  1. restic documentation, Restoring from backup
  2. restic documentation, Working with repositories
  3. GNU tar manual
  4. rsync manual page
  5. systemd.unit manual page
  6. NIST SP 800-34 Rev. 1, Contingency Planning Guide for Federal Information Systems