hub v0.12.0: retire Infra Backup, purge its plaintext secrets, fix backup-deadline email
Phase-1 of SPIKE-infra-backup-2026-06-15. The infra-backup mechanism was dead since slice 8C yet stored plaintext customer secrets at rest (app-secret key, restic password, Cloudflare tokens) — a zero-knowledge violation — and its absence made the daily expected_backup_missed email fire for healthy customers. - Repoint monitor.CheckBackupDeadlines backup half to the agent host-report's PBS snapshots (+vzdump): alarm only on no-backup / >26h stale / verify failed. Keep the db_dump half. No host-report → no backup alarm (liveness owns that). New store.GetLatestHostReportJSON. Tests incl. a companion that fails pre-fix. - Remove the infra-backup endpoints, store methods/types, and operator panel; /recovery now returns config_yaml only. - migrate(): DROP infra_backup_versions/infra_backups + VACUUM (+wal_checkpoint) to physically reclaim the plaintext pages, gated on table existence. Flagged out-of-scope: exposed creds need operator rotation; legacy reports table holds historical plaintext restic_password rows (separate leak, not purged here). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,48 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.12.0 — retire Infra Backup + purge its plaintext secrets + fix the daily backup-deadline email (2026-06-16)
|
||||
|
||||
Phase-1 of the Infra Backup retirement (per `documentation/audits/SPIKE-infra-backup-2026-06-15.md`).
|
||||
The mechanism had been dead since slice 8C, yet the hub still stored each version as a **plaintext
|
||||
JSON blob at rest** containing the customer's app-secret encryption key, restic password, and
|
||||
Cloudflare tokens — a zero-knowledge violation. Its absence was also the root cause of the daily
|
||||
`expected_backup_missed` false-alarm email.
|
||||
|
||||
### Changed
|
||||
- **Backup-deadline check repointed to PBS freshness.** `monitor.CheckBackupDeadlines` no longer
|
||||
looks for a `backup_completed` event (no component emits it anymore — the disk-tier backup moved to
|
||||
the agent in slice 8C, so the check fired daily for every healthy customer). It now reads the
|
||||
customer's **latest agent host-report** and raises `expected_backup_missed` only on positive
|
||||
evidence: no PBS snapshot / successful vzdump at all, the newest backup older than **26h**, or the
|
||||
newest PBS snapshot's `verify_state == "failed"`. A fresh-but-not-yet-verified snapshot is **not** a
|
||||
failure (PBS verifies on its own cadence) — alarming on it would just re-create the false alarm. The
|
||||
**db-dump half is unchanged** (the in-guest controller still emits `db_dump_completed`). A customer
|
||||
with **no host-report** (legacy/defunct) gets no backup alarm here — liveness is the
|
||||
host-staleness checker's job. New store accessor `GetLatestHostReportJSON`. Tests:
|
||||
`internal/monitor/deadline_test.go` (fresh+verified→quiet, stale→alarm, failed-verify→alarm,
|
||||
no-report→quiet, db-dump half preserved, plus a pure `assessBackupFreshness` table). The
|
||||
fresh+verified→quiet test is the **companion**: it fails against the old event-based check.
|
||||
|
||||
### Removed
|
||||
- **The Infra Backup feature**: ingest endpoint `POST /api/v1/infra-backup`, getters
|
||||
`GET /api/v1/infra-backup/{id}[/versions]` and their handlers; store methods
|
||||
`SaveInfraBackup` / `GetInfraBackup` / `GetInfraBackupByID` / `GetInfraBackupMeta` /
|
||||
`ListInfraBackupVersions` / `pruneInfraBackups` + the `InfraBackupMeta` / `InfraBackupVersion`
|
||||
types; the operator **"Infra Backup" panel** (`customer_unified.html`, `customer.html`). The
|
||||
`GET /api/v1/recovery/{id}` endpoint is kept but now returns **only** the generated `config_yaml`
|
||||
(no infra-backup payload). The customer-page **config-drift badge** that diffed against the stored
|
||||
controller.yaml is hidden (its at-rest source is gone); the live **"Show Diff"** path is unaffected.
|
||||
|
||||
### Security / migration
|
||||
- **Plaintext secret purge.** `migrate()` now `DROP`s `infra_backup_versions` + `infra_backups` and
|
||||
runs **`VACUUM`** (+ `wal_checkpoint(TRUNCATE)`) so the freed pages holding the plaintext keys/
|
||||
tokens are **physically reclaimed**, not merely delinked. Gated on table existence so normal
|
||||
restarts don't pay the VACUUM cost.
|
||||
- **Out of scope (flagged for the operator):** the exposed Cloudflare / hub / session credentials in
|
||||
the dropped blobs remain valid until rotated (operator step). Separately, the legacy `reports`
|
||||
table holds thousands of historical rows with a plaintext `restic_password` value from old
|
||||
controller versions — a distinct leak, not purged here (the live controller no longer sends it).
|
||||
|
||||
## v0.11.0 — slice 10D: DR capstone — recovery mode + re-enroll + directive serving (2026-06-10)
|
||||
|
||||
The hub half of the slice-10 DR capstone (closes slice 10). The hub ORCHESTRATES recovery but holds
|
||||
|
||||
Reference in New Issue
Block a user