From 560bcf0cae764015520b1d1efbef2a0bb4fe88b7 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 23 Apr 2026 15:51:27 +0400 Subject: [PATCH] feat: enforce TLS 1.3 minmum version for Kubernetes components Fixes #13120 Signed-off-by: Andrey Smirnov --- hack/release.toml | 7 +++ .../k8s/control_plane_static_pod.go | 48 +++++++++---------- .../k8s/control_plane_static_pod_test.go | 15 +++++- .../app/machined/pkg/system/services/etcd.go | 4 +- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/hack/release.toml b/hack/release.toml index 6d387c1d7..9b279cb85 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -34,6 +34,13 @@ NTS is enabled by default (without any configuration sources) for the default `t NTS can be enabled for custom time servers via the new `useNTS` field in the `TimeServerConfig` document. """ + [notes.tls13] + title = "TLS 1.3 Minimum Version" + description = """\ +Talos now runs etcd and kube-apiserver with a minimum TLS version of 1.3, improving security by leveraging the latest TLS features and cipher suites. +Custom settings for cipher suites have been removed, as they are ignored when TLS 1.3 is used, which simplifies configuration and ensures the use of modern, secure defaults. +""" + [make_deps] diff --git a/internal/app/machined/pkg/controllers/k8s/control_plane_static_pod.go b/internal/app/machined/pkg/controllers/k8s/control_plane_static_pod.go index 70de1a89f..85ca3cb17 100644 --- a/internal/app/machined/pkg/controllers/k8s/control_plane_static_pod.go +++ b/internal/app/machined/pkg/controllers/k8s/control_plane_static_pod.go @@ -369,30 +369,28 @@ func (ctrl *ControlPlaneStaticPodController) manageAPIServer(ctx context.Context "proxy-client-cert-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "front-proxy-client.crt")}, "proxy-client-key-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "front-proxy-client.key")}, "enable-bootstrap-token-auth": {"true"}, - // NB: using TLS 1.2 instead of 1.3 here for interoperability, since this is an externally-facing service. - "tls-min-version": {"VersionTLS12"}, - "tls-cipher-suites": {"TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"}, //nolint:lll - "encryption-provider-config": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "encryptionconfig.yaml")}, - "audit-policy-file": {filepath.Join(constants.KubernetesAPIServerConfigDir, "auditpolicy.yaml")}, - "audit-log-path": {filepath.Join(constants.KubernetesAuditLogDir, "kube-apiserver.log")}, - "audit-log-maxage": {"30"}, - "audit-log-maxbackup": {"10"}, - "audit-log-maxsize": {"100"}, - "profiling": {"false"}, - "etcd-cafile": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "etcd-client-ca.crt")}, - "etcd-certfile": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "etcd-client.crt")}, - "etcd-keyfile": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "etcd-client.key")}, - "etcd-servers": {strings.Join(cfg.EtcdServers, ",")}, - "kubelet-client-certificate": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "apiserver-kubelet-client.crt")}, - "kubelet-client-key": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "apiserver-kubelet-client.key")}, - "secure-port": {strconv.FormatInt(int64(cfg.LocalPort), 10)}, - "service-account-issuer": {cfg.ControlPlaneEndpoint}, - "service-account-key-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "service-account.pub")}, - "service-account-signing-key-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "service-account.key")}, - "service-cluster-ip-range": {strings.Join(cfg.ServiceCIDRs, ",")}, - "tls-cert-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "apiserver.crt")}, - "tls-private-key-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "apiserver.key")}, - "kubelet-preferred-address-types": {"InternalIP,ExternalIP,Hostname"}, + "tls-min-version": {"VersionTLS13"}, + "encryption-provider-config": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "encryptionconfig.yaml")}, + "audit-policy-file": {filepath.Join(constants.KubernetesAPIServerConfigDir, "auditpolicy.yaml")}, + "audit-log-path": {filepath.Join(constants.KubernetesAuditLogDir, "kube-apiserver.log")}, + "audit-log-maxage": {"30"}, + "audit-log-maxbackup": {"10"}, + "audit-log-maxsize": {"100"}, + "profiling": {"false"}, + "etcd-cafile": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "etcd-client-ca.crt")}, + "etcd-certfile": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "etcd-client.crt")}, + "etcd-keyfile": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "etcd-client.key")}, + "etcd-servers": {strings.Join(cfg.EtcdServers, ",")}, + "kubelet-client-certificate": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "apiserver-kubelet-client.crt")}, + "kubelet-client-key": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "apiserver-kubelet-client.key")}, + "secure-port": {strconv.FormatInt(int64(cfg.LocalPort), 10)}, + "service-account-issuer": {cfg.ControlPlaneEndpoint}, + "service-account-key-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "service-account.pub")}, + "service-account-signing-key-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "service-account.key")}, + "service-cluster-ip-range": {strings.Join(cfg.ServiceCIDRs, ",")}, + "tls-cert-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "apiserver.crt")}, + "tls-private-key-file": {filepath.Join(constants.KubernetesAPIServerSecretsDir, "apiserver.key")}, + "kubelet-preferred-address-types": {"InternalIP,ExternalIP,Hostname"}, } if cfg.AdvertisedAddress != "" { @@ -416,7 +414,6 @@ func (ctrl *ControlPlaneStaticPodController) manageAPIServer(ctx context.Context "enable-admission-plugins": argsbuilder.MergeAdditive, "feature-gates": argsbuilder.MergeAdditive, "authorization-mode": argsbuilder.MergeAdditive, - "tls-cipher-suites": argsbuilder.MergeAdditive, "etcd-servers": argsbuilder.MergeDenied, "client-ca-file": argsbuilder.MergeDenied, @@ -433,6 +430,7 @@ func (ctrl *ControlPlaneStaticPodController) manageAPIServer(ctx context.Context "service-account-key-file": argsbuilder.MergeDenied, "service-account-signing-key-file": argsbuilder.MergeDenied, "tls-cert-file": argsbuilder.MergeDenied, + "tls-min-version": argsbuilder.MergeDenied, "tls-private-key-file": argsbuilder.MergeDenied, "authorization-config": argsbuilder.MergeDenied, } diff --git a/internal/app/machined/pkg/controllers/k8s/control_plane_static_pod_test.go b/internal/app/machined/pkg/controllers/k8s/control_plane_static_pod_test.go index 50bcfb531..e69f6d8e8 100644 --- a/internal/app/machined/pkg/controllers/k8s/control_plane_static_pod_test.go +++ b/internal/app/machined/pkg/controllers/k8s/control_plane_static_pod_test.go @@ -55,7 +55,14 @@ func (suite *ControlPlaneStaticPodSuite) TestReconcileDefaults() { k8s.ControllerManagerID, k8s.SchedulerID, }, - func(*k8s.StaticPod, *assert.Assertions) {}, + func(staticPod *k8s.StaticPod, asrt *assert.Assertions) { + pod, err := k8sadapter.StaticPod(staticPod).Pod() + suite.Require().NoError(err) + + if staticPod.Metadata().ID() == k8s.APIServerID { + asrt.Contains(pod.Spec.Containers[0].Command, "--tls-min-version=VersionTLS13") + } + }, ) } @@ -281,6 +288,12 @@ func (suite *ControlPlaneStaticPodSuite) TestReconcileExtraArgsK8s() { }, expectError: true, }, + { + args: map[string]k8s.ArgValues{ + "tls-min-version": {Values: []string{"TLS1.2"}}, + }, + expectError: true, + }, } configStatus := k8s.NewConfigStatus(k8s.ControlPlaneNamespaceName, k8s.ConfigStatusStaticPodID) diff --git a/internal/app/machined/pkg/system/services/etcd.go b/internal/app/machined/pkg/system/services/etcd.go index 27313f865..bffbe93d3 100644 --- a/internal/app/machined/pkg/system/services/etcd.go +++ b/internal/app/machined/pkg/system/services/etcd.go @@ -199,8 +199,6 @@ func (e *Etcd) Runner(r runtime.Runtime) (runner.Runner, error) { env = append(env, "ETCD_UNSUPPORTED_ARCH="+goruntime.GOARCH) } - env = append(env, "ETCD_CIPHER_SUITES=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305") //nolint:lll - if e.learnerMemberID != 0 { var promoteCtx context.Context @@ -406,6 +404,7 @@ func (e *Etcd) argsForInit(ctx context.Context, r runtime.Runtime, spec *etcdres "experimental-initial-corrupt-check": {"true"}, "experimental-watch-progress-notify-interval": {"5s"}, "experimental-compact-hash-check-enabled": {"true"}, + "tls-min-version": {"TLS1.3"}, } extraArgs := make(argsbuilder.Args, len(spec.ExtraArgs)) @@ -486,6 +485,7 @@ func (e *Etcd) argsForControlPlane(ctx context.Context, r runtime.Runtime, spec "experimental-initial-corrupt-check": {"true"}, "experimental-watch-progress-notify-interval": {"5s"}, "experimental-compact-hash-check-enabled": {"true"}, + "tls-min-version": {"TLS1.3"}, } extraArgs := make(argsbuilder.Args, len(spec.ExtraArgs))