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>
111 lines
5.3 KiB
Go
111 lines
5.3 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package kubetypes
|
|
|
|
import "fmt"
|
|
|
|
const (
|
|
// Hostinfo App values for the Tailscale Kubernetes Operator components.
|
|
AppOperator = "k8s-operator"
|
|
AppInProcessAPIServerProxy = "k8s-operator-proxy"
|
|
AppIngressProxy = "k8s-operator-ingress-proxy"
|
|
AppIngressResource = "k8s-operator-ingress-resource"
|
|
AppEgressProxy = "k8s-operator-egress-proxy"
|
|
AppConnector = "k8s-operator-connector-resource"
|
|
AppProxyGroupEgress = "k8s-operator-proxygroup-egress"
|
|
AppProxyGroupIngress = "k8s-operator-proxygroup-ingress"
|
|
AppProxyGroupKubeAPIServer = "k8s-operator-proxygroup-kube-apiserver"
|
|
|
|
// Clientmetrics for Tailscale Kubernetes Operator components
|
|
MetricIngressProxyCount = "k8s_ingress_proxies" // L3
|
|
MetricIngressResourceCount = "k8s_ingress_resources" // L7
|
|
MetricIngressPGResourceCount = "k8s_ingress_pg_resources" // L7 on ProxyGroup
|
|
MetricServicePGResourceCount = "k8s_service_pg_resources" // L3 on ProxyGroup
|
|
MetricEgressProxyCount = "k8s_egress_proxies"
|
|
MetricConnectorResourceCount = "k8s_connector_resources"
|
|
MetricConnectorWithSubnetRouterCount = "k8s_connector_subnetrouter_resources"
|
|
MetricConnectorWithExitNodeCount = "k8s_connector_exitnode_resources"
|
|
MetricConnectorWithAppConnectorCount = "k8s_connector_appconnector_resources"
|
|
MetricNameserverCount = "k8s_nameserver_resources"
|
|
MetricRecorderCount = "k8s_recorder_resources"
|
|
MetricEgressServiceCount = "k8s_egress_service_resources"
|
|
MetricProxyGroupEgressCount = "k8s_proxygroup_egress_resources"
|
|
MetricProxyGroupIngressCount = "k8s_proxygroup_ingress_resources"
|
|
MetricProxyGroupAPIServerCount = "k8s_proxygroup_kube_apiserver_resources"
|
|
MetricTailnetCount = "k8s_tailnet_resources"
|
|
MetricPeerRelayCount = "k8s_peerrelay_resources"
|
|
|
|
// Keys that containerboot writes to state file that can be used to determine its state.
|
|
// fields set in Tailscale state Secret. These are mostly used by the Tailscale Kubernetes operator to determine
|
|
// the state of this tailscale device.
|
|
KeyDeviceID = "device_id" // node stable ID of the device
|
|
KeyDeviceFQDN = "device_fqdn" // device's tailnet hostname
|
|
KeyDeviceIPs = "device_ips" // device's tailnet IPs
|
|
KeyPodUID = "pod_uid" // Pod UID
|
|
KeyCapVer = "tailscale_capver" // tailcfg.CurrentCapabilityVersion of this proxy instance.
|
|
KeyReissueAuthkey = "reissue_authkey" // Proxies will set this to the authkey that failed, or "no-authkey", if they can't log in.
|
|
// KeyHTTPSEndpoint is a name of a field that can be set to the value of any HTTPS endpoint currently exposed by
|
|
// this device to the tailnet. This is used by the Kubernetes operator Ingress proxy to communicate to the operator
|
|
// that cluster workloads behind the Ingress can now be accessed via the given DNS name over HTTPS.
|
|
KeyHTTPSEndpoint = "https_endpoint"
|
|
ValueNoHTTPS = "no-https"
|
|
|
|
// Pod's IPv4 address header key as returned by containerboot health check endpoint.
|
|
PodIPv4Header string = "Pod-IPv4"
|
|
// Pod's IPv6 address header key as returned by containerboot health check endpoint.
|
|
PodIPv6Header string = "Pod-IPv6"
|
|
|
|
EgessServicesPreshutdownEP = "/internal-egress-services-preshutdown"
|
|
|
|
LabelManaged = "tailscale.com/managed"
|
|
LabelSecretType = "tailscale.com/secret-type" // "config", "state" "certs"
|
|
|
|
LabelSecretTypeConfig = "config"
|
|
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"
|
|
)
|
|
|
|
// APIServerProxyMode specifies whether the API server proxy will add
|
|
// impersonation headers to requests based on the caller's Tailscale identity.
|
|
// May be "auth" or "noauth".
|
|
type APIServerProxyMode string
|
|
|
|
func (a *APIServerProxyMode) UnmarshalJSON(data []byte) error {
|
|
switch string(data) {
|
|
case `"auth"`:
|
|
*a = APIServerProxyModeAuth
|
|
case `"noauth"`:
|
|
*a = APIServerProxyModeNoAuth
|
|
default:
|
|
return fmt.Errorf("unknown APIServerProxyMode %q", data)
|
|
}
|
|
return nil
|
|
}
|