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)