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:
2026-06-16 11:08:06 +02:00
parent 2f7acb7d07
commit 0635640848
10 changed files with 466 additions and 595 deletions
-23
View File
@@ -133,29 +133,6 @@
{{end}}
</section>
<!-- Infra Backup (Disaster Recovery) -->
<section class="card">
<h2>Infra Backup</h2>
{{if .InfraBackup}}
<div class="info-grid">
<div class="info-item">
<span class="label">Last Updated</span>
<span class="value">{{.InfraBackupAge}}</span>
</div>
<div class="info-item">
<span class="label">Deployed Stacks</span>
<span class="value">{{.InfraBackup.StackCount}}</span>
</div>
<div class="info-item">
<span class="label">Disks</span>
<span class="value">{{.InfraBackup.DiskCount}}</span>
</div>
</div>
{{else}}
<p style="color: #facc15">No infra backup received yet</p>
{{end}}
</section>
<!-- Health -->
<section class="card">
<h2>Health</h2>
@@ -302,55 +302,6 @@
{{end}}
{{end}}
<!-- Infra Backup -->
<section class="card">
<h2>Infra Backup</h2>
{{if .InfraBackup}}
<div class="info-grid">
<div class="info-item">
<span class="label">Last Updated</span>
<span class="value">{{.InfraBackupAge}}</span>
</div>
<div class="info-item">
<span class="label">Deployed Stacks</span>
<span class="value">{{.InfraBackup.StackCount}}</span>
</div>
<div class="info-item">
<span class="label">Disks</span>
<span class="value">{{.InfraBackup.DiskCount}}</span>
</div>
<div class="info-item">
<span class="label">Versions</span>
<span class="value">{{.InfraBackup.VersionCount}}</span>
</div>
</div>
{{if .InfraBackupVersions}}
<details style="margin-top: 0.75rem;">
<summary style="cursor: pointer; color: var(--text-secondary, #94a3b8); font-size: 0.85em;">Backup History ({{len .InfraBackupVersions}} versions)</summary>
<table style="width: 100%; margin-top: 0.5rem; font-size: 0.85em;">
<thead>
<tr>
<th style="text-align: left; padding: 0.25rem 0.5rem;">Date</th>
<th style="text-align: left; padding: 0.25rem 0.5rem;">Apps</th>
<th style="text-align: right; padding: 0.25rem 0.5rem;">Disks</th>
</tr>
</thead>
<tbody>
{{range .InfraBackupVersions}}
<tr>
<td style="padding: 0.25rem 0.5rem;">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
<td style="padding: 0.25rem 0.5rem;">{{.StackCount}}{{if .StackNames}}: {{range $i, $n := .StackNames}}{{if $i}}, {{end}}{{$n}}{{end}}{{end}}</td>
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{.DiskCount}}</td>
</tr>
{{end}}
</tbody>
</table>
</details>
{{end}}
{{else}}
<p style="color: #facc15">No infra backup received yet</p>
{{end}}
</section>
<!-- Health -->
<section class="card">
@@ -489,7 +440,7 @@
{{if eq .ConfigSyncStatus "in_sync"}}<span style="color: #22c55e;">&#x2713; In sync</span>
{{else if eq .ConfigSyncStatus "mismatch"}}<span style="color: #f59e0b;">&#x26A0; Config mismatch — {{.ConfigDiffCount}} difference{{if gt .ConfigDiffCount 1}}s{{end}}</span>
<button class="btn btn-outline btn-sm" style="margin-left: 0.5em; font-size: 0.8em;" onclick="showConfigDiff('{{.CustomerID}}')">Show Diff</button>
{{else}}<span style="color: #94a3b8;">Unknown — no infra backup available yet</span>
{{else}}<span style="color: #94a3b8;">Unknown — use "Show Diff" to compare live</span>
{{end}}
</span>
</div>