An untested backup is a belief, not a backup. This lab spends more time on the restore than the backup, because that is the half that fails.
1. Something worth backing up
mkdir -p ~/labs/backup/{source/{config,data,logs},dest,restore} && cd ~/labs/backup
printf 'listen=8080\nworkers=4\n' > source/config/app.conf
printf 'secret=do-not-lose-this\n' > source/config/secrets.env
chmod 600 source/config/secrets.env
for i in $(seq 1 50); do
echo "record $i" > "source/data/record-$i.txt"
done
dd if=/dev/urandom of=source/data/blob.bin bs=1K count=800 status=none
ln -s ../config/app.conf source/data/app.conf.link
echo "noisy" > source/logs/app.log
find source -type f | wc -lNote the 600 file and the symlink. Both are things a careless backup loses.
Verify
2. Archive with tar, preserving what matters
tar -czf dest/backup.tar.gz \
--exclude='logs/*' \
-C source .
ls -lh dest/backup.tar.gz
tar -tzf dest/backup.tar.gz | head -n 8
tar -tzf dest/backup.tar.gz | wc -l-C source . archives the _contents_ of source with relative paths, which is what you want — archiving /home/you/labs/backup/source bakes absolute paths in and restores to the wrong place. --exclude drops the logs. -t lists without extracting, and reading that list before you trust the archive is a thirty-second habit worth having.
Permissions and symlinks survive by default, which is why tar beats zip here.
Verify
3. Restore into an empty tree and verify
tar -xzf dest/backup.tar.gz -C restore
find restore -type f | wc -l
stat -c '%a %n' restore/config/secrets.env
ls -l restore/data/app.conf.linkNow the part people skip. Compare the trees rather than eyeballing them:
diff -r --no-dereference source restore
echo "diff exit=$?"diff -r recurses; --no-dereference compares the symlink itself rather than following it. The only differences should be the excluded logs.
Verify
Verify content by checksum, which catches corruption a size comparison misses:
( cd source && find . -type f -not -path "./logs/*" -exec sha256sum {} + \
| sort -k2 ) > /tmp/src.sums
( cd restore && find . -type f -exec sha256sum {} + | sort -k2 ) > /tmp/dst.sums
diff /tmp/src.sums /tmp/dst.sums && echo "all checksums match"Verify
4. Mirror with rsync instead, and understand the trailing slash
rsync -aAX --delete --exclude 'logs/' source/ dest/mirror/
find dest/mirror -type f | wc -l
stat -c '%a %n' dest/mirror/config/secrets.envThe flags: -a is archive mode (recursive, preserves permissions, times, symlinks, owner and group), -A adds ACLs, -X adds extended attributes. -aAX is the combination that preserves everything an ext4 file can carry.
The trailing slash is the whole trap. source/ copies the _contents_ into the destination. source without the slash copies the _directory itself_, creating dest/mirror/source/. Getting this wrong is how a mirror ends up one level deeper on every run.
--delete makes the destination match the source exactly, including removals. It is also the flag that turns a typo in the source path into data loss — always --dry-run first:
rsync -aAX --delete --exclude 'logs/' --dry-run -v source/ dest/mirror/ | tail -n 5Verify
5. Prove incremental behaviour
echo "changed" >> source/config/app.conf
rsync -aAX --delete --exclude 'logs/' --stats source/ dest/mirror/ \
| grep -E "Number of regular files transferred|Total transferred"One file transferred, not 53. rsync compares size and mtime and only sends what differs — which is why it, not tar, is what you schedule every fifteen minutes.
Then prove --delete really deletes:
rm source/data/record-1.txt
rsync -aAX --delete --exclude 'logs/' source/ dest/mirror/
test -e dest/mirror/data/record-1.txt \
&& echo "STILL THERE" || echo "removed from mirror too"That is the property you want in a mirror and the reason a mirror is not a backup: a deletion propagates, so ransomware or rm -rf propagates too. A mirror protects against hardware failure. Only versioned or offsite copies protect against a mistake.
Verify
6. What a real backup needs beyond this
This lab covered the mechanics. A backup you can rely on also needs:
| Property | Why it matters |
|---|---|
| Offsite copy | A fire, or a compromised account, takes the local copy with it |
| Versioning | Yesterday's good state, when today's backup captured the damage |
| Immutability | Object-lock or append-only, so an attacker cannot delete history |
| Tested restore | On a schedule, into a clean host, timed — that is your RTO |
| Encryption | At rest and in transit, with the key stored somewhere else |
The one to schedule is the restore test. Every organisation that lost data had backups; what they did not have was a restore anyone had performed.
Clean up
cd ~ && rm -rf ~/labs/backup /tmp/src.sums /tmp/dst.sumsWhere this goes next
The remaining labs in this group are the incident-response set: hardened service units, SSH, growing a disk under pressure, log limits, network path debugging, and load triage.