From 6c6dd62b60ef2df7985c85ecc28f46532b5bf4d0 Mon Sep 17 00:00:00 2001 From: Jaron Kramer <121427510+jkramer5103@users.noreply.github.com> Date: Sat, 31 Oct 2026 13:43:17 +0200 Subject: [PATCH] ipn/store/kubestore: omit ARI replaces for unstamped shared certs (#22053) A retained legacy cert may have been issued by a different account even when no pre-adoption local key is available. Require a matching fingerprint before claiming a shared-account renewal replaces it. Fixes #22041 Signed-off-by: Jaron Kramer --- ipn/store/kubestore/store_kube.go | 8 +++--- ipn/store/kubestore/store_kube_test.go | 36 ++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/ipn/store/kubestore/store_kube.go b/ipn/store/kubestore/store_kube.go index 992a3e052..219243d3f 100644 --- a/ipn/store/kubestore/store_kube.go +++ b/ipn/store/kubestore/store_kube.go @@ -284,9 +284,11 @@ func (s *Store) ShouldUseARIReplacesForRenewal(domain string) (bool, error) { } certFP := sec.Data[keyACMEAcctFP] if len(certFP) == 0 { - // Legacy cert, no fingerprint stamp. Assume misaligned only if - // we adopted a foreign shared key. - if len(s.preAdoptedLocalKey) > 0 { + // A legacy cert has no record of its issuing account. With a + // shared account, the current key may have been created or adopted + // after this cert was issued, even without a prior local key. + // Omit replaces until a renewal stamps the issuing fingerprint. + if s.acmeAccountsSecretName != "" || len(s.preAdoptedLocalKey) > 0 { return false, nil } return true, nil diff --git a/ipn/store/kubestore/store_kube_test.go b/ipn/store/kubestore/store_kube_test.go index 19c206ca5..938d7f8f5 100644 --- a/ipn/store/kubestore/store_kube_test.go +++ b/ipn/store/kubestore/store_kube_test.go @@ -1107,6 +1107,7 @@ func TestShouldUseARIReplacesForRenewal(t *testing.T) { tests := []struct { name string certShareMode string + sharedSecret string acmeInMemory []byte // per-pod ACME key present in memory preAdopted []byte // sha256 of pre-adoption local key (foreign-key path) certSecret map[string][]byte @@ -1173,6 +1174,30 @@ func TestShouldUseARIReplacesForRenewal(t *testing.T) { certSecret: map[string][]byte{}, // no fingerprint field want: false, }, + { + name: "legacy_cert_with_shared_account_returns_false", + certShareMode: "rw", + sharedSecret: "tailscale-acme-accounts", + acmeInMemory: acmeKey, + certSecret: map[string][]byte{}, + want: false, + }, + { + name: "shared_account_fingerprint_matches", + certShareMode: "rw", + sharedSecret: "tailscale-acme-accounts", + acmeInMemory: acmeKey, + certSecret: map[string][]byte{keyACMEAcctFP: curFP}, + want: true, + }, + { + name: "shared_account_fingerprint_differs", + certShareMode: "rw", + sharedSecret: "tailscale-acme-accounts", + acmeInMemory: acmeKey, + certSecret: map[string][]byte{keyACMEAcctFP: otherFP}, + want: false, + }, } for _, tt := range tests { @@ -1186,11 +1211,12 @@ func TestShouldUseARIReplacesForRenewal(t *testing.T) { }, } s := &Store{ - client: client, - certShareMode: tt.certShareMode, - memory: mem.Store{}, - preAdoptedLocalKey: tt.preAdopted, - logf: t.Logf, + client: client, + certShareMode: tt.certShareMode, + acmeAccountsSecretName: tt.sharedSecret, + memory: mem.Store{}, + preAdoptedLocalKey: tt.preAdopted, + logf: t.Logf, } if len(tt.acmeInMemory) > 0 { s.memory.WriteState(ipn.StateKey(acmeAccountStateKey), tt.acmeInMemory)