diff --git a/api/bootstrap/kubeadm/v1beta1/conversion.go b/api/bootstrap/kubeadm/v1beta1/conversion.go index 3f6bf3eeb155..8c5d7c57f410 100644 --- a/api/bootstrap/kubeadm/v1beta1/conversion.go +++ b/api/bootstrap/kubeadm/v1beta1/conversion.go @@ -81,6 +81,9 @@ func RestoreKubeadmConfigSpec(restored *bootstrapv1.KubeadmConfigSpec, dst *boot dst.ClusterConfiguration.CACertificateValidityPeriodDays = restored.ClusterConfiguration.CACertificateValidityPeriodDays } } + if restored.ClusterConfiguration.EncryptionAlgorithm != "" { + dst.ClusterConfiguration.EncryptionAlgorithm = restored.ClusterConfiguration.EncryptionAlgorithm + } } func RestoreBoolIntentKubeadmConfigSpec(src *KubeadmConfigSpec, dst *bootstrapv1.KubeadmConfigSpec, hasRestored bool, restored *bootstrapv1.KubeadmConfigSpec) error { diff --git a/api/bootstrap/kubeadm/v1beta1/zz_generated.conversion.go b/api/bootstrap/kubeadm/v1beta1/zz_generated.conversion.go index bae21feb41fd..16b20da9ed1e 100644 --- a/api/bootstrap/kubeadm/v1beta1/zz_generated.conversion.go +++ b/api/bootstrap/kubeadm/v1beta1/zz_generated.conversion.go @@ -691,6 +691,7 @@ func autoConvert_v1beta2_ClusterConfiguration_To_v1beta1_ClusterConfiguration(in out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) // WARNING: in.CertificateValidityPeriodDays requires manual conversion: does not exist in peer-type // WARNING: in.CACertificateValidityPeriodDays requires manual conversion: does not exist in peer-type + // WARNING: in.EncryptionAlgorithm requires manual conversion: does not exist in peer-type return nil } diff --git a/api/bootstrap/kubeadm/v1beta2/kubeadm_types.go b/api/bootstrap/kubeadm/v1beta2/kubeadm_types.go index cf1d0d5ffcd9..17e3bfc7840f 100644 --- a/api/bootstrap/kubeadm/v1beta2/kubeadm_types.go +++ b/api/bootstrap/kubeadm/v1beta2/kubeadm_types.go @@ -72,6 +72,23 @@ const ( KubeadmConfigDataSecretNotAvailableReason = clusterv1.NotAvailableReason ) +// EncryptionAlgorithmType can define an asymmetric encryption algorithm type. +// +kubebuilder:validation:Enum=ECDSA-P256;ECDSA-P384;RSA-2048;RSA-3072;RSA-4096 +type EncryptionAlgorithmType string + +const ( + // EncryptionAlgorithmECDSAP256 defines the ECDSA encryption algorithm type with curve P256. + EncryptionAlgorithmECDSAP256 EncryptionAlgorithmType = "ECDSA-P256" + // EncryptionAlgorithmECDSAP384 defines the ECDSA encryption algorithm type with curve P384. + EncryptionAlgorithmECDSAP384 EncryptionAlgorithmType = "ECDSA-P384" + // EncryptionAlgorithmRSA2048 defines the RSA encryption algorithm type with key size 2048 bits. + EncryptionAlgorithmRSA2048 EncryptionAlgorithmType = "RSA-2048" + // EncryptionAlgorithmRSA3072 defines the RSA encryption algorithm type with key size 3072 bits. + EncryptionAlgorithmRSA3072 EncryptionAlgorithmType = "RSA-3072" + // EncryptionAlgorithmRSA4096 defines the RSA encryption algorithm type with key size 4096 bits. + EncryptionAlgorithmRSA4096 EncryptionAlgorithmType = "RSA-4096" +) + // InitConfiguration contains a list of elements that is specific "kubeadm init"-only runtime // information. // +kubebuilder:validation:MinProperties=1 @@ -199,6 +216,16 @@ type ClusterConfiguration struct { // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=36500 CACertificateValidityPeriodDays int32 `json:"caCertificateValidityPeriodDays,omitempty"` + + // encryptionAlgorithm holds the type of asymmetric encryption algorithm used for keys and certificates. + // Can be one of "RSA-2048", "RSA-3072", "RSA-4096", "ECDSA-P256" or "ECDSA-P384". + // For Kubernetes 1.34 or above, "ECDSA-P384" is supported. + // If not specified, Cluster API will use RSA-2048 as default. + // When this field is modified every certificate generated afterward will use the new + // encryptionAlgorithm. Existing CA certificates and service account keys are not rotated. + // This field is only supported with Kubernetes v1.31 or above. + // +optional + EncryptionAlgorithm EncryptionAlgorithmType `json:"encryptionAlgorithm,omitempty"` } // IsDefined returns true if the ClusterConfiguration is defined. diff --git a/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml b/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml index 5c0dbd17e0e7..2902ef5f8e90 100644 --- a/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml +++ b/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml @@ -4853,6 +4853,22 @@ spec: minLength: 1 type: string type: object + encryptionAlgorithm: + description: |- + encryptionAlgorithm holds the type of asymmetric encryption algorithm used for keys and certificates. + Can be one of "RSA-2048", "RSA-3072", "RSA-4096", "ECDSA-P256" or "ECDSA-P384". + For Kubernetes 1.34 or above, "ECDSA-P384" is supported. + If not specified, Cluster API will use RSA-2048 as default. + When this field is modified every certificate generated afterward will use the new + encryptionAlgorithm. Existing CA certificates and service account keys are not rotated. + This field is only supported with Kubernetes v1.31 or above. + enum: + - ECDSA-P256 + - ECDSA-P384 + - RSA-2048 + - RSA-3072 + - RSA-4096 + type: string etcd: description: |- etcd holds configuration for etcd. diff --git a/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml b/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml index cd0a899bf5fb..6a458e6de99a 100644 --- a/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml +++ b/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml @@ -4734,6 +4734,22 @@ spec: minLength: 1 type: string type: object + encryptionAlgorithm: + description: |- + encryptionAlgorithm holds the type of asymmetric encryption algorithm used for keys and certificates. + Can be one of "RSA-2048", "RSA-3072", "RSA-4096", "ECDSA-P256" or "ECDSA-P384". + For Kubernetes 1.34 or above, "ECDSA-P384" is supported. + If not specified, Cluster API will use RSA-2048 as default. + When this field is modified every certificate generated afterward will use the new + encryptionAlgorithm. Existing CA certificates and service account keys are not rotated. + This field is only supported with Kubernetes v1.31 or above. + enum: + - ECDSA-P256 + - ECDSA-P384 + - RSA-2048 + - RSA-3072 + - RSA-4096 + type: string etcd: description: |- etcd holds configuration for etcd. diff --git a/bootstrap/kubeadm/types/upstreamv1beta3/conversion_test.go b/bootstrap/kubeadm/types/upstreamv1beta3/conversion_test.go index c4fe0aba558b..ae1032b0254d 100644 --- a/bootstrap/kubeadm/types/upstreamv1beta3/conversion_test.go +++ b/bootstrap/kubeadm/types/upstreamv1beta3/conversion_test.go @@ -276,6 +276,7 @@ func hubClusterConfigurationFuzzer(obj *bootstrapv1.ClusterConfiguration, c rand obj.CertificateValidityPeriodDays = 0 obj.CACertificateValidityPeriodDays = 0 + obj.EncryptionAlgorithm = "" for i, arg := range obj.APIServer.ExtraArgs { if arg.Value == nil { diff --git a/bootstrap/kubeadm/types/upstreamv1beta3/zz_generated.conversion.go b/bootstrap/kubeadm/types/upstreamv1beta3/zz_generated.conversion.go index ea1aee4b6f8b..a0dadb029f93 100644 --- a/bootstrap/kubeadm/types/upstreamv1beta3/zz_generated.conversion.go +++ b/bootstrap/kubeadm/types/upstreamv1beta3/zz_generated.conversion.go @@ -399,6 +399,7 @@ func autoConvert_v1beta2_ClusterConfiguration_To_upstreamv1beta3_ClusterConfigur out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) // WARNING: in.CertificateValidityPeriodDays requires manual conversion: does not exist in peer-type // WARNING: in.CACertificateValidityPeriodDays requires manual conversion: does not exist in peer-type + // WARNING: in.EncryptionAlgorithm requires manual conversion: does not exist in peer-type return nil } diff --git a/bootstrap/kubeadm/types/upstreamv1beta4/conversion.go b/bootstrap/kubeadm/types/upstreamv1beta4/conversion.go index e730dac72b11..410d87c8e871 100644 --- a/bootstrap/kubeadm/types/upstreamv1beta4/conversion.go +++ b/bootstrap/kubeadm/types/upstreamv1beta4/conversion.go @@ -67,7 +67,6 @@ func (dst *JoinConfiguration) ConvertFrom(srcRaw conversion.Hub) error { func Convert_upstreamv1beta4_ClusterConfiguration_To_v1beta2_ClusterConfiguration(in *ClusterConfiguration, out *bootstrapv1.ClusterConfiguration, s apimachineryconversion.Scope) error { // Following fields do not exist in CABPK v1beta1 version: // - Proxy (Not supported yet) - // - EncryptionAlgorithm (Not supported yet) if err := autoConvert_upstreamv1beta4_ClusterConfiguration_To_v1beta2_ClusterConfiguration(in, out, s); err != nil { return err } diff --git a/bootstrap/kubeadm/types/upstreamv1beta4/conversion_test.go b/bootstrap/kubeadm/types/upstreamv1beta4/conversion_test.go index 89e2143b8ff0..631d0af42c76 100644 --- a/bootstrap/kubeadm/types/upstreamv1beta4/conversion_test.go +++ b/bootstrap/kubeadm/types/upstreamv1beta4/conversion_test.go @@ -107,7 +107,6 @@ func spokeClusterConfigurationFuzzer(obj *ClusterConfiguration, c randfill.Conti c.FillNoCustom(obj) obj.Proxy = Proxy{} - obj.EncryptionAlgorithm = "" obj.CertificateValidityPeriod = ptr.To[metav1.Duration](metav1.Duration{Duration: time.Duration(c.Int31n(3*365)+1) * time.Hour * 24}) obj.CACertificateValidityPeriod = ptr.To[metav1.Duration](metav1.Duration{Duration: time.Duration(c.Int31n(100*365)+1) * time.Hour * 24}) diff --git a/bootstrap/kubeadm/types/upstreamv1beta4/zz_generated.conversion.go b/bootstrap/kubeadm/types/upstreamv1beta4/zz_generated.conversion.go index b2f95bf2569f..5bb43af374a6 100644 --- a/bootstrap/kubeadm/types/upstreamv1beta4/zz_generated.conversion.go +++ b/bootstrap/kubeadm/types/upstreamv1beta4/zz_generated.conversion.go @@ -430,7 +430,7 @@ func autoConvert_upstreamv1beta4_ClusterConfiguration_To_v1beta2_ClusterConfigur out.ImageRepository = in.ImageRepository out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) // WARNING: in.ClusterName requires manual conversion: does not exist in peer-type - // WARNING: in.EncryptionAlgorithm requires manual conversion: does not exist in peer-type + out.EncryptionAlgorithm = v1beta2.EncryptionAlgorithmType(in.EncryptionAlgorithm) // WARNING: in.CertificateValidityPeriod requires manual conversion: does not exist in peer-type // WARNING: in.CACertificateValidityPeriod requires manual conversion: does not exist in peer-type return nil @@ -458,6 +458,7 @@ func autoConvert_v1beta2_ClusterConfiguration_To_upstreamv1beta4_ClusterConfigur out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) // WARNING: in.CertificateValidityPeriodDays requires manual conversion: does not exist in peer-type // WARNING: in.CACertificateValidityPeriodDays requires manual conversion: does not exist in peer-type + out.EncryptionAlgorithm = EncryptionAlgorithmType(in.EncryptionAlgorithm) return nil } diff --git a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml index cd683831541d..5fc53686c597 100644 --- a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml +++ b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml @@ -5771,6 +5771,22 @@ spec: minLength: 1 type: string type: object + encryptionAlgorithm: + description: |- + encryptionAlgorithm holds the type of asymmetric encryption algorithm used for keys and certificates. + Can be one of "RSA-2048", "RSA-3072", "RSA-4096", "ECDSA-P256" or "ECDSA-P384". + For Kubernetes 1.34 or above, "ECDSA-P384" is supported. + If not specified, Cluster API will use RSA-2048 as default. + When this field is modified every certificate generated afterward will use the new + encryptionAlgorithm. Existing CA certificates and service account keys are not rotated. + This field is only supported with Kubernetes v1.31 or above. + enum: + - ECDSA-P256 + - ECDSA-P384 + - RSA-2048 + - RSA-3072 + - RSA-4096 + type: string etcd: description: |- etcd holds configuration for etcd. diff --git a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml index e82ff9bcda9c..694318b53ed9 100644 --- a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml +++ b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml @@ -4156,6 +4156,22 @@ spec: minLength: 1 type: string type: object + encryptionAlgorithm: + description: |- + encryptionAlgorithm holds the type of asymmetric encryption algorithm used for keys and certificates. + Can be one of "RSA-2048", "RSA-3072", "RSA-4096", "ECDSA-P256" or "ECDSA-P384". + For Kubernetes 1.34 or above, "ECDSA-P384" is supported. + If not specified, Cluster API will use RSA-2048 as default. + When this field is modified every certificate generated afterward will use the new + encryptionAlgorithm. Existing CA certificates and service account keys are not rotated. + This field is only supported with Kubernetes v1.31 or above. + enum: + - ECDSA-P256 + - ECDSA-P384 + - RSA-2048 + - RSA-3072 + - RSA-4096 + type: string etcd: description: |- etcd holds configuration for etcd. diff --git a/controlplane/kubeadm/internal/cluster.go b/controlplane/kubeadm/internal/cluster.go index 8c29744a1211..dc41ca104b9b 100644 --- a/controlplane/kubeadm/internal/cluster.go +++ b/controlplane/kubeadm/internal/cluster.go @@ -30,6 +30,7 @@ import ( "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" + bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/controllers/clustercache" "sigs.k8s.io/cluster-api/util/cache" @@ -43,7 +44,7 @@ type ManagementCluster interface { GetMachinesForCluster(ctx context.Context, cluster *clusterv1.Cluster, filters ...collections.Func) (collections.Machines, error) GetMachinePoolsForCluster(ctx context.Context, cluster *clusterv1.Cluster) (*clusterv1.MachinePoolList, error) - GetWorkloadCluster(ctx context.Context, clusterKey client.ObjectKey) (WorkloadCluster, error) + GetWorkloadCluster(ctx context.Context, clusterKey client.ObjectKey, keyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType) (WorkloadCluster, error) } // Management holds operations on the management cluster. @@ -59,13 +60,14 @@ type Management struct { // ClientCertEntry is an Entry for the Cache that stores the client cert. type ClientCertEntry struct { - Cluster client.ObjectKey - ClientCert *tls.Certificate + Cluster client.ObjectKey + ClientCert *tls.Certificate + EncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType } // Key returns the cache key of a ClientCertEntry. func (r ClientCertEntry) Key() string { - return r.Cluster.String() + return fmt.Sprintf("%s/%s", r.Cluster.String(), r.EncryptionAlgorithm) } // RemoteClusterConnectionError represents a failure to connect to a remote cluster. @@ -77,7 +79,7 @@ type RemoteClusterConnectionError struct { // Error satisfies the error interface. func (e *RemoteClusterConnectionError) Error() string { return e.Name + ": " + e.Err.Error() } -// Unwrap satisfies the unwrap error inteface. +// Unwrap satisfies the unwrap error interface. func (e *RemoteClusterConnectionError) Unwrap() error { return e.Err } // Get implements client.Reader. @@ -111,7 +113,7 @@ func (m *Management) GetMachinePoolsForCluster(ctx context.Context, cluster *clu // GetWorkloadCluster builds a cluster object. // The cluster comes with an etcd client generator to connect to any etcd pod living on a managed machine. -func (m *Management) GetWorkloadCluster(ctx context.Context, clusterKey client.ObjectKey) (WorkloadCluster, error) { +func (m *Management) GetWorkloadCluster(ctx context.Context, clusterKey client.ObjectKey, keyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType) (WorkloadCluster, error) { // TODO(chuckha): Inject this dependency. // TODO(chuckha): memoize this function. The workload client only exists as long as a reconciliation loop. restConfig, err := m.ClusterCache.GetRESTConfig(ctx, clusterKey) @@ -140,17 +142,15 @@ func (m *Management) GetWorkloadCluster(ctx context.Context, clusterKey client.O var clientCert tls.Certificate if keyData != nil { // Get client cert from cache if possible, otherwise generate it and add it to the cache. - // TODO: When we implement ClusterConfiguration.EncryptionAlgorithm we should add it to - // the ClientCertEntries and make it part of the key. - if entry, ok := m.ClientCertCache.Has(ClientCertEntry{Cluster: clusterKey}.Key()); ok { + if entry, ok := m.ClientCertCache.Has(ClientCertEntry{Cluster: clusterKey, EncryptionAlgorithm: keyEncryptionAlgorithm}.Key()); ok { clientCert = *entry.ClientCert } else { // The client cert expires after 10 years, but that's okay as the cache has a TTL of 1 day. - clientCert, err = generateClientCert(crtData, keyData) + clientCert, err = generateClientCert(crtData, keyData, keyEncryptionAlgorithm) if err != nil { return nil, err } - m.ClientCertCache.Add(ClientCertEntry{Cluster: clusterKey, ClientCert: &clientCert}) + m.ClientCertCache.Add(ClientCertEntry{Cluster: clusterKey, ClientCert: &clientCert, EncryptionAlgorithm: keyEncryptionAlgorithm}) } } else { clientCert, err = m.getAPIServerEtcdClientCert(ctx, clusterKey) diff --git a/controlplane/kubeadm/internal/cluster_test.go b/controlplane/kubeadm/internal/cluster_test.go index 6838fdd824e4..cb1f9087785f 100644 --- a/controlplane/kubeadm/internal/cluster_test.go +++ b/controlplane/kubeadm/internal/cluster_test.go @@ -39,6 +39,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/reconcile" + bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/controllers/clustercache" "sigs.k8s.io/cluster-api/controllers/remote" @@ -249,7 +250,7 @@ func TestGetWorkloadCluster(t *testing.T) { }) g.Expect(err).ToNot(HaveOccurred()) - workloadCluster, err := m.GetWorkloadCluster(ctx, tt.clusterKey) + workloadCluster, err := m.GetWorkloadCluster(ctx, tt.clusterKey, bootstrapv1.EncryptionAlgorithmRSA2048) if tt.expectErr { g.Expect(err).To(HaveOccurred()) g.Expect(workloadCluster).To(BeNil()) diff --git a/controlplane/kubeadm/internal/control_plane.go b/controlplane/kubeadm/internal/control_plane.go index 6c16276f63df..d7e6f4973124 100644 --- a/controlplane/kubeadm/internal/control_plane.go +++ b/controlplane/kubeadm/internal/control_plane.go @@ -372,7 +372,7 @@ func (c *ControlPlane) GetWorkloadCluster(ctx context.Context) (WorkloadCluster, return c.workloadCluster, nil } - workloadCluster, err := c.managementCluster.GetWorkloadCluster(ctx, client.ObjectKeyFromObject(c.Cluster)) + workloadCluster, err := c.managementCluster.GetWorkloadCluster(ctx, client.ObjectKeyFromObject(c.Cluster), c.GetKeyEncryptionAlgorithm()) if err != nil { return nil, err } @@ -467,3 +467,12 @@ func (c *ControlPlane) StatusToLogKeyAndValues(newMachine, deletedMachine *clust "etcdMembers", strings.Join(etcdMembers, ", "), } } + +// GetKeyEncryptionAlgorithm returns the control plane EncryptionAlgorithm. +// If its unset the default encryption algorithm is returned. +func (c *ControlPlane) GetKeyEncryptionAlgorithm() bootstrapv1.EncryptionAlgorithmType { + if c.KCP.Spec.KubeadmConfigSpec.ClusterConfiguration.EncryptionAlgorithm == "" { + return bootstrapv1.EncryptionAlgorithmRSA2048 + } + return c.KCP.Spec.KubeadmConfigSpec.ClusterConfiguration.EncryptionAlgorithm +} diff --git a/controlplane/kubeadm/internal/controllers/fakes_test.go b/controlplane/kubeadm/internal/controllers/fakes_test.go index 8d819b52d6ad..e2caf8caaf45 100644 --- a/controlplane/kubeadm/internal/controllers/fakes_test.go +++ b/controlplane/kubeadm/internal/controllers/fakes_test.go @@ -49,7 +49,7 @@ func (f *fakeManagementCluster) List(ctx context.Context, list client.ObjectList return f.Reader.List(ctx, list, opts...) } -func (f *fakeManagementCluster) GetWorkloadCluster(_ context.Context, _ client.ObjectKey) (internal.WorkloadCluster, error) { +func (f *fakeManagementCluster) GetWorkloadCluster(_ context.Context, _ client.ObjectKey, _ bootstrapv1.EncryptionAlgorithmType) (internal.WorkloadCluster, error) { return f.Workload, f.WorkloadErr } diff --git a/controlplane/kubeadm/internal/controllers/helpers.go b/controlplane/kubeadm/internal/controllers/helpers.go index 7a22da80aecb..c35446381378 100644 --- a/controlplane/kubeadm/internal/controllers/helpers.go +++ b/controlplane/kubeadm/internal/controllers/helpers.go @@ -52,7 +52,6 @@ func (r *KubeadmControlPlaneReconciler) reconcileKubeconfig(ctx context.Context, if endpoint.IsZero() { return ctrl.Result{}, nil } - controllerOwnerRef := *metav1.NewControllerRef(controlPlane.KCP, controlplanev1.GroupVersion.WithKind(kubeadmControlPlaneKind)) clusterName := util.ObjectKey(controlPlane.Cluster) configSecret, err := secret.GetFromNamespacedName(ctx, r.SecretCachingClient, clusterName, secret.Kubeconfig) @@ -64,6 +63,7 @@ func (r *KubeadmControlPlaneReconciler) reconcileKubeconfig(ctx context.Context, clusterName, endpoint.String(), controllerOwnerRef, + kubeconfig.KeyEncryptionAlgorithm(controlPlane.GetKeyEncryptionAlgorithm()), ) if errors.Is(createErr, kubeconfig.ErrDependentCertificateNotFound) { return ctrl.Result{RequeueAfter: dependentCertRequeueAfter}, nil @@ -90,7 +90,7 @@ func (r *KubeadmControlPlaneReconciler) reconcileKubeconfig(ctx context.Context, if needsRotation { log.Info("Rotating kubeconfig secret") - if err := kubeconfig.RegenerateSecret(ctx, r.Client, configSecret); err != nil { + if err := kubeconfig.RegenerateSecret(ctx, r.Client, configSecret, kubeconfig.KeyEncryptionAlgorithm(controlPlane.GetKeyEncryptionAlgorithm())); err != nil { return ctrl.Result{}, errors.Wrap(err, "failed to regenerate kubeconfig") } } diff --git a/controlplane/kubeadm/internal/controllers/update.go b/controlplane/kubeadm/internal/controllers/update.go index a85076fddb8a..d84fd5d9c709 100644 --- a/controlplane/kubeadm/internal/controllers/update.go +++ b/controlplane/kubeadm/internal/controllers/update.go @@ -69,7 +69,8 @@ func (r *KubeadmControlPlaneReconciler) updateControlPlane( workloadCluster.UpdateAPIServerInKubeadmConfigMap(controlPlane.KCP.Spec.KubeadmConfigSpec.ClusterConfiguration.APIServer), workloadCluster.UpdateControllerManagerInKubeadmConfigMap(controlPlane.KCP.Spec.KubeadmConfigSpec.ClusterConfiguration.ControllerManager), workloadCluster.UpdateSchedulerInKubeadmConfigMap(controlPlane.KCP.Spec.KubeadmConfigSpec.ClusterConfiguration.Scheduler), - workloadCluster.UpdateCertificateValidityPeriodDays(controlPlane.KCP.Spec.KubeadmConfigSpec.ClusterConfiguration.CertificateValidityPeriodDays)) + workloadCluster.UpdateCertificateValidityPeriodDays(controlPlane.KCP.Spec.KubeadmConfigSpec.ClusterConfiguration.CertificateValidityPeriodDays), + workloadCluster.UpdateEncryptionAlgorithm(controlPlane.KCP.Spec.KubeadmConfigSpec.ClusterConfiguration.EncryptionAlgorithm)) // Etcd local and external are mutually exclusive and they cannot be switched, once set. if controlPlane.IsEtcdManaged() { diff --git a/controlplane/kubeadm/internal/filters_test.go b/controlplane/kubeadm/internal/filters_test.go index 1251b3697720..4423fcd46328 100644 --- a/controlplane/kubeadm/internal/filters_test.go +++ b/controlplane/kubeadm/internal/filters_test.go @@ -324,7 +324,7 @@ func TestMatchesKubeadmConfig(t *testing.T) { + CertificatesDir: "foo", ImageRepository: "", FeatureGates: nil, - ... // 2 identical fields + ... // 3 identical fields }, InitConfiguration: {NodeRegistration: {ImagePullPolicy: "IfNotPresent"}}, JoinConfiguration: {NodeRegistration: {ImagePullPolicy: "IfNotPresent"}}, @@ -1659,7 +1659,7 @@ func TestUpToDate(t *testing.T) { + CertificatesDir: "bar", ImageRepository: "", FeatureGates: nil, - ... // 2 identical fields + ... // 3 identical fields }, InitConfiguration: {NodeRegistration: {ImagePullPolicy: "IfNotPresent"}}, JoinConfiguration: {NodeRegistration: {ImagePullPolicy: "IfNotPresent"}}, diff --git a/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplane.go b/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplane.go index a132bd90f5a4..390268c57857 100644 --- a/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplane.go +++ b/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplane.go @@ -157,6 +157,7 @@ func (webhook *KubeadmControlPlane) ValidateUpdate(_ context.Context, oldObj, ne {spec, kubeadmConfigSpec, clusterConfiguration, scheduler}, {spec, kubeadmConfigSpec, clusterConfiguration, scheduler, "*"}, {spec, kubeadmConfigSpec, clusterConfiguration, "certificateValidityPeriodDays"}, + {spec, kubeadmConfigSpec, clusterConfiguration, "encryptionAlgorithm"}, // spec.kubeadmConfigSpec.initConfiguration {spec, kubeadmConfigSpec, initConfiguration, nodeRegistration}, {spec, kubeadmConfigSpec, initConfiguration, nodeRegistration, "*"}, diff --git a/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplane_test.go b/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplane_test.go index 1d738699dc93..27bec1e462e8 100644 --- a/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplane_test.go +++ b/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplane_test.go @@ -359,6 +359,7 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) { }, CertificateValidityPeriodDays: 100, CACertificateValidityPeriodDays: 365, + EncryptionAlgorithm: bootstrapv1.EncryptionAlgorithmRSA2048, }, JoinConfiguration: bootstrapv1.JoinConfiguration{ NodeRegistration: bootstrapv1.NodeRegistrationOptions{ @@ -755,6 +756,9 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) { CACertificateValidityPeriodDays: 730, } + validEncryptionAlgorithm := before.DeepCopy() + validEncryptionAlgorithm.Spec.KubeadmConfigSpec.ClusterConfiguration.EncryptionAlgorithm = bootstrapv1.EncryptionAlgorithmRSA3072 + tests := []struct { name string enableIgnitionFeature bool @@ -1118,6 +1122,11 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) { before: before, kcp: invalidUpdateCACertificateValidityPeriodDays, }, + { + name: "should allow to update encryptionAlgorithm", + before: before, + kcp: validEncryptionAlgorithm, + }, } for _, tt := range tests { diff --git a/controlplane/kubeadm/internal/workload_cluster.go b/controlplane/kubeadm/internal/workload_cluster.go index 657236f7e008..d4f6401664b9 100644 --- a/controlplane/kubeadm/internal/workload_cluster.go +++ b/controlplane/kubeadm/internal/workload_cluster.go @@ -20,7 +20,6 @@ import ( "context" "crypto" "crypto/rand" - "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" @@ -88,6 +87,7 @@ type WorkloadCluster interface { UpdateControllerManagerInKubeadmConfigMap(controllerManager bootstrapv1.ControllerManager) func(*bootstrapv1.ClusterConfiguration) UpdateSchedulerInKubeadmConfigMap(scheduler bootstrapv1.Scheduler) func(*bootstrapv1.ClusterConfiguration) UpdateCertificateValidityPeriodDays(certificateValidityPeriodDays int32) func(*bootstrapv1.ClusterConfiguration) + UpdateEncryptionAlgorithm(encryptionAlgorithm bootstrapv1.EncryptionAlgorithmType) func(*bootstrapv1.ClusterConfiguration) UpdateKubeProxyImageInfo(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane) error UpdateCoreDNS(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane) error RemoveEtcdMemberForMachine(ctx context.Context, machine *clusterv1.Machine) error @@ -195,6 +195,13 @@ func (w *Workload) UpdateCertificateValidityPeriodDays(certificateValidityPeriod } } +// UpdateEncryptionAlgorithm updates EncryptionAlgorithmType in kubeadm config map. +func (w *Workload) UpdateEncryptionAlgorithm(encryptionAlgorithm bootstrapv1.EncryptionAlgorithmType) func(*bootstrapv1.ClusterConfiguration) { + return func(c *bootstrapv1.ClusterConfiguration) { + c.EncryptionAlgorithm = encryptionAlgorithm + } +} + // UpdateClusterConfiguration gets the ClusterConfiguration kubeadm-config ConfigMap, converts it to the // Cluster API representation, and then applies a mutation func; if changes are detected, the // data are converted back into the Kubeadm API version in use for the target Kubernetes version and the @@ -347,7 +354,7 @@ func calculateAPIServerPort(config *bootstrapv1.KubeadmConfig) int32 { return 6443 } -func generateClientCert(caCertEncoded, caKeyEncoded []byte) (tls.Certificate, error) { +func generateClientCert(caCertEncoded, caKeyEncoded []byte, keyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType) (tls.Certificate, error) { caCert, err := certs.DecodeCertPEM(caCertEncoded) if err != nil { return tls.Certificate{}, err @@ -356,7 +363,8 @@ func generateClientCert(caCertEncoded, caKeyEncoded []byte) (tls.Certificate, er if err != nil { return tls.Certificate{}, err } - clientKey, err := certs.NewPrivateKey() + + clientKey, err := certs.NewSigner(keyEncryptionAlgorithm) if err != nil { return tls.Certificate{}, err } @@ -364,10 +372,15 @@ func generateClientCert(caCertEncoded, caKeyEncoded []byte) (tls.Certificate, er if err != nil { return tls.Certificate{}, err } - return tls.X509KeyPair(certs.EncodeCertPEM(x509Cert), certs.EncodePrivateKeyPEM(clientKey)) + encodedClientKey, err := certs.EncodePrivateKeyPEMFromSigner(clientKey) + if err != nil { + return tls.Certificate{}, err + } + + return tls.X509KeyPair(certs.EncodeCertPEM(x509Cert), encodedClientKey) } -func newClientCert(caCert *x509.Certificate, key *rsa.PrivateKey, caKey crypto.Signer) (*x509.Certificate, error) { +func newClientCert(caCert *x509.Certificate, key crypto.Signer, caKey crypto.Signer) (*x509.Certificate, error) { cfg := certs.Config{ CommonName: "cluster-api.x-k8s.io", } diff --git a/internal/api/bootstrap/kubeadm/v1alpha3/conversion.go b/internal/api/bootstrap/kubeadm/v1alpha3/conversion.go index c87e37847dc7..495122163ad7 100644 --- a/internal/api/bootstrap/kubeadm/v1alpha3/conversion.go +++ b/internal/api/bootstrap/kubeadm/v1alpha3/conversion.go @@ -100,6 +100,7 @@ func RestoreKubeadmConfigSpec(dst *bootstrapv1.KubeadmConfigSpec, restored *boot dst.ClusterConfiguration.Scheduler.ExtraEnvs = restored.ClusterConfiguration.Scheduler.ExtraEnvs dst.ClusterConfiguration.CertificateValidityPeriodDays = restored.ClusterConfiguration.CertificateValidityPeriodDays dst.ClusterConfiguration.CACertificateValidityPeriodDays = restored.ClusterConfiguration.CACertificateValidityPeriodDays + dst.ClusterConfiguration.EncryptionAlgorithm = restored.ClusterConfiguration.EncryptionAlgorithm dst.ClusterConfiguration.Etcd.Local.ExtraEnvs = restored.ClusterConfiguration.Etcd.Local.ExtraEnvs dst.InitConfiguration.Timeouts = restored.InitConfiguration.Timeouts diff --git a/internal/api/bootstrap/kubeadm/v1alpha3/zz_generated.conversion.go b/internal/api/bootstrap/kubeadm/v1alpha3/zz_generated.conversion.go index 77b59e6b8fb8..3c0ad05f900e 100644 --- a/internal/api/bootstrap/kubeadm/v1alpha3/zz_generated.conversion.go +++ b/internal/api/bootstrap/kubeadm/v1alpha3/zz_generated.conversion.go @@ -561,6 +561,7 @@ func autoConvert_v1beta2_ClusterConfiguration_To_v1alpha3_ClusterConfiguration(i out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) // WARNING: in.CertificateValidityPeriodDays requires manual conversion: does not exist in peer-type // WARNING: in.CACertificateValidityPeriodDays requires manual conversion: does not exist in peer-type + // WARNING: in.EncryptionAlgorithm requires manual conversion: does not exist in peer-type return nil } diff --git a/internal/api/bootstrap/kubeadm/v1alpha4/conversion.go b/internal/api/bootstrap/kubeadm/v1alpha4/conversion.go index 54a1ef93fada..7e9a833105a1 100644 --- a/internal/api/bootstrap/kubeadm/v1alpha4/conversion.go +++ b/internal/api/bootstrap/kubeadm/v1alpha4/conversion.go @@ -100,6 +100,7 @@ func RestoreKubeadmConfigSpec(dst *bootstrapv1.KubeadmConfigSpec, restored *boot dst.ClusterConfiguration.Scheduler.ExtraEnvs = restored.ClusterConfiguration.Scheduler.ExtraEnvs dst.ClusterConfiguration.CertificateValidityPeriodDays = restored.ClusterConfiguration.CertificateValidityPeriodDays dst.ClusterConfiguration.CACertificateValidityPeriodDays = restored.ClusterConfiguration.CACertificateValidityPeriodDays + dst.ClusterConfiguration.EncryptionAlgorithm = restored.ClusterConfiguration.EncryptionAlgorithm dst.ClusterConfiguration.Etcd.Local.ExtraEnvs = restored.ClusterConfiguration.Etcd.Local.ExtraEnvs dst.InitConfiguration.Timeouts = restored.InitConfiguration.Timeouts diff --git a/internal/api/bootstrap/kubeadm/v1alpha4/zz_generated.conversion.go b/internal/api/bootstrap/kubeadm/v1alpha4/zz_generated.conversion.go index 314c9b9e7e85..c15764f46920 100644 --- a/internal/api/bootstrap/kubeadm/v1alpha4/zz_generated.conversion.go +++ b/internal/api/bootstrap/kubeadm/v1alpha4/zz_generated.conversion.go @@ -560,6 +560,7 @@ func autoConvert_v1beta2_ClusterConfiguration_To_v1alpha4_ClusterConfiguration(i out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) // WARNING: in.CertificateValidityPeriodDays requires manual conversion: does not exist in peer-type // WARNING: in.CACertificateValidityPeriodDays requires manual conversion: does not exist in peer-type + // WARNING: in.EncryptionAlgorithm requires manual conversion: does not exist in peer-type return nil } diff --git a/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start-runtimesdk.yaml b/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start-runtimesdk.yaml index f684b31ba45a..7569ca4b7c94 100644 --- a/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start-runtimesdk.yaml +++ b/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start-runtimesdk.yaml @@ -99,6 +99,7 @@ spec: apiServer: # host.docker.internal is required by kubetest when running on MacOS because of the way ports are proxied. certSANs: [localhost, 127.0.0.1, 0.0.0.0, host.docker.internal] + encryptionAlgorithm: "RSA-4096" --- apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 kind: DockerMachineTemplate diff --git a/util/certs/certs.go b/util/certs/certs.go index 225438bb4e49..71c6e2436a59 100644 --- a/util/certs/certs.go +++ b/util/certs/certs.go @@ -19,13 +19,18 @@ package certs import ( "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" + "fmt" "github.com/pkg/errors" kerrors "k8s.io/apimachinery/pkg/util/errors" + + bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2" ) // NewPrivateKey creates an RSA private key. @@ -113,3 +118,72 @@ func DecodePrivateKeyPEM(encoded []byte) (crypto.Signer, error) { return nil, kerrors.NewAggregate(errs) } + +// NewSigner creates a private key based on the provided encryption key algorithm. +func NewSigner(keyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType) (crypto.Signer, error) { + switch keyEncryptionAlgorithm { + case bootstrapv1.EncryptionAlgorithmECDSAP256: + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case bootstrapv1.EncryptionAlgorithmECDSAP384: + return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + } + rsaKeySize := rsaKeySizeFromAlgorithmType(keyEncryptionAlgorithm) + if rsaKeySize == 0 { + return nil, errors.Errorf("cannot obtain key size from unknown RSA algorithm: %q", keyEncryptionAlgorithm) + } + return rsa.GenerateKey(rand.Reader, rsaKeySize) +} + +// EncodePrivateKeyPEMFromSigner converts a known private key type of RSA or ECDSA to +// a PEM encoded block or returns an error. +func EncodePrivateKeyPEMFromSigner(key crypto.PrivateKey) ([]byte, error) { + switch t := key.(type) { + case *ecdsa.PrivateKey: + derBytes, err := x509.MarshalECPrivateKey(t) + if err != nil { + return nil, err + } + block := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: derBytes, + } + return pem.EncodeToMemory(block), nil + case *rsa.PrivateKey: + block := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(t), + } + return pem.EncodeToMemory(block), nil + default: + return nil, fmt.Errorf("private key is not a recognized type: %T", key) + } +} + +// EncodePublicKeyPEMFromSigner returns PEM-encoded public key data. +func EncodePublicKeyPEMFromSigner(key crypto.PublicKey) ([]byte, error) { + der, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return []byte{}, err + } + block := pem.Block{ + Type: "PUBLIC KEY", + Bytes: der, + } + return pem.EncodeToMemory(&block), nil +} + +// rsaKeySizeFromAlgorithmType takes a known RSA algorithm defined in the kubeadm API and returns its key size. +// For unknown types it returns 0. +// For an empty type ("") which is the default (zero value) on the API field it returns the default size of 2048. +func rsaKeySizeFromAlgorithmType(keyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType) int { + switch keyEncryptionAlgorithm { + case bootstrapv1.EncryptionAlgorithmRSA2048, "": + return 2048 + case bootstrapv1.EncryptionAlgorithmRSA3072: + return 3072 + case bootstrapv1.EncryptionAlgorithmRSA4096: + return 4096 + default: + return 0 + } +} diff --git a/util/certs/types.go b/util/certs/types.go index 60398468723f..462b781b7fc6 100644 --- a/util/certs/types.go +++ b/util/certs/types.go @@ -19,7 +19,6 @@ package certs import ( "crypto" "crypto/rand" - "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "math" @@ -49,7 +48,7 @@ type Config struct { } // NewSignedCert creates a signed certificate using the given CA certificate and key. -func (cfg *Config) NewSignedCert(key *rsa.PrivateKey, caCert *x509.Certificate, caKey crypto.Signer) (*x509.Certificate, error) { +func (cfg *Config) NewSignedCert(key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer) (*x509.Certificate, error) { serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) if err != nil { return nil, errors.Wrap(err, "failed to generate random integer for signed cerficate") diff --git a/util/kubeconfig/kubeconfig.go b/util/kubeconfig/kubeconfig.go index b93c07d03af9..a2087f6d1b1a 100644 --- a/util/kubeconfig/kubeconfig.go +++ b/util/kubeconfig/kubeconfig.go @@ -54,14 +54,17 @@ func FromSecret(ctx context.Context, c client.Reader, cluster client.ObjectKey) } // New creates a new Kubeconfig using the cluster name and specified endpoint. -func New(clusterName, endpoint string, caCert *x509.Certificate, caKey crypto.Signer) (*api.Config, error) { +func New(clusterName, endpoint string, caCert *x509.Certificate, caKey crypto.Signer, options ...KubeConfigOption) (*api.Config, error) { cfg := &certs.Config{ CommonName: "kubernetes-admin", Organization: []string{"system:masters"}, Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } - clientKey, err := certs.NewPrivateKey() + kubeConfigOptions := &KubeConfigOptions{} + kubeConfigOptions.ApplyOptions(options) + + clientKey, err := certs.NewSigner(kubeConfigOptions.keyEncryptionAlgorithm) if err != nil { return nil, errors.Wrap(err, "unable to create private key") } @@ -71,6 +74,11 @@ func New(clusterName, endpoint string, caCert *x509.Certificate, caKey crypto.Si return nil, errors.Wrap(err, "unable to sign certificate") } + encodedClientKey, err := certs.EncodePrivateKeyPEMFromSigner(clientKey) + if err != nil { + return nil, errors.Wrap(err, "unable to encode private key") + } + userName := fmt.Sprintf("%s-admin", clusterName) contextName := fmt.Sprintf("%s@%s", userName, clusterName) @@ -89,7 +97,7 @@ func New(clusterName, endpoint string, caCert *x509.Certificate, caKey crypto.Si }, AuthInfos: map[string]*api.AuthInfo{ userName: { - ClientKeyData: certs.EncodePrivateKeyPEM(clientKey), + ClientKeyData: encodedClientKey, ClientCertificateData: certs.EncodeCertPEM(clientCert), }, }, @@ -98,23 +106,23 @@ func New(clusterName, endpoint string, caCert *x509.Certificate, caKey crypto.Si } // CreateSecret creates the Kubeconfig secret for the given cluster. -func CreateSecret(ctx context.Context, c client.Client, cluster *clusterv1.Cluster) error { +func CreateSecret(ctx context.Context, c client.Client, cluster *clusterv1.Cluster, options ...KubeConfigOption) error { name := util.ObjectKey(cluster) return CreateSecretWithOwner(ctx, c, name, cluster.Spec.ControlPlaneEndpoint.String(), metav1.OwnerReference{ APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", Name: cluster.Name, UID: cluster.UID, - }) + }, options...) } // CreateSecretWithOwner creates the Kubeconfig secret for the given cluster name, namespace, endpoint, and owner reference. -func CreateSecretWithOwner(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string, owner metav1.OwnerReference) error { +func CreateSecretWithOwner(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string, owner metav1.OwnerReference, options ...KubeConfigOption) error { server, err := url.JoinPath("https://", endpoint) if err != nil { return err } - out, err := generateKubeconfig(ctx, c, clusterName, server) + out, err := generateKubeconfig(ctx, c, clusterName, server, options...) if err != nil { return err } @@ -181,7 +189,7 @@ func NeedsClientCertRotation(configSecret *corev1.Secret, threshold time.Duratio } // RegenerateSecret creates and stores a new Kubeconfig in the given secret. -func RegenerateSecret(ctx context.Context, c client.Client, configSecret *corev1.Secret) error { +func RegenerateSecret(ctx context.Context, c client.Client, configSecret *corev1.Secret, options ...KubeConfigOption) error { clusterName, _, err := secret.ParseSecretName(configSecret.Name) if err != nil { return errors.Wrap(err, "failed to parse secret name") @@ -197,7 +205,7 @@ func RegenerateSecret(ctx context.Context, c client.Client, configSecret *corev1 } endpoint := config.Clusters[clusterName].Server key := client.ObjectKey{Name: clusterName, Namespace: configSecret.Namespace} - out, err := generateKubeconfig(ctx, c, key, endpoint) + out, err := generateKubeconfig(ctx, c, key, endpoint, options...) if err != nil { return err } @@ -205,7 +213,7 @@ func RegenerateSecret(ctx context.Context, c client.Client, configSecret *corev1 return c.Update(ctx, configSecret) } -func generateKubeconfig(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string) ([]byte, error) { +func generateKubeconfig(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string, options ...KubeConfigOption) ([]byte, error) { clusterCA, err := secret.GetFromNamespacedName(ctx, c, clusterName, secret.ClusterCA) if err != nil { if apierrors.IsNotFound(err) { @@ -228,7 +236,7 @@ func generateKubeconfig(ctx context.Context, c client.Client, clusterName client return nil, errors.New("CA private key not found") } - cfg, err := New(clusterName.Name, endpoint, cert, key) + cfg, err := New(clusterName.Name, endpoint, cert, key, options...) if err != nil { return nil, errors.Wrap(err, "failed to generate a kubeconfig") } diff --git a/util/kubeconfig/options.go b/util/kubeconfig/options.go new file mode 100644 index 000000000000..bbeaee3a2498 --- /dev/null +++ b/util/kubeconfig/options.go @@ -0,0 +1,46 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubeconfig + +import bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2" + +// KubeConfigOption helps to modify KubeConfigOptions. +type KubeConfigOption interface { //nolint:revive + // ApplyKubeConfigOption applies this options to the given KubeConfigOptions options. + ApplyKubeConfigOption(*KubeConfigOptions) +} + +// KubeConfigOptions allows to set options for generating a kubeconfig. +type KubeConfigOptions struct { //nolint:revive + keyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType +} + +// ApplyOptions applies the given list options on these options, +// and then returns itself (for convenient chaining). +func (o *KubeConfigOptions) ApplyOptions(opts []KubeConfigOption) { + for _, opt := range opts { + opt.ApplyKubeConfigOption(o) + } +} + +// KeyEncryptionAlgorithm allows to specify the key encryption algorithm type. +type KeyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType + +// ApplyKubeConfigOption applies this configuration to the given kube configuration options. +func (t KeyEncryptionAlgorithm) ApplyKubeConfigOption(opts *KubeConfigOptions) { + opts.keyEncryptionAlgorithm = bootstrapv1.EncryptionAlgorithmType(t) +} diff --git a/util/secret/certificates.go b/util/secret/certificates.go index 8af1c12eecf3..b8dc6405fe8b 100644 --- a/util/secret/certificates.go +++ b/util/secret/certificates.go @@ -18,8 +18,8 @@ package secret import ( "context" + "crypto" "crypto/rand" - "crypto/rsa" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" @@ -69,6 +69,7 @@ type Certificates []*Certificate // NewCertificatesForInitialControlPlane returns a list of certificates configured for a control plane node. func NewCertificatesForInitialControlPlane(config *bootstrapv1.ClusterConfiguration) Certificates { var validityPeriodDays int32 + var keyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType certificatesDir := DefaultCertificatesDir if config != nil { if config.CertificatesDir != "" { @@ -77,34 +78,41 @@ func NewCertificatesForInitialControlPlane(config *bootstrapv1.ClusterConfigurat if config.CACertificateValidityPeriodDays != 0 { validityPeriodDays = config.CACertificateValidityPeriodDays } + if config.EncryptionAlgorithm != "" { + keyEncryptionAlgorithm = config.EncryptionAlgorithm + } } certificates := Certificates{ &Certificate{ - Purpose: ClusterCA, - CertFile: path.Join(certificatesDir, "ca.crt"), - KeyFile: path.Join(certificatesDir, "ca.key"), - ValidityPeriodDays: validityPeriodDays, + Purpose: ClusterCA, + CertFile: path.Join(certificatesDir, "ca.crt"), + KeyFile: path.Join(certificatesDir, "ca.key"), + ValidityPeriodDays: validityPeriodDays, + KeyEncryptionAlgorithm: keyEncryptionAlgorithm, }, &Certificate{ - Purpose: ServiceAccount, - CertFile: path.Join(certificatesDir, "sa.pub"), - KeyFile: path.Join(certificatesDir, "sa.key"), - ValidityPeriodDays: validityPeriodDays, + Purpose: ServiceAccount, + CertFile: path.Join(certificatesDir, "sa.pub"), + KeyFile: path.Join(certificatesDir, "sa.key"), + ValidityPeriodDays: validityPeriodDays, + KeyEncryptionAlgorithm: keyEncryptionAlgorithm, }, &Certificate{ - Purpose: FrontProxyCA, - CertFile: path.Join(certificatesDir, "front-proxy-ca.crt"), - KeyFile: path.Join(certificatesDir, "front-proxy-ca.key"), - ValidityPeriodDays: validityPeriodDays, + Purpose: FrontProxyCA, + CertFile: path.Join(certificatesDir, "front-proxy-ca.crt"), + KeyFile: path.Join(certificatesDir, "front-proxy-ca.key"), + ValidityPeriodDays: validityPeriodDays, + KeyEncryptionAlgorithm: keyEncryptionAlgorithm, }, } etcdCert := &Certificate{ - Purpose: EtcdCA, - CertFile: path.Join(certificatesDir, "etcd", "ca.crt"), - KeyFile: path.Join(certificatesDir, "etcd", "ca.key"), - ValidityPeriodDays: validityPeriodDays, + Purpose: EtcdCA, + CertFile: path.Join(certificatesDir, "etcd", "ca.crt"), + KeyFile: path.Join(certificatesDir, "etcd", "ca.key"), + ValidityPeriodDays: validityPeriodDays, + KeyEncryptionAlgorithm: keyEncryptionAlgorithm, } // TODO make sure all the fields are actually defined and return an error if not @@ -114,13 +122,13 @@ func NewCertificatesForInitialControlPlane(config *bootstrapv1.ClusterConfigurat CertFile: config.Etcd.External.CAFile, External: true, } - apiserverEtcdClientCert := &Certificate{ + apiServerEtcdClientCert := &Certificate{ Purpose: APIServerEtcdClient, CertFile: config.Etcd.External.CertFile, KeyFile: config.Etcd.External.KeyFile, External: true, } - certificates = append(certificates, apiserverEtcdClientCert) + certificates = append(certificates, apiServerEtcdClientCert) } certificates = append(certificates, etcdCert) @@ -331,13 +339,14 @@ func (c Certificates) LookupOrGenerateCached(ctx context.Context, secretCachingC // Certificate represents a single certificate CA. type Certificate struct { - Generated bool - External bool - Purpose Purpose - KeyPair *certs.KeyPair - CertFile, KeyFile string - Secret *corev1.Secret - ValidityPeriodDays int32 + Generated bool + External bool + Purpose Purpose + KeyPair *certs.KeyPair + CertFile, KeyFile string + Secret *corev1.Secret + ValidityPeriodDays int32 + KeyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType } // Hashes hashes all the certificates stored in a CA certificate. @@ -420,7 +429,7 @@ func (c *Certificate) Generate() error { generator = generateServiceAccountKeys } - kp, err := generator(c.ValidityPeriodDays) + kp, err := generator(c.ValidityPeriodDays, c.KeyEncryptionAlgorithm) if err != nil { return err } @@ -473,35 +482,44 @@ func secretToKeyPair(s *corev1.Secret) (*certs.KeyPair, error) { }, nil } -func generateCACert(validityPeriodDays int32) (*certs.KeyPair, error) { - x509Cert, privKey, err := newCertificateAuthority(validityPeriodDays) +func generateCACert(validityPeriodDays int32, keyAlgorithmType bootstrapv1.EncryptionAlgorithmType) (*certs.KeyPair, error) { + x509Cert, privateKey, err := newCertificateAuthority(validityPeriodDays, keyAlgorithmType) + if err != nil { + return nil, err + } + encodedKey, err := certs.EncodePrivateKeyPEMFromSigner(privateKey) if err != nil { return nil, err } return &certs.KeyPair{ Cert: certs.EncodeCertPEM(x509Cert), - Key: certs.EncodePrivateKeyPEM(privKey), + Key: encodedKey, }, nil } -func generateServiceAccountKeys(_ int32) (*certs.KeyPair, error) { - saCreds, err := certs.NewPrivateKey() +func generateServiceAccountKeys(_ int32, keyEncryptionAlgorithm bootstrapv1.EncryptionAlgorithmType) (*certs.KeyPair, error) { + saCreds, err := certs.NewSigner(keyEncryptionAlgorithm) if err != nil { return nil, err } - saPub, err := certs.EncodePublicKeyPEM(&saCreds.PublicKey) + saPub, err := certs.EncodePublicKeyPEMFromSigner(saCreds.Public()) if err != nil { return nil, err } + saKey, err := certs.EncodePrivateKeyPEMFromSigner(saCreds) + if err != nil { + return nil, err + } + return &certs.KeyPair{ Cert: saPub, - Key: certs.EncodePrivateKeyPEM(saCreds), + Key: saKey, }, nil } // newCertificateAuthority creates new certificate and private key for the certificate authority. -func newCertificateAuthority(validityPeriodDays int32) (*x509.Certificate, *rsa.PrivateKey, error) { - key, err := certs.NewPrivateKey() +func newCertificateAuthority(validityPeriodDays int32, keyAlgorithmType bootstrapv1.EncryptionAlgorithmType) (*x509.Certificate, crypto.Signer, error) { + key, err := certs.NewSigner(keyAlgorithmType) if err != nil { return nil, nil, err } @@ -515,7 +533,7 @@ func newCertificateAuthority(validityPeriodDays int32) (*x509.Certificate, *rsa. } // newSelfSignedCACert creates a CA certificate. -func newSelfSignedCACert(key *rsa.PrivateKey, validityPeriodDays int32) (*x509.Certificate, error) { +func newSelfSignedCACert(key crypto.Signer, validityPeriodDays int32) (*x509.Certificate, error) { cfg := certs.Config{ CommonName: "kubernetes", }