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-andgetent passwdfor 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 snapshotsagainst 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/passwdandetc/groupfrom 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 snapshotsexits 0 and the row for the chosen ID carries aTimeearlier 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 readsrestore exit: 0. ASummary:line of the formRestored 6 / 7 files/dirswith a non-zero exit is a failed restore however calm the console looks. - ✓
md5sum -crun from inside the restored tree against the pre-incident manifest printsOKfor every path and exits 0. OneFAILEDline, or exit 1, means the bytes are not the bytes whatever the restore reported. - ✓Metadata survived, checked one command at a time:
getfacl -pon the configuration file lists the extra entry rather than only the three base ones;getfattr -don the indexed file prints the stored attribute rather than nothing;getcapon the capability-bearing binary printscap_net_raw=eprather than an empty line;du --apparent-sizeanddustill disagree on the sparse image; andstat -c %hon 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 -nogroupprints nothing.findexits 0 whether or not it matched, so the empty output is the result and the exit status is not. - ✓
systemctl is-activeprintsactivefor 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.serviceover the restore window shows one start, not a repeating start-fail-start cycle. A unit that is restarting every thirty seconds still answersactivetois-activein the moment you happen to ask. - ✓The write probe returned HTTP
201and 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.
$ restic snapshotsID 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 snapshotsBoth 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
| Decision | Choose the first when | Choose the second when |
|---|---|---|
| Original host or clean host | The original host survives, the fault is confined to data, and speed outranks discovery | The 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 restore | Almost always: the run creates the accounts, units and paths the restore lands into | Only 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.
$ 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 : yesThat 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.
$ restic restore 3fe43af4 --target /work/restorerestoring 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: 0Two 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 see | Where to look first |
|---|---|
Restore exits 0, md5sum -c prints FAILED on one path | The repository, not the restore. A pack read back wrong and the transfer still completed |
| Service starts, then permission errors the configuration does not explain | The ACL or the file capability did not travel. Content matched; metadata did not |
| The data path fills the root filesystem | A path that was a separate filesystem at source is a plain directory here |
| Files written during start-up disappear | The service started before its mount; those writes are underneath it |
Every unit is active, the transaction fails during the TLS handshake | Identity material came from the archive instead of escrow, or the trust store was never populated |
| The restored tree lists numeric owners instead of names | The 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.