766500dfc3
Spike-proven that backup/restore-to-PBS reuse Phase A unchanged; the only new code is the verify capability, a small PBS-API client, and PBSSnapshot reporting. - internal/pbs: fingerprint-pinned, token-authed PBS-API client (Verify/Snapshots/ TaskStatus, node-from-UPID; secret read from /etc/pve/priv/storage/<id>.pw at runtime, never logged) + the verify maintenance loop (own cadence, default 6h, NOT gated/journaled, like the watchdog) + SnapshotStore. - hub: PBSSnapshot filled (namespace/type/id/time/size/owner/protected/encrypted/ verify_state/verify_upid); PBSReporter collector seam; cross-repo golden + bidirectional key-set tests; hub handler parses pbs_snapshots + logs a failed-verify WARN. - backup: report the ACTUAL vzdump mode (parsed from the task log; PVE may downgrade snapshot->stop). proxmox.Storage.Username. config PBSVerifyCadence/secret-dir. --selftest=pbs-verify. Backup/restore-to-PBS unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
1.6 KiB
Go
49 lines
1.6 KiB
Go
package pbs
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// pinnedTLS builds a tls.Config that pins the PBS server's leaf cert by SHA-256 — the same
|
|
// model as the PVE client (proxmox/tls.go). PBS serves a self-signed cert, so we disable the
|
|
// default chain check but enforce an exact-cert match: a spoofed PBS presents a different
|
|
// fingerprint and is rejected. fingerprint is hex with optional colons (the form in
|
|
// /etc/pve/storage.cfg and the slice-5 durable_id).
|
|
func pinnedTLS(fingerprint string) (*tls.Config, error) {
|
|
want, err := normalizeFingerprint(fingerprint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &tls.Config{
|
|
InsecureSkipVerify: true, //nolint:gosec // replaced by the exact-cert pin below
|
|
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
|
if len(rawCerts) == 0 {
|
|
return fmt.Errorf("pbs: TLS pin: peer presented no certificate")
|
|
}
|
|
got := sha256.Sum256(rawCerts[0])
|
|
if hex.EncodeToString(got[:]) != want {
|
|
return fmt.Errorf("pbs: TLS pin mismatch: server cert sha256 does not match configured fingerprint")
|
|
}
|
|
return nil
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// normalizeFingerprint lowercases and strips colons/whitespace, validating a 64-char
|
|
// (32-byte) hex SHA-256.
|
|
func normalizeFingerprint(fp string) (string, error) {
|
|
s := strings.ToLower(strings.NewReplacer(":", "", " ", "", "\t", "").Replace(fp))
|
|
if len(s) != 64 {
|
|
return "", fmt.Errorf("pbs: fingerprint must be a SHA-256 (64 hex chars), got %d", len(s))
|
|
}
|
|
if _, err := hex.DecodeString(s); err != nil {
|
|
return "", fmt.Errorf("pbs: fingerprint is not valid hex: %w", err)
|
|
}
|
|
return s, nil
|
|
}
|