agent: EnsureLeaf signals + loud-WARNs a regenerated leaf (prevention B.1) v0.46.0

EnsureLeaf returns generated bool; call-site logs INFO 'leaf LOADED' vs WARN 'leaf REGENERATED —
previously issued bootstrap pins now INVALID'. Catches the 2026-06-28 silent-regen incident class.
Test: first=generated, second=loaded+same fp. No new sudo surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
This commit is contained in:
2026-06-29 21:45:25 +02:00
parent de6b72651b
commit 9b0d6c2c82
4 changed files with 51 additions and 14 deletions
+12 -7
View File
@@ -31,19 +31,24 @@ const certValidity = 10 * 365 * 24 * time.Hour
//
// Persisting the generated pair keeps the fingerprint STABLE across agent restarts — a fresh
// cert each boot would silently invalidate every already-issued bootstrap's pin.
func EnsureLeaf(certPath, keyPath, bridgeHost string) (tls.Certificate, string, error) {
// The returned `generated` is false when an existing pair was LOADED (the fingerprint is stable) and
// true when a fresh leaf was GENERATED (every previously-issued bootstrap pin is now invalid — the
// caller logs this LOUD, B.1, so an accidental regeneration like the 2026-06-28 migration is visible
// immediately instead of silently breaking every controller's pin for days).
func EnsureLeaf(certPath, keyPath, bridgeHost string) (cert tls.Certificate, fingerprint string, generated bool, err error) {
if fileExists(certPath) && fileExists(keyPath) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
cert, err = tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return tls.Certificate{}, "", fmt.Errorf("localapi: load leaf %s: %w", certPath, err)
return tls.Certificate{}, "", false, fmt.Errorf("localapi: load leaf %s: %w", certPath, err)
}
fp, err := leafFingerprint(cert)
fingerprint, err = leafFingerprint(cert)
if err != nil {
return tls.Certificate{}, "", err
return tls.Certificate{}, "", false, err
}
return cert, fp, nil
return cert, fingerprint, false, nil // LOADED
}
return generateLeaf(certPath, keyPath, bridgeHost)
cert, fingerprint, err = generateLeaf(certPath, keyPath, bridgeHost)
return cert, fingerprint, true, err // GENERATED
}
// generateLeaf creates a self-signed ECDSA-P256 leaf, writes the cert (0644) + key (0600) to
+12 -3
View File
@@ -14,14 +14,23 @@ func TestEnsureLeaf_StableFingerprintAcrossReload(t *testing.T) {
certPath := filepath.Join(dir, "leaf.crt")
keyPath := filepath.Join(dir, "leaf.key")
cert1, fp1, err := EnsureLeaf(certPath, keyPath, "192.168.0.162")
cert1, fp1, gen1, err := EnsureLeaf(certPath, keyPath, "192.168.0.162")
if err != nil {
t.Fatalf("first ensure: %v", err)
}
cert2, fp2, err := EnsureLeaf(certPath, keyPath, "192.168.0.162")
// B.1/B.3: first call GENERATES; second call LOADS (generated=false) with the SAME fingerprint —
// persistence keeps the pin stable. A regression that regenerated would flip gen2 true AND change
// the fp, failing both asserts (the prevention this change exists for).
if !gen1 {
t.Fatal("first EnsureLeaf must report generated=true")
}
cert2, fp2, gen2, err := EnsureLeaf(certPath, keyPath, "192.168.0.162")
if err != nil {
t.Fatalf("second ensure: %v", err)
}
if gen2 {
t.Fatal("second EnsureLeaf must report generated=false (LOADED, not regenerated)")
}
if fp1 != fp2 {
t.Fatalf("fingerprint changed across reload: %s != %s", fp1, fp2)
}
@@ -40,7 +49,7 @@ func TestEnsureLeaf_StableFingerprintAcrossReload(t *testing.T) {
// The generated leaf is a usable TLS server cert whose presented leaf matches the pin.
func TestEnsureLeaf_ServesPinnableLeaf(t *testing.T) {
dir := t.TempDir()
cert, fp, err := EnsureLeaf(filepath.Join(dir, "c"), filepath.Join(dir, "k"), "10.0.0.1")
cert, fp, _, err := EnsureLeaf(filepath.Join(dir, "c"), filepath.Join(dir, "k"), "10.0.0.1")
if err != nil {
t.Fatalf("ensure: %v", err)
}