From 9e442586af64610b68d2eceb80437c0002df6463 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Mon, 29 Jun 2026 11:43:23 +0200 Subject: [PATCH] docs(wiki): settle on-site encrypted backup + disaster-recovery design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New concept page backup-recovery.md resolving the design half of open-question #5. Driving scenario: a stolen/destroyed PC whose LUKS+TPM disk is unrecoverable by design — recovery stands up a NEW PC, restores a backup, and keeps signing the SAME chain. Settled: admin-driven encrypted full-DB backup (SQLite online-backup/VACUUM INTO, snapshots included) to local/USB, SMB/NFS, or SFTP targets; manual button + an in-process daily timer; keep-last-N + dailies retention; restore is admin-only / out-of-band (operator-adversary surface). A restored copy must still verifyChain. Key custody (the load-bearing decision, bears on #6): three independent keys — EVENT_SIGNING_KEY kept an extractable, escrowed software key DECOUPLED from the TPM so the ledger survives total hardware loss (the conscious trade: a TPM-sealed signing key would be unforgeable but permanently unverifiable after the machine dies); a NEW dedicated park_buzi_backup_key in Komodo for backup encryption, separate from the signing key; the LUKS/TPM disk key, appliance-only and deliberately non-recoverable. Keys are never inside the backup they unlock. Updated open-questions #5 (design SETTLED) + #10 note; disk-os-hardening deploy runbook (why the signing key is not sealed + park_buzi_backup_key); index catalog + concept count. Design only — not yet built. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- wiki/concepts/backup-recovery.md | 128 +++++++++++++++++++++++++++++ wiki/concepts/disk-os-hardening.md | 10 ++- wiki/decisions/open-questions.md | 24 ++++-- wiki/index.md | 3 +- wiki/log.md | 20 +++++ 5 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 wiki/concepts/backup-recovery.md diff --git a/wiki/concepts/backup-recovery.md b/wiki/concepts/backup-recovery.md new file mode 100644 index 0000000..df69431 --- /dev/null +++ b/wiki/concepts/backup-recovery.md @@ -0,0 +1,128 @@ +--- +type: concept +tags: [parking, durability, backup, recovery, security, crypto] +sources: [] +updated: 2026-06-29 +--- + +# Backup & Disaster Recovery + +The appliance's [[sqlite]] DB **is** the signed [[append-only-event-chain]] — the whole +revenue/audit history. A disk failure or a stolen/destroyed PC currently means **total +loss** (this is [[open-questions]] #5). This page is the settled design for an on-site, +admin-driven backup that survives **total hardware loss** and restores to a fresh appliance +with the signed chain still verifying. (Designed 2026-06-29.) + +## The recovery scenario it must satisfy + +The driving scenario (the one that forces every decision below): **the PC is gone** — stolen +or destroyed. Its SSD is LUKS-encrypted and **TPM-sealed**, so the disk is unrecoverable *by +design* (a stolen disk won't unlock off its own TPM — see [[disk-os-hardening]], [[tpm]]). We do +**not** want the dead disk; we want to stand up a **new PC**, restore the backup, and continue +signing the **same** chain. For that to work, recovery must depend on **(a)** the backup file and +**(b) two keys held out-of-band** — never on the dead machine. + +## Key custody — the load-bearing decision + +This is the part the whole plan rests on, and it interacts with the secure-element question +([[open-questions]] #6). Three **independent** keys, three custodians: + +| Key | Lives | Recoverable after PC loss? | Job | +| --- | --- | --- | --- | +| **`EVENT_SIGNING_KEY`** | [[fleet-deployment-komodo\|Komodo]] secret (`park_buzi_event_signing_key`), escrowed offsite | **Yes — by design** | Signs + verifies the ledger chain | +| **`park_buzi_backup_key`** *(new)* | Komodo secret, escrowed offsite, **separate** from the signing key | **Yes** | Encrypts/decrypts the backup file | +| **LUKS / TPM disk key** | The appliance's TPM only | **No — deliberately** | At-rest protection of the powered-off SSD | + +- **The signing key is decoupled from the TPM** — kept an *extractable software HMAC secret* + ([[append-only-event-chain]], `signer.ts`), held in Komodo and escrowed by the operator. This is a + **conscious trade**: a truly non-extractable TPM-sealed signing key (the #6 upgrade) would make the + ledger unforgeable even against a host-root attacker — but it would also make the **old ledger + permanently unverifiable after total hardware loss** (the sealed key dies with the machine; + `buildVerifier(keyId)` would return `undefined` forever). You cannot have *both* "key can never be + extracted" *and* "I can rescue the key after the machine dies" — they are the same property from two + sides. Against the [[threat-model|primary adversary]] (the **booth operator**, who has a UI login, not + host root) an escrowed software key is already tamper-evident, so the recoverable design is chosen + **today**; revisiting #6 means re-accepting the unverifiable-after-loss cost. See [[tpm]] "TPM vs. + ATECC608", [[fleet-deployment-komodo]] (the "EVENT_SIGNING_KEY-in-Core is a fraud-root blast radius" + caveat is the same trade). + +- **Backup key is separate from the signing key** even though Komodo holds both — so they can be + managed independently. Rationale: (1) the **signing key must almost never rotate** (every rotation + fractures the chain into a new `keyId` segment — old events stay pinned to the old key forever), + whereas the **backup key may want routine rotation** (a USB went home, a target was decommissioned); + coupling them drags the cheap op into the expensive one. (2) The backup key **travels to every backup + destination** (USB, NAS, SFTP); the signing key should travel *nowhere* but Komodo → process memory — + sharing one key means every backup target conceptually exposes the signing key. (3) Keeping them + separate keeps the **#6 TPM-migration door open** without re-wiring backups. Decided 2026-06-29 + (the "one fewer secret to escrow" simplicity of a shared key is real, but weakest here because Komodo + already holds both). + +> **The keys are never inside the backup they unlock.** A key can't decrypt the file it's locked in. +> Recovery = backup file **+** both escrowed keys, supplied out-of-band. The runbook must say this +> plainly so nobody "helpfully" stores the keys next to the backups. + +## What a backup contains + +**Full SQLite DB, snapshots included** — one self-contained, restore-to-identical-appliance file +(ledger + sessions + config + subscriptions + the [[entry-exit-points|snapshot]] BLOBs). Chosen for +completeness over size. + +> **Size caveat (interacts with [[open-questions]] #10).** Snapshot BLOBs **dominate** DB size and +> bloat *every* backup. They are unsigned, advisory, and already disk-pressure-pruned +> ([[entry-exit-points]]). A future **"exclude snapshots" toggle** (ledger/sessions/config only — much +> smaller, signed chain still fully preserved) is the obvious knob if backup size becomes a problem; the +> default is the complete picture. + +The backup is produced via SQLite **online-backup / `VACUUM INTO`** (a consistent snapshot of the +live WAL-mode DB — **never a raw file copy**, which can capture a torn WAL), then encrypted with +`park_buzi_backup_key`. **Acceptance test:** a restored copy must still pass `verifyChain` — the +signed chain is the thing being protected, so an unverifiable restore is a failed backup. + +## Triggers + +- **Manual** — an admin-only **"Back up now"** button runs immediately to the configured target. +- **Periodic** — an **in-process daily timer** (same pattern as the snapshot-retention prune, + [[entry-exit-points]] / `snapshot-retention.ts`): runs only if the configured target is + reachable/mounted; surfaces last-success / last-error in the UI. No OS cron — it lives inside the + Fastify process, works inside the [[container-deployment|Docker container]], and is configured in + one place. ([[offline-first]]: the periodic path must tolerate a missing/unmounted target without + failing the app.) + +## Destinations (admin-configurable) + +All three supported in the first cut; the manual button and the periodic timer share them: + +- **Local / USB / SATA disk** — a mounted path on an attached disk. Simplest, fully offline, matches + the air-gapped appliance. The strong first target. +- **Network drive (SMB/NFS)** — a mounted share on the isolated LAN (a site NAS). Still + local-network, no internet ([[network-isolation]]). +- **SFTP** — push to an SFTP endpoint, useful for an offsite copy. **FTP is excluded** (plaintext + credentials + data); SFTP is the safe equivalent. + +## Retention at the destination + +**Keep last N + thinned dailies** (e.g. last 7 daily / last 4 weekly) — bounded disk use, and it +survives the "a bad/partial run clobbered the only good copy" failure. (A single rolling +overwrite-latest file was rejected for exactly that reason.) + +## Threat-model fit — restore is the dangerous half + +Writing a backup is benign; **restore is operator-adversary surface** ([[threat-model]]). A restored +DB *replaces* the live signed chain — so a malicious restore is a way to swap in a doctored history. +Therefore: + +- **Restore is NOT a booth button.** It is an **admin-only, out-of-band runbook action** (new + appliance, deliberate provisioning step), not something reachable from the operator console. +- The backup **target configuration** and the **"Back up now"** action are admin-gated. +- Backups do **not** weaken the chain's tamper-evidence: a restored chain is re-verified with the + escrowed `EVENT_SIGNING_KEY`; a tampered restore fails `verifyChain` just as a tampered live DB + would. The backup is a **durability** control, not an integrity one — integrity stays with the + signed chain + [[reconciliation]]. + +## Status + +Design settled 2026-06-29; **not yet built**. Resolves the *design* half of [[open-questions]] #5 +(implementation pending), and records the key-custody stance that bears on #6 (signing stays +decoupled from the TPM) and #10 (snapshots bloat backups → future exclude toggle). See +[[append-only-event-chain]], [[disk-os-hardening]], [[tpm]], [[fleet-deployment-komodo]], +[[reconciliation]]. diff --git a/wiki/concepts/disk-os-hardening.md b/wiki/concepts/disk-os-hardening.md index 4c75c81..046462a 100644 --- a/wiki/concepts/disk-os-hardening.md +++ b/wiki/concepts/disk-os-hardening.md @@ -2,7 +2,7 @@ type: concept tags: [parking, security, platform] sources: [parking-system-architecture] -updated: 2026-06-21 +updated: 2026-06-29 --- # Disk / OS Hardening @@ -44,7 +44,13 @@ Env in `apps/server/.env` on the appliance (see `apps/server/.env.example`). The - **`JWT_SECRET`** — ≥32 random chars; the server refuses to boot without a strong one (no insecure default). `openssl rand -hex 32`. See [[local-jwt-auth]]. - **`EVENT_SIGNING_KEY`** — dedicated HMAC key for the signed ledger; ≥16 chars. Falls back to - `JWT_SECRET` with a warning if unset — set a dedicated one before production. + `JWT_SECRET` with a warning if unset — set a dedicated one before production. Deliberately kept an + **extractable, escrowed software key (NOT TPM-sealed)** so the ledger survives total hardware loss — + see [[backup-recovery]] for the custody trade vs. [[open-questions]] #6. +- **`park_buzi_backup_key`** *(planned)* — dedicated key for encrypting [[backup-recovery|DB backups]], + **separate** from `EVENT_SIGNING_KEY` (independent rotation; backups travel, the signing key + shouldn't). Both escrowed offsite in [[fleet-deployment-komodo|Komodo]]; recovery needs both, held + out-of-band. - **`COOKIE_SECURE=0`** — **REQUIRED on the plain-HTTP LAN appliance.** Auth/CSRF cookies are `Secure` by **default** (fail-safe). The appliance serves the SPA same-origin over **plain http** on the booth LAN, where a `Secure` cookie is **never sent** — so without this opt-out diff --git a/wiki/decisions/open-questions.md b/wiki/decisions/open-questions.md index b62d87d..757343a 100644 --- a/wiki/decisions/open-questions.md +++ b/wiki/decisions/open-questions.md @@ -2,7 +2,7 @@ type: decision tags: [parking, decisions, open] sources: [parking-system-architecture] -updated: 2026-06-21 +updated: 2026-06-29 status: open --- @@ -23,13 +23,18 @@ procurement. (See [[parking-system-architecture]] §10.) 4. **Reconciliation channel.** Even if "offline," establish *some* periodic path (USB, hotspot, manager visit) to reconcile the signed log against an external authority — the real anti-fraud control. See [[reconciliation]]. -5. **Durability / backup.** Backup strategy for the [[sqlite]] database + recovery plan; "sync - later" currently leaves a disk failure as **total revenue-history loss**. _(Confirmed in-scope - to design, 2026-06-15.)_ Because the DB is the signed [[append-only-event-chain]], a backup must - preserve the chain intact (a restored copy must still `verifyChain`); options include SQLite - WAL/online-backup snapshots to a second disk/USB + the periodic external export that doubles as - the [[reconciliation]] channel (#4). Encryption at rest already applies ([[disk-os-hardening]]). - Design TBD. +5. **Durability / backup.** _(Design SETTLED 2026-06-29 — see [[backup-recovery]]; implementation + pending.)_ A disk failure / stolen-or-destroyed PC currently leaves **total revenue-history loss**. + Settled design: an **admin-driven encrypted full-DB backup** (online-backup/`VACUUM INTO`, snapshots + included) to a **local/USB · SMB/NFS · SFTP** target, **manual button + in-process daily timer**, + **keep-last-N + dailies** retention, encrypted with a **dedicated `park_buzi_backup_key`** (separate + Komodo secret, *not* the signing key). Recovery = backup file **+** the two escrowed keys held + out-of-band; a restored copy must still `verifyChain`. **Key-custody stance:** `EVENT_SIGNING_KEY` + stays **decoupled from the TPM** (an extractable, escrowed software key) precisely so it survives + total hardware loss — the conscious trade against #6 (a TPM-sealed signing key would be unforgeable + but **unverifiable after the machine dies**). **Restore is admin-only/out-of-band** (operator-adversary + surface — [[threat-model]]). See [[backup-recovery]], [[fleet-deployment-komodo]], [[disk-os-hardening]], + [[reconciliation]] (#4). 6. **Secure-element integration.** Confirm [[atecc608]] wiring/usage on the host (event signing). The [[esp32-custom-controller]] command-authentication use is **deferred — not being implemented for now** (access control is the [[dingtian-relay]] behind @@ -62,6 +67,9 @@ procurement. (See [[parking-system-architecture]] §10.) reclaim deleted-blob pages without `VACUUM`. **Undecided:** pruning policy (age-based vs. total-size cap), VACUUM cadence, and how this interacts with the #5 backup strategy (blobs bloat every backup). Until decided, snapshots accumulate unbounded. See [[entry-exit-points]]. + _(Update 2026-06-29: pruning is now disk-pressure based — see [[entry-exit-points]]; and the + settled #5 backup includes snapshot BLOBs by default, with a noted future "exclude snapshots" + toggle since they dominate backup size — see [[backup-recovery]].)_ 11. **Appliance OS image → WebKitGTK version (Tauri dependency).** _(Raised by [[desktop-shell-tauri]], 2026-06-21; narrowed same day.)_ The chosen [[desktop-shell-tauri|Tauri v2 desktop shell]] renders through the **host's WebKitGTK**, not a diff --git a/wiki/index.md b/wiki/index.md index 8e80299..83664ad 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -7,7 +7,7 @@ updated: 2026-06-21 # Index Content catalog for the wiki. Start at [[overview]]. Maintained on every ingest. -Counts: 4 sources · 19 entities · 45 concepts · 7 decision records. +Counts: 4 sources · 19 entities · 46 concepts · 7 decision records. ## Overview & navigation - [[overview]] — the top-level synthesis and entry point. @@ -56,6 +56,7 @@ Counts: 4 sources · 19 entities · 45 concepts · 7 decision records. - [[append-only-event-chain]] — append-only + hash chain + ATECC608 signing = unforgeable log. - [[reconciliation]] — the real anti-fraud control; what remote sync actually is. - [[disk-os-hardening]] — LUKS/GRUB/Secure Boot; worthwhile but not the main event. +- [[backup-recovery]] — admin-driven encrypted full-DB backup (local/SMB/SFTP) + DR; signing key escrowed & decoupled from TPM so the ledger survives total hardware loss; restore is admin-only. ## Concepts — device architecture & safety - [[device-adapter-pattern]] — business logic talks to interfaces; swap hardware → new adapter. diff --git a/wiki/log.md b/wiki/log.md index 88e895b..6acda89 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1901,3 +1901,23 @@ verified-on-hardware protocol facts (cjihao serial, `.jsp` path, `Connection: cl `gee-reader-serial-binding`→`dingtian-reader-serial-binding`. The only surviving "GEE" mentions are deliberate naming-correction notes + the raw PDF filename. Behaviour unchanged — naming + the persisted id only. build/lint/test green. + +## [2026-06-29] design | On-site encrypted backup + disaster recovery (resolves open-question #5 design) +New concept page [[backup-recovery]]. Driving scenario: the PC is stolen/destroyed and its LUKS+TPM +SSD is unrecoverable by design — recovery must stand up a NEW PC, restore a backup, and keep signing +the SAME chain. Settled: admin-driven **encrypted full-DB backup** (SQLite online-backup/`VACUUM INTO`, +snapshots INCLUDED) to **local/USB · SMB/NFS · SFTP** targets; **manual button + in-process daily timer** +(same pattern as snapshot prune); **keep-last-N + dailies** retention; restore is **admin-only/out-of-band** +(operator-adversary surface). Restored copy must still `verifyChain`. + +KEY-CUSTODY decision (the load-bearing part, bears on #6): three independent keys — (1) `EVENT_SIGNING_KEY` +kept an **extractable, escrowed software key DECOUPLED from the TPM** so the ledger survives total hardware +loss [conscious trade: a TPM-sealed signing key would be unforgeable but PERMANENTLY UNVERIFIABLE after the +machine dies — same property from two sides]; (2) **new dedicated `park_buzi_backup_key`** in Komodo for +backup encryption, SEPARATE from the signing key (independent rotation; backups travel, signing key +shouldn't; keeps the #6 TPM door open); (3) LUKS/TPM disk key, appliance-only, deliberately non-recoverable. +Keys are NEVER inside the backup they unlock — recovery = backup file + both escrowed keys, out-of-band. + +Updated: [[open-questions]] #5 (design SETTLED) + #10 note (backup includes snapshot BLOBs by default, future +exclude toggle); [[disk-os-hardening]] deploy env runbook (EVENT_SIGNING_KEY-not-sealed rationale + +`park_buzi_backup_key`); index catalog + concept count 45→46. Design only — NOT yet built.