cmd/k8s-operator,ipn/store/kubestore,kube/kubetypes: share ACME account key per tailnet

Introduce a per-tailnet shared ACME account key so that all ingress
ProxyGroup replicas on a tailnet present the same account identity to
Let's Encrypt. This lets renewals claim the ARI "replaces" exemption
from the 50-certs-per-week rate limit, surviving Pod restarts,
ProxyGroup recreation, and cluster migrations.

The operator provisions a "tailscale-acme-accounts" Secret in its
namespace, guarded by a finalizer and a deletion warning event, and
watched so it is recreated promptly if removed. Proxies migrate any
pre-existing per-pod key into the shared Secret on first boot, adopt
the shared key on subsequent boots, and restore it on cert writes if
the Secret was recreated empty. Certs are stamped with the fingerprint
of the issuing account so renewals skip the "replaces" claim when the
account doesn't match.

Opt-in per-ProxyGroup via the tailscale.com/share-acme-account
annotation, or operator-wide via OPERATOR_SHARED_ACME_ACCOUNT_KEY.

Updates #18251
Updates #20288

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
This commit is contained in:
chaosinthecrd
2026-07-27 12:06:41 +01:00
committed by Tom Meadows
parent 2900f3494a
commit 97a75c837d
13 changed files with 975 additions and 34 deletions
+22
View File
@@ -65,6 +65,28 @@ const (
LabelSecretTypeState = "state"
LabelSecretTypeCerts = "certs"
// ACMEAccountsSecretName is the name of the Secret in the operator
// namespace that holds shared ACME account private keys, one field per
// tailnet. Sharing one account key across all replicas of every
// ProxyGroup attached to a tailnet preserves Let's Encrypt's renewal
// exemption (via the ARI "replaces" extension) across pod restarts,
// ProxyGroup recreation, and ingress migration.
ACMEAccountsSecretName = "tailscale-acme-accounts"
// ACMEAccountDefaultKey is the field used inside ACMEAccountsSecretName
// for the "default" tailnet (i.e., when ProxyGroup.spec.tailnet is empty
// and the operator uses its own credentials).
ACMEAccountDefaultKey = "_default"
// ACMEAccountKeySuffix is appended to the tailnet identifier to form a
// field name within ACMEAccountsSecretName.
ACMEAccountKeySuffix = ".acme-account.key.pem"
// ACMEAccountsFinalizer blocks accidental deletion of
// ACMEAccountsSecretName; losing those keys breaks ARI "replaces"
// renewals for every cert in the tailnet. See #18251.
ACMEAccountsFinalizer = "tailscale.com/acme-account-protection"
KubeAPIServerConfigFile = "config.hujson"
APIServerProxyModeAuth APIServerProxyMode = "auth"
APIServerProxyModeNoAuth APIServerProxyMode = "noauth"