Objective
By the end of this lab you will have three files that most estates do not have: an inventory of what exists, a classification of what each item would cost to lose, and a requirement table that names an RPO, a retention period and one copy production cannot alter, per item.
The estate is fictional and small. The commands are real, and the method transfers unchanged to an estate you cannot hold in your head, because it starts from the filesystem rather than from anyone’s recollection.
Architecture
Seven items, sorted into two classes, feeding three artefacts. The diagram is the shape of the answer you are going to produce.
flowchart LR
subgraph EST["rbdr-estate (supplied)"]
A["rbdr-orders-db"]
B["rbdr-wiki-vol"]
C["rbdr-fw-appliance"]
D["rbdr-git-server"]
E["rbdr-tfstate-bucket"]
F["rbdr-ca"]
G["rbdr-monitoring"]
end
EST --> INV["rbdr-inventory.csv<br/>find, du, stat"]
INV --> CLS["rbdr-classification.csv<br/>rebuild / re-derive / cannot-continue"]
CLS --> REQ["rbdr-requirements.csv<br/>RPO, retention, copy production cannot alter"]
REQ --> CHK["rbdr-check.sh<br/>fails an incomplete row"]
Requirements
- A Linux shell with GNU
find(for-printf), GNUdu(for-sb) and GNUstat(for-c). BSD and macOS variants take different flags. - No privilege. Everything happens under your own home directory.
- Names used: one directory,
rbdr-lab-01, created directly in$HOME. Every file and directory the lab creates is prefixedrbdr-, so Cleanup can assert it removed exactly what it made and nothing else.
Scenario
You have inherited an estate nobody documented. There is a PostgreSQL order database, a container running the internal wiki with its pages on a volume, a firewall appliance, a Git server holding the infrastructure repositories, Terraform state in a bucket, a certificate authority, and a monitoring stack.
The previous administrator applied one policy to all of it: nightly, keep thirty days. That policy is simultaneously too expensive for two of these items and catastrophically insufficient for the other five, and no one can say which is which without doing the work below.
Tasks
Task 1 — Record the pre-lab state
Cleanup is only provable against a recorded starting point. Capture it before anything else exists.
LAB="$HOME/rbdr-lab-01"
test -e "$LAB" && { echo "rbdr-lab-01 already exists; move it aside first"; exit 1; }
mkdir -p "$LAB"
ls -A "$HOME" | grep -v '^rbdr-lab-01$' | sort > "$LAB/rbdr-state.pre-lab"
wc -l < "$LAB/rbdr-state.pre-lab"
The grep -v removes the lab directory itself, so the file describes your home
directory as it was one second before the lab began.
Task 2 — Build the supplied estate
EST="$LAB/rbdr-estate"
mkdir -p "$EST"/rbdr-orders-db/base "$EST"/rbdr-orders-db/pg_wal \
"$EST"/rbdr-wiki-vol/pages "$EST"/rbdr-fw-appliance \
"$EST"/rbdr-git-server/infra.git "$EST"/rbdr-tfstate-bucket \
"$EST"/rbdr-ca/private "$EST"/rbdr-monitoring/tsdb \
"$EST"/rbdr-monitoring/dashboards
head -c 2200000 /dev/urandom > "$EST/rbdr-orders-db/base/orders.dat"
for n in 1 2 3; do
head -c 160000 /dev/urandom > "$EST/rbdr-orders-db/pg_wal/segment-0000000$n"
done
for p in runbook onboarding oncall; do
echo "wiki page $p" > "$EST/rbdr-wiki-vol/pages/$p.md"
done
echo 'pass in on wan proto tcp to port 443' > "$EST/rbdr-fw-appliance/config.xml"
head -c 900000 /dev/urandom > "$EST/rbdr-git-server/infra.git/pack.pack"
echo '{"version":4,"serial":118}' > "$EST/rbdr-tfstate-bucket/terraform.tfstate"
echo 'CA-PRIVATE-KEY-MATERIAL' > "$EST/rbdr-ca/private/ca.key"
echo 'CA-CERTIFICATE' > "$EST/rbdr-ca/ca.crt"
head -c 4400000 /dev/urandom > "$EST/rbdr-monitoring/tsdb/chunk-000001"
echo '{"title":"estate overview"}' > "$EST/rbdr-monitoring/dashboards/estate.json"
Task 3 — Inventory what is actually there
INV="$LAB/rbdr-inventory.csv"
echo 'item,path,files,bytes,newest_write' > "$INV"
for d in "$EST"/rbdr-*; do
item=$(basename "$d")
files=$(find "$d" -type f | wc -l)
bytes=$(du -sb "$d" | cut -f1)
newest=$(find "$d" -type f -printf '%T@ %p\n' | sort -nr | head -n 1 | cut -d' ' -f2-)
when=$(stat -c '%y' "$newest" | cut -d'.' -f1)
echo "$item,$d,$files,$bytes,$when" >> "$INV"
done
column -s, -t < "$INV"
Three facts per item, none of them opinions. find -type f | wc -l is how many
objects a restore has to recreate; du -sb is the apparent size in bytes, which
drives transfer time; find -printf '%T@ %p' sorted numerically finds the
newest file, and stat -c '%y' says when it last changed.
Notice that the monitoring TSDB is the largest item and the CA is one of the smallest. Size and importance are unrelated, and an estate protected by size alone protects exactly the wrong things.
Task 4 — Classify each item
For each item ask the lesson’s question: if this were gone right now, and no copy existed anywhere we control, what would we do? Write the answer and the route that justifies it. Fields must not contain commas.
CLS="$LAB/rbdr-classification.csv"
cat > "$CLS" <<'CSV'
item,class,reconstruction_route,evidence
rbdr-orders-db,cannot-continue,none - accepted writes exist only here,rows acknowledged to customers
rbdr-wiki-vol,cannot-continue,none - pages were typed by staff,the volume holds the only copy
rbdr-fw-appliance,re-derive,rendered from the network repository,appliance is a rendering target
rbdr-git-server,cannot-continue,none - it is the upstream for everything else,other items re-derive from it
rbdr-tfstate-bucket,cannot-continue,none - state maps resources to real ids,import by hand is not a route
rbdr-ca,cannot-continue,none - the private key came from randomness once,every issued certificate depends on it
rbdr-monitoring,re-derive,dashboards live in git and the TSDB refills,history is lost but the service returns
CSV
awk -F, 'NR>1 {print $2}' "$CLS" | sort | uniq -c
Two of the seven are re-derivable and five are not. The firewall appliance is re-derivable only because its configuration is generated from a repository; on an estate where somebody clicked the rules into a web interface, the same appliance is in the third class. Classification is a property of the estate, not of the device.
Task 5 — Derive the requirement table
Now turn the classification into numbers. Each row states a recovery point objective, how long copies are kept, and one copy that production credentials cannot alter.
REQ="$LAB/rbdr-requirements.csv"
cat > "$REQ" <<'CSV'
item,class,rpo,retention,copy_production_cannot_alter
rbdr-orders-db,cannot-continue,5 minutes,7 years,object-locked bucket in a second account
rbdr-wiki-vol,cannot-continue,24 hours,90 days,repository on the backup host with append-only keys
rbdr-fw-appliance,re-derive,7 days,30 days,none needed beyond the config repository
rbdr-git-server,cannot-continue,1 hour,1 year,mirror in a second region plus a weekly offline copy
rbdr-tfstate-bucket,cannot-continue,per apply,1 year,versioned bucket the pipeline cannot delete from
rbdr-ca,cannot-continue,per issuance,life of the longest certificate plus 1 year,offline escrow held by two custodians
rbdr-monitoring,re-derive,24 hours,14 days,none needed beyond the dashboard repository
CSV
column -s, -t < "$REQ"
The RPO column is not a number a product supplies; it is the loss window the business has agreed to accept, and it dictates the schedule rather than following from one. Two rows have an RPO expressed as an event rather than a duration, because state and issued certificates change on demand and not on a clock.
Task 6 — Write the validator, then prove it has teeth
cat > "$LAB/rbdr-check.sh" <<'CHECK'
#!/usr/bin/env bash
set -u
REQ="$1"
rc=0
if [ "$(head -n 1 "$REQ")" != "item,class,rpo,retention,copy_production_cannot_alter" ]; then
echo "FAIL header is not the agreed five columns"; rc=1
fi
bad_cols=$(awk -F, 'NF!=5 {print NR": "$0}' "$REQ")
if [ -n "$bad_cols" ]; then echo "FAIL wrong column count:"; echo "$bad_cols"; rc=1; fi
bad_ret=$(awk -F, 'NR>1 && $2=="cannot-continue" && $4=="" {print NR": "$1}' "$REQ")
if [ -n "$bad_ret" ]; then echo "FAIL cannot-continue row with no retention:"; echo "$bad_ret"; rc=1; fi
[ "$rc" -eq 0 ] && echo "PASS requirement table is complete"
exit "$rc"
CHECK
chmod +x "$LAB/rbdr-check.sh"
"$LAB/rbdr-check.sh" "$REQ"; echo "exit=$?"
# The failing case: blank one retention and confirm the check refuses it.
awk -F, 'BEGIN{OFS=","} $1=="rbdr-ca"{$4=""} {print}' "$REQ" > "$LAB/rbdr-requirements.broken.csv"
"$LAB/rbdr-check.sh" "$LAB/rbdr-requirements.broken.csv"; echo "exit=$?"
A check that has never failed is not evidence of anything. The second run must
print FAIL cannot-continue row with no retention: followed by 7: rbdr-ca,
and exit 1.
Task 7 — Take a copy, lose the original, restore it, and measure the gap
A requirement table is a claim. This task is the smallest possible test of one.
CP="$LAB/rbdr-copy"; mkdir -p "$CP"
tar -C "$EST" -cf "$CP/rbdr-orders-db.tar" rbdr-orders-db
COPY_AT=$(stat -c '%Y' "$CP/rbdr-orders-db.tar")
sha256sum "$EST/rbdr-orders-db/base/orders.dat" > "$CP/rbdr-orders-db.sha256"
sleep 3
echo 'order 90210 accepted' >> "$EST/rbdr-orders-db/base/orders.dat"
WRITE_AT=$(stat -c '%Y' "$EST/rbdr-orders-db/base/orders.dat")
rm -rf "$EST/rbdr-orders-db"
START=$(date +%s%N)
tar -C "$EST" -xf "$CP/rbdr-orders-db.tar"
END=$(date +%s%N)
echo "Actual restore time: $(( (END - START) / 1000000 )) ms"
echo "Actual RPO observed: $(( WRITE_AT - COPY_AT )) s"
sha256sum -c "$CP/rbdr-orders-db.sha256"; echo "checksum exit=$?"
grep -c 'order 90210 accepted' "$EST/rbdr-orders-db/base/orders.dat"
The restore succeeds, the checksum matches, and the order the estate accepted three seconds after the copy was taken is gone. That gap is the RPO, measured rather than asserted, and it is the number Task 5 was trying to bound.
Validation
Run each line and compare against the stated result and exit code.
for f in rbdr-state.pre-lab rbdr-inventory.csv rbdr-classification.csv \
rbdr-requirements.csv rbdr-check.sh; do
test -s "$LAB/$f" && echo "OK $f"
done
awk -F, 'END{print NR}' "$LAB/rbdr-inventory.csv"
awk -F, 'NF!=5' "$LAB/rbdr-requirements.csv"; echo "columns exit=$?"
awk -F, 'NR>1 && $2=="cannot-continue" && $4==""' "$LAB/rbdr-requirements.csv" | wc -l
"$LAB/rbdr-check.sh" "$LAB/rbdr-requirements.csv"; echo "exit=$?"
"$LAB/rbdr-check.sh" "$LAB/rbdr-requirements.broken.csv"; echo "exit=$?"
- The
forloop prints fiveOKlines, one per deliverable. awk END{print NR}on the inventory prints8— a header plus seven items.awk -F, 'NF!=5'on the requirement table prints nothing and reportscolumns exit=0. Any output is a comma inside a field.- The cannot-continue-without-retention count is
0. - The good table prints
PASS requirement table is completeandexit=0. - The broken table prints
FAIL cannot-continue row with no retention:, then7: rbdr-ca, thenexit=1. - Task 7 printed
rbdr-orders-db/base/orders.dat: OKandchecksum exit=0, and thegrep -cprinted0.
Expected Outcome
Five files under rbdr-lab-01, a classification that puts five of seven items
in the class with no reconstruction route, and a requirement table whose every
irreplaceable row names a retention and a copy production cannot alter.
- Actual restore time: ______ ms (Task 7 prints it; a few milliseconds for this 2.6 MB item).
- Actual RPO observed: ______ s (Task 7 prints it;
3if you did not change thesleep).
The second number is the point of the whole lab. It is small here because the copy and the write were seconds apart. In production it is the schedule interval, and it is the same measurement.
Troubleshooting
find: unrecognized: -printf. You are on BusyBox or BSD find. Substitute
stat -c '%Y %n' over the file list, or run the lab on a GNU userland.
du: invalid option -- 'b'. Not GNU du. Use du -sk and multiply by
1024, accepting that the figure becomes disk usage rather than apparent size.
stat: invalid option -- 'c'. BSD stat uses -f. The format strings
differ; %y and %Y are GNU spellings.
column: command not found. column ships in util-linux and is only used
to display the CSV files. Substitute cat — nothing in the lab depends on it.
The check prints FAIL wrong column count on a row you believe is correct.
A field contains a comma. column -s, -t < "$REQ" shows the misalignment
immediately.
The check prints PASS on the broken file. awk rebuilt the record without
your edit — confirm with grep '^rbdr-ca,' "$LAB/rbdr-requirements.broken.csv",
which must end in a comma followed by the copy description.
sha256sum -c reports FAILED. The tar was created after the appended
write rather than before it. Rebuild the estate item and redo Task 7 in order.
grep -c prints 1 in Task 7. Same cause: the copy was taken too late, so
there is no gap to observe and nothing was lost.
Cleanup
PRE_LAB=$(cat "$LAB/rbdr-state.pre-lab")
rm -rf "$LAB"
diff <(printf '%s\n' "$PRE_LAB") <(ls -A "$HOME" | sort) \
&& echo "OK home directory matches its pre-lab state"
The diff must produce no output and the message must print. Any difference
means the lab left something behind or removed something it did not create —
both worth knowing before you trust the pattern on a server.
Production notes
- Run the inventory step against a real estate before proposing any policy. The argument about retention becomes tractable the moment there is a table with a row per item and a named reconstruction route in each one.
- Publish the classification, not just the schedule. The schedule is what the backup team owns; the classification is what the service owner has to agree to, and disagreement surfaces there rather than during a recovery.
- Review the table when the estate changes shape, not on a calendar. An unlisted item is protected by nothing.
- The
copy_production_cannot_altercolumn is deliberately awkward to fill in. If the honest answer is “none”, that is a finding, and it belongs in the table where somebody has to read it.
What You Learned
- An inventory comes from the filesystem, not from memory.
find,duandstatanswer how many objects, how many bytes and how recently written, which are the three inputs every later decision needs. - Size does not predict importance. The largest item here re-derives itself and the smallest one cannot be reconstructed by any means.
- Classification is contextual. The firewall appliance is re-derivable only while its configuration is generated from a repository.
- Retention is a consequence of classification. A row with no reconstruction route and no stated retention is an incomplete requirement, which is exactly what the validator refuses.
- A check you have never seen fail proves nothing, which is why Task 6 feeds it a broken row and requires exit 1.
- The recovery-point gap is measurable in a mock estate, and the measurement is identical in kind to the one that decides whether a customer’s order survived.