For: Byron · Machine: Mia (Mac Mini) · Targets: NAS + immutable cloud + air-gapped drive · Threat model: ransomware (encryption and exfiltration/extortion), runaway process, disk failure, fire/theft
Where you are. A real backup system already exists (~/fleet-backup/): encrypted snapshots of every agent's config + workspace + crons, the OB1 Postgres dump, Docker image digest pins, secrets sealed in secrets.enc, 3-deep rotation, and a 15KB DR runbook. Good work — but it writes only to Mia's own disk, so any disk failure, theft, or ransomware destroys the backups with the machine. Each snapshot is only ~1.6 GB (the irreplaceable set is small; the 160 GB is re-downloadable Ollama models), which makes a much stronger posture cheap.
The proposal, and what a hard review changed. A 3-2-1-1-0 design — but built on three corrected principles the first draft got wrong:
Decision sought. Approve the approach; grant a least-privilege NAS repo path and a B2/S3 bucket with Object Lock. Then I migrate the existing local backup into an offsite, immutable, independently-verified system — reusing the runbook and encryption already built.
Open the target-state architecture diagram full-size, or jump to the hardened console mockup.
| Tier | Contents | Size | Policy |
|---|---|---|---|
| 1 · Irreplaceable | Docker volumes (agent sessions/credentials/data/agent-state), OB1 Postgres dump, openclaw-gia/config (memory, skills, project source, data, .env), ~/.claude, gitea repos+config, ~/.ssh, all keys/tokens, LaunchDaemons/Agents plists, ~/.zprofile, Homebrew Brewfile | ~1.6 GB (dedup ⇒ small increments once capture is consistent — see §5) | Hourly, encrypted → NAS and cloud-immutable. |
| 2 · Re-derivable, slow | Avatar model envs (~9 GB), jlens 27B model, Python venvs | ~15 GB | Cold: snapshot once; documented re-fetch is the real recovery path. |
| 3 · Re-downloadable | ~/.ollama (160 GB), Homebrew binaries, node_modules, Docker images (digest-pinned) | ~180 GB | Never backed up — restore = re-run installers, ollama pull, docker pull @digest. |
Routine backup moves single-digit GB, which is what makes hourly + offsite + immutable affordable.
Hourly file-copies of live databases produce corrupt restores. So capture is quiesced, not naive:
tmutil localsnapshot) → back up from the frozen snapshot, so every file is crash-consistent as of one instant.pg_dump inside the container is the source of truth; the live PGDATA volume is EXCLUDED from the file backup (a live copy is torn).~/.claude, WAL-mode): captured via sqlite3 .backup / WAL checkpoint, never a raw file copy of an open DB.limactl shell colima docker run -v … tar), since they live in the Lima disk image, not on macOS — with the agents' writers briefly paused for consistency.gitea dump (not a live data-dir copy).~/.ssh/~/.claude. Sleep is handled with caffeinate/pmset wake windows so scheduled runs actually fire.restic check --read-data-subset=10% (⇒ all data deep-verified every ~10 weeks) run from the NAS, plus a real scratch-Postgres restore test.--append-only on the NAS and Object Lock in the cloud — a compromised client can add but cannot rewrite/delete. (Borg is not offered — its append-only defers deletes into a log a later prune executes: a footgun here.)restic check, the canary evaluation, and prune run off Mia. Alerting uses an external dead-man's switch (e.g. healthchecks.io): the backup pings it on success; if the ping is missing — the exact thing a compromised, silent Mia produces — the switch alerts you. Mia is never trusted to report its own health.A · Continuous, zero production risk (run from the NAS/independent domain): restic check; restore the OB1 dump into a throwaway Postgres and run a real query (not just pg_restore --list, which a truncated dump passes); sqlite3 integrity_check on restored SQLite; decrypt-test secrets.enc. Proves restorability, touches nothing in production.
B · Deep rehearsal on isolated hardware — under three iron rules that make a live clone impossible:
Prefer a bare-metal second Mac Mini (also tests the Homebrew/TCC/Colima bootstrap a nested VM skips). A rented cloud Mac means shipping every secret to third-party hardware — acceptable only with the neutered-credential rule, and noted that it then doesn't test the real secret chain. Pick explicitly.
> ⚠️ Cardinal rule (red in the runbook): never boot a restored clone on the live network with live credentials. Two agents sharing one identity = double-processing, WhatsApp session theft, duplicate deploys.
If Mia is compromised, assume the repo was read. The runbook includes a credential inventory and rotation order: gog Google OAuth (Gmail/Calendar/Contacts) · gitea token · Vercel token · GoDaddy API key/secret · Anthropic · Gemini · xAI · LinkedIn access token · OWNER_KEYs (branding/city) · OB1 Postgres URL/password · Tailscale auth/machine keys · all ~/.ssh keys (regenerate + re-authorize) · the restic repo key (rotate + re-encrypt). Each with where it's used and how to rotate. A backup you can restore into a world where the stolen keys still work is only half a recovery.
restore latest. Ransomware persistence (workspace source, plists, crons) lives in Tier 1. Identify last-known-good before the incident using the independent timeline; forensic pause; scan restored content before executing anything.secrets.enc key. Missing one deadlocks the recovery.brew bundle (Brewfile) → Colima (saved profile) → grant TCC/FDA.restic restore <good-snap> → config, ~/.claude, gitea, ~/.ssh, keys, plists.docker run -v … tar -x), not dropped in as macOS files; OB1 restored from dump (mind Postgres major-version match); gitea restored.docker save of pinned images is kept in Tier 2 to break the circular dependency.RTO honest estimate: a few hours to a working fleet, which the first rehearsal will correct — that's what rehearsals are for.
→ See the static mockup of the console (sample data; the real one ships in Phase 3, tailnet-gated).
Entire console is Tailscale-gated — not just a "detail view." A public page showing retention depth, canary state, and air-gap-drive-last-connected is a targeting package for an attacker (tells them how long to dwell and when your offline copy is stalest); it never ships publicly. Read-only, no mutating endpoints, no secrets rendered, minimal publish pipeline (to shrink the Vercel-token blast radius). It shows: last backup per tier · size · retention ladder · last independent restic check + scratch-restore result (one 0-errors light) · canary status · cloud Object-Lock + air-gap freshness · RPO/RTO · restore-runbook link. It reports; it cannot act.
| Phase | Scope | Effort |
|---|---|---|
| 0 | NAS rest-server append-only repo (confirm this Synology supports it + Object-Lock-equivalent immutable snapshots) · B2/S3 Object Lock bucket · consistent-capture harness (APFS snapshot + quiescing) · throughput/quota math | 1 day |
| 1 | Offsite the essentials: Tier-1 hourly → NAS and cloud-immutable; retention; keep local as the fast tier | 1 day |
| 2 | Hardening: append-only, Object Lock, NAS lockdown, canary, least-priv account, external dead-man's switch | 1–2 days |
| 3 | Independent validation (off-Mia check + scratch-Postgres restore) · credential-rotation runbook · tailnet-only console | 2 days |
| 4 | Cold + air-gap (Tier-2 once; monthly A/B USB rotation) · first deep rehearsal on isolated hardware | 1 day + rehearsal |
secrets.enc key live only in your password manager (I generate; you store; never plaintext on Mia).Draft 1 was run through Claude Fable 5 as a hostile senior-SRE review. It found real, load-bearing errors — all fixed above. The disposition:
restic check, and alerting were all Mia-side — theater if Mia is owned. v2 moves verification/prune/canary off Mia and adds an external dead-man's switch so a silent compromised host trips the alarm (§4.6).pg_dump with live PGDATA excluded, sqlite3 .backup, tar-from-VM for volumes.--append-only + Object Lock; Borg dropped; Phase 0 rewritten.restore latest restores the ransomware; no snapshot-selection, no offline bootstrap-credential chain." Fixed in §7 (last-known-good selection, forensic pause, the offline credential chain spelled out, correct volume-restore, digest-pin circular-dependency broken with a mirror).pg_restore --list only reads the TOC." Fixed: continuous validation now restores into a scratch Postgres and runs a query.Fable's verdict on v1 was "not credible yet — fix 1–7." v2 addresses all seven. Next step on your word: Phase 0 (needs the NAS repo path + a B2/S3 Object Lock bucket), after which I migrate the existing local backup offsite and immutable, then build the tailnet-gated console.