backup pipeline: configs, runbooks, NH3 Synology rest-server, cross-site rsync

Bundles the post-2026-04-21 work that built out the two-layer backup
architecture (PBS for VM images + restic for file/DB), plus the cross-
site mirror and the disaster-recovery runbook.

- configs/restic/esh-docker-vm/profiles.yaml: drop the obsolete
  *_offen_backup_data exclude (offen sidecars retired fleet-wide
  2026-04-23; restic now covers the equivalent scope directly).
- configs/restic/esh-vm-db/: new profile for the dedicated DB VM
  (10.0.50.60), with pre-backup pg_dumpall + mongodump hooks.
- configs/rsync/: ana-nas → nh3-nas (04:00 daily, runs as lkraven)
  and nh3-nas → ana-nas (05:00 daily, runs as root because DSM
  rest-server-nh3 writes mode-400 files only root can read).
- docs/runbooks/pbs-deployment.md: 9-phase PBS rollout runbook,
  refined during the 2026-04-22 deployment with per-hypervisor
  namespaces, NFSv3 + ZFS-case-insensitivity workaround, and the
  Synology syno_acl flatten step.
- docs/runbooks/disaster-recovery.md: blast-radius runbook ordered
  Tier 0 → 5 (ana-nas → hypervisors → Docker hosts → VMs → specialty);
  references incident memory + recovery-step playbooks per consumer.
This commit is contained in:
vh
2026-04-24 21:56:22 -07:00
parent 4971e5ad41
commit 574c72daa5
12 changed files with 1114 additions and 20 deletions
+164
View File
@@ -0,0 +1,164 @@
# restic / esh-vm-db
**Two-database host** at the ESH site (PostgreSQL 15 + MongoDB). Covered
at the VM-image layer by PBS-ANA via esh-pve (or whichever ESH
hypervisor owns this VM — confirm on next inventory pass). This restic
profile adds DB-level granularity via pre-backup dumps.
## What's backed up
| Path | Purpose |
|---|---|
| `/etc` | Host config — systemd, ssh, chrony, apt, pg_hba.conf, mongod.conf |
| `/root` | Root's ad-hoc scripts, shell history, ssh keys |
| `/home` | User homes (lkraven + any DB-admin locals) |
| `/var/lib/restic/stage` | **pg_dumpall.sql.gz** + **mongodump/** produced by pre-backup.sh |
## What's **not** backed up (by design)
- **`/var/lib/postgresql`** — raw PGDATA. Live-capture risk;
`pg_dumpall` in pre-backup covers it consistently.
- **`/var/lib/mongodb`** — raw mongo dbPath. Same reasoning;
`mongodump` covers it.
- NFS mount `/mnt/backup` (from esh-nas — not ours to mirror).
## Pre-backup hook
`pre-backup.sh` runs as root before restic. It:
1. Checks `pg_isready` on :5432 — if OK, runs `pg_dumpall` piped
through gzip to `$STAGE/pg_dumpall.sql.gz`
2. Checks mongo ping via `mongosh` — if OK, runs `mongodump` into
`$STAGE/mongodump/`
Both dumps are atomic (write to `.tmp`, then rename). If either DB is
unreachable, the script logs a WARN and continues — a failed DB dump
doesn't abort the whole restic run, and restic falls back to whatever
stage content is left over from the prior successful dump.
## Deploy (one-time)
### 1. Create rest-server-ana htpasswd entry
**Do NOT use `sudo` for .htpasswd writes on ana-docker.** The file is
NFS-mounted from ana-nas and owned by uid 1000 (the rest-server user,
which equals lkraven). Sudo-root on the client gets squashed to
nobody on the NFS server and can't read/write the file. lkraven
writes it natively, using the docker group for the bcrypt helper.
```bash
# Pick password in password manager first
HTPW='<new-pw-saved-to-pw-manager>'
ssh -t ana-docker "docker run --rm httpd:2.4-alpine htpasswd -nbB esh-vm-db '$HTPW' | \
tee /tmp/htline.txt > /dev/null && \
sed -i '/^esh-vm-db:/d' /mnt/backup/restic/repo/ana/.htpasswd && \
cat /tmp/htline.txt >> /mnt/backup/restic/repo/ana/.htpasswd && \
rm /tmp/htline.txt && \
grep ^esh-vm-db: /mnt/backup/restic/repo/ana/.htpasswd && \
docker restart rest-server"
unset HTPW
```
### 2. Install secrets on esh-vm-db
```bash
ssh -t esh-vm-db 'sudo install -d -o root -g root -m 0700 /etc/restic /var/lib/restic /var/lib/restic/stage'
# restic.env — URL-encode the password if it has special chars
ssh -t esh-vm-db "sudo bash -c '
read -sp \"htpasswd pw for rest-server-ana: \" HTPW; echo
cat > /etc/restic/restic.env <<EOF
RESTIC_REPOSITORY=rest:http://esh-vm-db:\$HTPW@10.250.50.70:8000/esh-vm-db/
EOF
chmod 600 /etc/restic/restic.env
'"
# Repo passphrase (prints once — save to password manager)
ssh -t esh-vm-db 'sudo bash -c "
openssl rand -base64 48 | tr -d \"\\n\" > /etc/restic/password
chmod 600 /etc/restic/password
echo === SAVE THIS TO PASSWORD MANAGER NOW ===
cat /etc/restic/password
echo
"'
```
### 3. Initialize the repo
```bash
ssh -t esh-vm-db 'sudo bash -c "
set -a; . /etc/restic/restic.env; set +a
RESTIC_PASSWORD_FILE=/etc/restic/password restic init
"'
```
### 4. Install prerequisites (restic, resticprofile, mongosh client)
```bash
ssh -t esh-vm-db 'which restic || sudo apt-get install -y restic; \
which mongosh || echo "NOTE: mongosh not found; pre-backup mongo ping will fail safely — install via MongoDB APT repo if needed"; \
curl -sfL https://raw.githubusercontent.com/creativeprojects/resticprofile/master/install.sh | sudo sh -s -- -b /usr/local/bin; \
/usr/local/bin/resticprofile --version'
```
### 5. Deploy profile + hook
```bash
scp configs/restic/esh-vm-db/profiles.yaml esh-vm-db:/tmp/
scp configs/restic/esh-vm-db/pre-backup.sh esh-vm-db:/tmp/
ssh -t esh-vm-db 'sudo install -o root -g root -m 0644 /tmp/profiles.yaml /etc/restic/profiles.yaml && \
sudo install -o root -g root -m 0755 /tmp/pre-backup.sh /etc/restic/pre-backup.sh && \
rm /tmp/profiles.yaml /tmp/pre-backup.sh'
```
### 6. Schedule + verify
```bash
ssh -t esh-vm-db 'sudo resticprofile --config /etc/restic/profiles.yaml schedule --all && \
systemctl list-timers "resticprofile*" --no-pager'
# First manual run
ssh -t esh-vm-db 'sudo resticprofile --config /etc/restic/profiles.yaml backup --verbose'
```
Expect first run to land ~50-200 MB (mostly the mongodump directory + pg_dumpall).
Cross-check from Backrest UI on ana-docker.
## Restore
### Full host config
```bash
ssh -t esh-vm-db 'sudo resticprofile --config /etc/restic/profiles.yaml restore latest --target /tmp/restore --path /etc'
```
### Just the PG dump
```bash
ssh -t esh-vm-db 'sudo resticprofile --config /etc/restic/profiles.yaml restore latest --target /tmp/restore --path /var/lib/restic/stage/pg_dumpall.sql.gz'
# Then: gunzip + psql < pg_dumpall.sql
```
### Just a mongo DB
```bash
ssh -t esh-vm-db 'sudo resticprofile --config /etc/restic/profiles.yaml restore latest --target /tmp/restore --path /var/lib/restic/stage/mongodump'
# Then: mongorestore /tmp/restore/var/lib/restic/stage/mongodump/
```
## Gotchas
- **mongosh must be installed** or the mongo pre-backup step silently
skips (logged as WARN). Install from the MongoDB APT repo if not
already present — the stock Debian `mongodb-clients` package is
out of date and doesn't include `mongosh`.
- **Mongo authentication** — if mongod ever gets auth enabled (it's
currently open to 0.0.0.0 with no auth, which is its own concern),
`mongodump` will need `--username/--password` flags. Reference in
pre-backup.sh when that change happens.
- **pg_hba.conf** — `pg_dumpall` requires local postgres superuser
access. Currently works via `sudo -u postgres` + peer auth on the
local socket. If `pg_hba.conf` ever changes peer → md5 for local,
the hook needs a `~postgres/.pgpass` entry.
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# pre-backup.sh — esh-vm-db.
# Runs as root from resticprofile's `run-before`.
#
# Produces consistent DB dumps in /var/lib/restic/stage/. Two DBs here:
# - Postgres 15 (port 5432, local) — pg_dumpall all databases
# - MongoDB (port 27017, local) — mongodump all databases
#
# Peer-data DBs (paperless-ng on Postgres) are the primary consumers;
# raw volume capture isn't in the restic source list so these dumps
# are the ONLY way restic sees DB data.
#
# Errors in individual blocks log a WARN; whole script doesn't abort.
set -euo pipefail
STAGE=/var/lib/restic/stage
install -d -o root -g root -m 0700 "$STAGE"
log() { printf '%s pre-backup(esh-vm-db): %s\n' "$(date -Is)" "$*"; }
warn() { printf '%s pre-backup(esh-vm-db): WARN: %s\n' "$(date -Is)" "$*" >&2; }
# ---- Postgres ----------------------------------------------------------
PG_DUMP="$STAGE/pg_dumpall.sql.gz"
if sudo -u postgres pg_isready -h localhost -p 5432 > /dev/null 2>&1; then
log "pg_dumpall starting → $PG_DUMP"
if sudo -u postgres pg_dumpall -h localhost -p 5432 | gzip > "$PG_DUMP.tmp"; then
mv "$PG_DUMP.tmp" "$PG_DUMP"
log "pg_dumpall done ($(du -h "$PG_DUMP" | cut -f1))"
else
warn "pg_dumpall failed (exit $?); keeping previous dump if any"
rm -f "$PG_DUMP.tmp"
fi
else
warn "postgres not ready on :5432 — skipping pg_dumpall"
fi
# ---- MongoDB -----------------------------------------------------------
MONGO_DIR="$STAGE/mongodump"
if mongosh --quiet --eval 'db.adminCommand({ping: 1}).ok' | grep -q '^1$'; then
log "mongodump starting → $MONGO_DIR"
rm -rf "$MONGO_DIR.tmp"
if mongodump --out "$MONGO_DIR.tmp" > /dev/null 2>&1; then
rm -rf "$MONGO_DIR"
mv "$MONGO_DIR.tmp" "$MONGO_DIR"
log "mongodump done ($(du -sh "$MONGO_DIR" | cut -f1))"
else
warn "mongodump failed (exit $?); keeping previous dump if any"
rm -rf "$MONGO_DIR.tmp"
fi
else
warn "mongo not reachable via mongosh — skipping mongodump"
fi
# ---- Retention on stage dir --------------------------------------------
# restic dedupes identical dumps at the chunk level, so we can safely keep
# overwriting the same files. No explicit rotation needed here.
log "pre-backup complete"
+89
View File
@@ -0,0 +1,89 @@
# resticprofile config for esh-vm-db.
#
# Two-database host (Postgres 15 on :5432, MongoDB on :27017). Small
# VM on an ESH hypervisor; backed up at VM-image level by PBS-ANA via
# its hypervisor. This restic profile captures:
#
# 1. Host config (/etc, /root) — fast config recovery without
# waiting for VM-image restore
# 2. Per-DB dumps via pre-backup hook — pg_dumpall + mongodump,
# written to /var/lib/restic/stage/ and included in the restic
# snapshot. Gives us DB-level restore granularity alongside the
# VM-image restore from PBS.
#
# What's DELIBERATELY NOT in source:
# - /var/lib/postgresql — raw live PGDATA. Inconsistent if captured
# while postgres is running; pg_dumpall above covers this.
# - /var/lib/mongodb — same reasoning; mongodump covers it.
#
# Writes cross-site to rest-server-ana (10.250.50.70:8000). No local
# NH3-style rest-server on the ESH side; esh-docker-vm and vm-esh-nas
# also use rest-server-ana, so this follows fleet pattern.
version: "1"
global:
priority: low
ionice: true
ionice-class: 2
ionice-level: 7
min-memory: 100
default:
env-file: /etc/restic/restic.env
env:
RESTIC_PASSWORD_FILE: /etc/restic/password
initialize: false
lock: /var/lock/restic-esh-vm-db.lock
backup:
verbose: 1
run-before:
- /etc/restic/pre-backup.sh
run-after:
- date +%s > /var/lib/restic/last-success
source:
- /etc # host config
- /root # root's scripts, ssh keys, shell history
- /home # user home dirs
- /var/lib/restic/stage # pg_dumpall + mongodump outputs from pre-backup
exclude:
# Raw DB data is captured via pre-backup dumps, not volume-level
- /var/lib/postgresql
- /var/lib/mongodb
# NFS mount from esh-nas (backup target for other stacks — not ours to mirror)
- /mnt/backup
# Ephemeral / regenerable
- "**/*.log"
- "**/*.log.*"
- "**/*.pid"
- /root/.cache
- /root/.local/share/Trash
- /root/.npm
- /root/.python_history
- /home/*/.cache
- /home/*/.local/share/Trash
- /home/*/.npm
tag:
- host:esh-vm-db
- site:esh
- fleet:home-lab
schedule: "*-*-* 01:00:00"
schedule-permission: system
schedule-log: /var/log/restic-backup.log
forget:
keep-daily: 7
keep-weekly: 4
keep-monthly: 12
keep-yearly: 3
tag:
- host:esh-vm-db
# Schedule removed: forget against --append-only rest-server always
# fails (delete ops blocked). Run manually during prune ceremony.
check:
read-data-subset: 10%
schedule: "Sun *-*-* 05:00:00"
schedule-permission: system
schedule-log: /var/log/restic-check.log