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,7 +1,6 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -186,34 +185,13 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
}
|
||||
}
|
||||
|
||||
// Config value comparison (parse both YAMLs, compare actual values)
|
||||
var configSyncStatus string // "in_sync", "mismatch", "unknown"
|
||||
// Config drift badge: the at-rest comparison source (infra-backup) was retired
|
||||
// 2026-06-16. With no stored controller.yaml to diff against, the passive badge is
|
||||
// left empty (the template hides it when ConfigSyncStatus == ""). The live "Show
|
||||
// Diff" path (handleCompareConfig, which fetches the controller's config over HTTP)
|
||||
// is unaffected and remains the way to check drift on demand.
|
||||
var configSyncStatus string // "" hides the badge; "in_sync"/"mismatch" reserved for a future live source
|
||||
var configDiffCount int
|
||||
if cfg != nil {
|
||||
infraData, _ := s.store.GetInfraBackup(customerID)
|
||||
if infraData != nil {
|
||||
controllerYAML := extractControllerYAML(infraData)
|
||||
if controllerYAML != "" {
|
||||
templateYAML := defaultControllerTemplate
|
||||
if s.templateFetcher != nil {
|
||||
templateYAML = s.templateFetcher.Template()
|
||||
}
|
||||
if hubYAML, err := configgen.Generate(templateYAML, cfg); err == nil {
|
||||
diffs := compareYAMLValues(hubYAML, controllerYAML)
|
||||
configDiffCount = len(diffs)
|
||||
if configDiffCount == 0 {
|
||||
configSyncStatus = "in_sync"
|
||||
} else {
|
||||
configSyncStatus = "mismatch"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
configSyncStatus = "unknown"
|
||||
}
|
||||
} else {
|
||||
configSyncStatus = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Version check
|
||||
var latestVersion string
|
||||
@@ -225,13 +203,10 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
}
|
||||
}
|
||||
|
||||
// History, notifications, events, infra backup
|
||||
// History, notifications, events
|
||||
var history []store.CustomerSummary
|
||||
var notifPrefs *store.NotificationPrefs
|
||||
var recentNotifs []store.NotificationLogEntry
|
||||
var infraMeta *store.InfraBackupMeta
|
||||
var infraBackupAge string
|
||||
var infraBackupVersions []store.InfraBackupVersion
|
||||
var events []store.Event
|
||||
var eventCounts map[string]int
|
||||
|
||||
@@ -241,11 +216,6 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
history, _ = s.store.GetCustomerHistory(customerID, 24*time.Hour)
|
||||
notifPrefs, _ = s.store.GetNotificationPrefs(customerID)
|
||||
recentNotifs, _ = s.store.GetRecentNotifications(customerID, 10)
|
||||
infraMeta, _ = s.store.GetInfraBackupMeta(customerID)
|
||||
if infraMeta != nil {
|
||||
infraBackupAge = timeAgo(infraMeta.UpdatedAt)
|
||||
}
|
||||
infraBackupVersions, _ = s.store.ListInfraBackupVersions(customerID)
|
||||
events, _ = s.store.GetRecentEvents(customerID, 50)
|
||||
eventCounts, _ = s.store.CountEventsBySeverity(customerID, time.Now().Add(-24*time.Hour))
|
||||
appTelemetry, _ = s.store.GetCustomerAppSummary(customerID, time.Now().Add(-7*24*time.Hour))
|
||||
@@ -274,9 +244,6 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
ConfigSyncStatus string // "in_sync", "mismatch", "unknown"
|
||||
ConfigDiffCount int
|
||||
|
||||
InfraBackup *store.InfraBackupMeta
|
||||
InfraBackupAge string
|
||||
InfraBackupVersions []store.InfraBackupVersion
|
||||
NotifPrefs *store.NotificationPrefs
|
||||
RecentNotifications []store.NotificationLogEntry
|
||||
History []store.CustomerSummary
|
||||
@@ -316,9 +283,6 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
ConfigSyncStatus: configSyncStatus,
|
||||
ConfigDiffCount: configDiffCount,
|
||||
|
||||
InfraBackup: infraMeta,
|
||||
InfraBackupAge: infraBackupAge,
|
||||
InfraBackupVersions: infraBackupVersions,
|
||||
NotifPrefs: notifPrefs,
|
||||
RecentNotifications: recentNotifs,
|
||||
History: history,
|
||||
@@ -734,22 +698,7 @@ func buildConfigJSON(r *http.Request) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// --- Config comparison helpers ---
|
||||
|
||||
// extractControllerYAML decodes the controller.yaml from an infra backup JSON payload.
|
||||
func extractControllerYAML(infraData []byte) string {
|
||||
var parsed struct {
|
||||
ControllerConfigB64 string `json:"controller_config_b64"`
|
||||
}
|
||||
if err := json.Unmarshal(infraData, &parsed); err != nil || parsed.ControllerConfigB64 == "" {
|
||||
return ""
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(parsed.ControllerConfigB64)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
// --- Config comparison helpers (used by the live "Show Diff" handler) ---
|
||||
|
||||
// volatileKeys are YAML keys ignored during config comparison (always differ or deprecated).
|
||||
var volatileKeys = map[string]bool{
|
||||
|
||||
@@ -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;">✓ In sync</span>
|
||||
{{else if eq .ConfigSyncStatus "mismatch"}}<span style="color: #f59e0b;">⚠ 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>
|
||||
|
||||
Reference in New Issue
Block a user