diff --git a/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go b/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go index cbbf5d84834..83529471c3b 100644 --- a/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go +++ b/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go @@ -26,6 +26,7 @@ import ( bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + "k8s.io/kubernetes/cmd/kubeadm/app/constants" ) // Funcs returns the fuzzer functions for the kubeadm apis. @@ -97,6 +98,8 @@ func fuzzClusterConfiguration(obj *kubeadm.ClusterConfiguration, c fuzz.Continue obj.Etcd.Local.ExtraEnvs = []kubeadm.EnvVar{} obj.EncryptionAlgorithm = kubeadm.EncryptionAlgorithmRSA2048 obj.Proxy.Disabled = false + obj.CertificateValidityPeriod = &metav1.Duration{Duration: constants.CertificateValidityPeriod} + obj.CACertificateValidityPeriod = &metav1.Duration{Duration: constants.CACertificateValidityPeriod} } func fuzzDNS(obj *kubeadm.DNS, c fuzz.Continue) { diff --git a/cmd/kubeadm/app/apis/kubeadm/types.go b/cmd/kubeadm/app/apis/kubeadm/types.go index 440c75d7a15..82d89bf086a 100644 --- a/cmd/kubeadm/app/apis/kubeadm/types.go +++ b/cmd/kubeadm/app/apis/kubeadm/types.go @@ -149,6 +149,14 @@ type ClusterConfiguration struct { // EncryptionAlgorithm holds the type of asymmetric encryption algorithm used for keys and certificates. // Can be one of "RSA-2048" (default), "RSA-3072", "RSA-4096" or "ECDSA-P256". EncryptionAlgorithm EncryptionAlgorithmType + + // CertificateValidityPeriod specifies the validity period for a non-CA certificate generated by kubeadm. + // Default value: 8760h (365 days * 24 hours = 1 year) + CertificateValidityPeriod *metav1.Duration + + // CACertificateValidityPeriod specifies the validity period for a CA certificate generated by kubeadm. + // Default value: 87600h (365 days * 24 hours * 10 = 10 years) + CACertificateValidityPeriod *metav1.Duration } // ControlPlaneComponent holds settings common to control plane component of the cluster diff --git a/cmd/kubeadm/app/apis/kubeadm/v1beta3/conversion.go b/cmd/kubeadm/app/apis/kubeadm/v1beta3/conversion.go index b5c655d6099..10775424924 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1beta3/conversion.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1beta3/conversion.go @@ -19,10 +19,12 @@ package v1beta3 import ( "sort" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" "k8s.io/utils/ptr" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + "k8s.io/kubernetes/cmd/kubeadm/app/constants" ) // Convert_kubeadm_InitConfiguration_To_v1beta3_InitConfiguration converts a private InitConfiguration to public InitConfiguration. @@ -38,9 +40,11 @@ func Convert_v1beta3_InitConfiguration_To_kubeadm_InitConfiguration(in *InitConf } err = Convert_v1beta3_ClusterConfiguration_To_kubeadm_ClusterConfiguration(&ClusterConfiguration{}, &out.ClusterConfiguration, s) // Required to pass fuzzer tests. This ClusterConfiguration is empty and is never defaulted. - // If we call Convert_v1beta3_ClusterConfiguration_To_kubeadm_ClusterConfiguration() it will receive - // a default value, thus here we need to reset it back to "". + // If we call Convert_v1beta3_ClusterConfiguration_To_kubeadm_ClusterConfiguration() these fields will receive + // a default value, thus here we need to reset them back to empty. out.EncryptionAlgorithm = "" + out.CertificateValidityPeriod = nil + out.CACertificateValidityPeriod = nil // Set default timeouts. kubeadm.SetDefaultTimeouts(&out.Timeouts) return err @@ -64,9 +68,11 @@ func Convert_kubeadm_ClusterConfiguration_To_v1beta3_ClusterConfiguration(in *ku // Convert_v1beta3_ClusterConfiguration_To_kubeadm_ClusterConfiguration is required due to missing EncryptionAlgorithm in v1beta3. func Convert_v1beta3_ClusterConfiguration_To_kubeadm_ClusterConfiguration(in *ClusterConfiguration, out *kubeadm.ClusterConfiguration, s conversion.Scope) error { - // Required to pass validation and fuzzer tests. The field is missing in v1beta3, thus we have to - // default it to a sane (default) value in the internal type. + // Required to pass validation and fuzzer tests. These fields are missing in v1beta3, thus we have to + // default them to a sane (default) value in the internal type. out.EncryptionAlgorithm = kubeadm.EncryptionAlgorithmRSA2048 + out.CertificateValidityPeriod = &metav1.Duration{Duration: constants.CertificateValidityPeriod} + out.CACertificateValidityPeriod = &metav1.Duration{Duration: constants.CACertificateValidityPeriod} return autoConvert_v1beta3_ClusterConfiguration_To_kubeadm_ClusterConfiguration(in, out, s) } diff --git a/cmd/kubeadm/app/apis/kubeadm/v1beta3/zz_generated.conversion.go b/cmd/kubeadm/app/apis/kubeadm/v1beta3/zz_generated.conversion.go index 39c0bc82264..3e8c3f69eb3 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1beta3/zz_generated.conversion.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1beta3/zz_generated.conversion.go @@ -361,6 +361,8 @@ func autoConvert_kubeadm_ClusterConfiguration_To_v1beta3_ClusterConfiguration(in out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) out.ClusterName = in.ClusterName // WARNING: in.EncryptionAlgorithm requires manual conversion: does not exist in peer-type + // 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 } diff --git a/cmd/kubeadm/app/apis/kubeadm/v1beta4/defaults.go b/cmd/kubeadm/app/apis/kubeadm/v1beta4/defaults.go index 713b8ca0c8a..a7259910962 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1beta4/defaults.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1beta4/defaults.go @@ -108,6 +108,17 @@ func SetDefaults_ClusterConfiguration(obj *ClusterConfiguration) { obj.EncryptionAlgorithm = DefaultEncryptionAlgorithm } + if obj.CertificateValidityPeriod == nil { + obj.CertificateValidityPeriod = &metav1.Duration{ + Duration: constants.CertificateValidityPeriod, + } + } + if obj.CACertificateValidityPeriod == nil { + obj.CACertificateValidityPeriod = &metav1.Duration{ + Duration: constants.CACertificateValidityPeriod, + } + } + SetDefaults_Etcd(obj) } diff --git a/cmd/kubeadm/app/apis/kubeadm/v1beta4/doc.go b/cmd/kubeadm/app/apis/kubeadm/v1beta4/doc.go index e1bd96ff5ff..d4c81f1a717 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1beta4/doc.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1beta4/doc.go @@ -44,6 +44,10 @@ limitations under the License. // and will be ignored when passing --config to upgrade subcommands. // - Add a `Timeouts` structure to `InitConfiguration`, `JoinConfiguration`, `ResetConfiguration` and `UpgradeConfiguration` // that can be used to configure various timeouts. +// - Add a `CertificateValidityPeriod` and `CACertificateValidityPeriod` fields to `ClusterConfiguration`. These fields +// can be used to control the validity period of certificates generated by kubeadm during sub-commands such as `init`, +// `join`, `upgrade` and `certs`. Default values continue to be 1 year for non-CA certificates and 10 years for CA +// certificates. Only non-CA certificates continue to be renewable by `kubeadm certs renew`. // // Migration from old kubeadm config versions // diff --git a/cmd/kubeadm/app/apis/kubeadm/v1beta4/types.go b/cmd/kubeadm/app/apis/kubeadm/v1beta4/types.go index 0da59a7c1e4..07a084389ff 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1beta4/types.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1beta4/types.go @@ -152,6 +152,16 @@ type ClusterConfiguration struct { // Can be one of "RSA-2048" (default), "RSA-3072", "RSA-4096" or "ECDSA-P256". // +optional EncryptionAlgorithm EncryptionAlgorithmType `json:"encryptionAlgorithm,omitempty"` + + // CertificateValidityPeriod specifies the validity period for a non-CA certificate generated by kubeadm. + // Default value: 8760h (365 days * 24 hours = 1 year) + // +optional + CertificateValidityPeriod *metav1.Duration `json:"certificateValidityPeriod,omitempty"` + + // CACertificateValidityPeriod specifies the validity period for a CA certificate generated by kubeadm. + // Default value: 87600h (365 days * 24 hours * 10 = 10 years) + // +optional + CACertificateValidityPeriod *metav1.Duration `json:"caCertificateValidityPeriod,omitempty"` } // ControlPlaneComponent holds settings common to control plane component of the cluster diff --git a/cmd/kubeadm/app/apis/kubeadm/v1beta4/zz_generated.conversion.go b/cmd/kubeadm/app/apis/kubeadm/v1beta4/zz_generated.conversion.go index 12d871f64ac..9c9d3b6ac1e 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1beta4/zz_generated.conversion.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1beta4/zz_generated.conversion.go @@ -24,8 +24,8 @@ package v1beta4 import ( unsafe "unsafe" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" @@ -453,6 +453,8 @@ func autoConvert_v1beta4_ClusterConfiguration_To_kubeadm_ClusterConfiguration(in out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) out.ClusterName = in.ClusterName out.EncryptionAlgorithm = kubeadm.EncryptionAlgorithmType(in.EncryptionAlgorithm) + out.CertificateValidityPeriod = (*v1.Duration)(unsafe.Pointer(in.CertificateValidityPeriod)) + out.CACertificateValidityPeriod = (*v1.Duration)(unsafe.Pointer(in.CACertificateValidityPeriod)) return nil } @@ -488,6 +490,8 @@ func autoConvert_kubeadm_ClusterConfiguration_To_v1beta4_ClusterConfiguration(in out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) out.ClusterName = in.ClusterName out.EncryptionAlgorithm = EncryptionAlgorithmType(in.EncryptionAlgorithm) + out.CertificateValidityPeriod = (*v1.Duration)(unsafe.Pointer(in.CertificateValidityPeriod)) + out.CACertificateValidityPeriod = (*v1.Duration)(unsafe.Pointer(in.CACertificateValidityPeriod)) return nil } @@ -654,7 +658,7 @@ func autoConvert_v1beta4_HostPathMount_To_kubeadm_HostPathMount(in *HostPathMoun out.HostPath = in.HostPath out.MountPath = in.MountPath out.ReadOnly = in.ReadOnly - out.PathType = v1.HostPathType(in.PathType) + out.PathType = corev1.HostPathType(in.PathType) return nil } @@ -668,7 +672,7 @@ func autoConvert_kubeadm_HostPathMount_To_v1beta4_HostPathMount(in *kubeadm.Host out.HostPath = in.HostPath out.MountPath = in.MountPath out.ReadOnly = in.ReadOnly - out.PathType = v1.HostPathType(in.PathType) + out.PathType = corev1.HostPathType(in.PathType) return nil } @@ -856,10 +860,10 @@ func Convert_kubeadm_Networking_To_v1beta4_Networking(in *kubeadm.Networking, ou func autoConvert_v1beta4_NodeRegistrationOptions_To_kubeadm_NodeRegistrationOptions(in *NodeRegistrationOptions, out *kubeadm.NodeRegistrationOptions, s conversion.Scope) error { out.Name = in.Name out.CRISocket = in.CRISocket - out.Taints = *(*[]v1.Taint)(unsafe.Pointer(&in.Taints)) + out.Taints = *(*[]corev1.Taint)(unsafe.Pointer(&in.Taints)) out.KubeletExtraArgs = *(*[]kubeadm.Arg)(unsafe.Pointer(&in.KubeletExtraArgs)) out.IgnorePreflightErrors = *(*[]string)(unsafe.Pointer(&in.IgnorePreflightErrors)) - out.ImagePullPolicy = v1.PullPolicy(in.ImagePullPolicy) + out.ImagePullPolicy = corev1.PullPolicy(in.ImagePullPolicy) out.ImagePullSerial = (*bool)(unsafe.Pointer(in.ImagePullSerial)) return nil } @@ -872,10 +876,10 @@ func Convert_v1beta4_NodeRegistrationOptions_To_kubeadm_NodeRegistrationOptions( func autoConvert_kubeadm_NodeRegistrationOptions_To_v1beta4_NodeRegistrationOptions(in *kubeadm.NodeRegistrationOptions, out *NodeRegistrationOptions, s conversion.Scope) error { out.Name = in.Name out.CRISocket = in.CRISocket - out.Taints = *(*[]v1.Taint)(unsafe.Pointer(&in.Taints)) + out.Taints = *(*[]corev1.Taint)(unsafe.Pointer(&in.Taints)) out.KubeletExtraArgs = *(*[]Arg)(unsafe.Pointer(&in.KubeletExtraArgs)) out.IgnorePreflightErrors = *(*[]string)(unsafe.Pointer(&in.IgnorePreflightErrors)) - out.ImagePullPolicy = v1.PullPolicy(in.ImagePullPolicy) + out.ImagePullPolicy = corev1.PullPolicy(in.ImagePullPolicy) out.ImagePullSerial = (*bool)(unsafe.Pointer(in.ImagePullSerial)) return nil } @@ -962,13 +966,13 @@ func Convert_kubeadm_ResetConfiguration_To_v1beta4_ResetConfiguration(in *kubead } func autoConvert_v1beta4_Timeouts_To_kubeadm_Timeouts(in *Timeouts, out *kubeadm.Timeouts, s conversion.Scope) error { - out.ControlPlaneComponentHealthCheck = (*metav1.Duration)(unsafe.Pointer(in.ControlPlaneComponentHealthCheck)) - out.KubeletHealthCheck = (*metav1.Duration)(unsafe.Pointer(in.KubeletHealthCheck)) - out.KubernetesAPICall = (*metav1.Duration)(unsafe.Pointer(in.KubernetesAPICall)) - out.EtcdAPICall = (*metav1.Duration)(unsafe.Pointer(in.EtcdAPICall)) - out.TLSBootstrap = (*metav1.Duration)(unsafe.Pointer(in.TLSBootstrap)) - out.Discovery = (*metav1.Duration)(unsafe.Pointer(in.Discovery)) - out.UpgradeManifests = (*metav1.Duration)(unsafe.Pointer(in.UpgradeManifests)) + out.ControlPlaneComponentHealthCheck = (*v1.Duration)(unsafe.Pointer(in.ControlPlaneComponentHealthCheck)) + out.KubeletHealthCheck = (*v1.Duration)(unsafe.Pointer(in.KubeletHealthCheck)) + out.KubernetesAPICall = (*v1.Duration)(unsafe.Pointer(in.KubernetesAPICall)) + out.EtcdAPICall = (*v1.Duration)(unsafe.Pointer(in.EtcdAPICall)) + out.TLSBootstrap = (*v1.Duration)(unsafe.Pointer(in.TLSBootstrap)) + out.Discovery = (*v1.Duration)(unsafe.Pointer(in.Discovery)) + out.UpgradeManifests = (*v1.Duration)(unsafe.Pointer(in.UpgradeManifests)) return nil } @@ -978,13 +982,13 @@ func Convert_v1beta4_Timeouts_To_kubeadm_Timeouts(in *Timeouts, out *kubeadm.Tim } func autoConvert_kubeadm_Timeouts_To_v1beta4_Timeouts(in *kubeadm.Timeouts, out *Timeouts, s conversion.Scope) error { - out.ControlPlaneComponentHealthCheck = (*metav1.Duration)(unsafe.Pointer(in.ControlPlaneComponentHealthCheck)) - out.KubeletHealthCheck = (*metav1.Duration)(unsafe.Pointer(in.KubeletHealthCheck)) - out.KubernetesAPICall = (*metav1.Duration)(unsafe.Pointer(in.KubernetesAPICall)) - out.EtcdAPICall = (*metav1.Duration)(unsafe.Pointer(in.EtcdAPICall)) - out.TLSBootstrap = (*metav1.Duration)(unsafe.Pointer(in.TLSBootstrap)) - out.Discovery = (*metav1.Duration)(unsafe.Pointer(in.Discovery)) - out.UpgradeManifests = (*metav1.Duration)(unsafe.Pointer(in.UpgradeManifests)) + out.ControlPlaneComponentHealthCheck = (*v1.Duration)(unsafe.Pointer(in.ControlPlaneComponentHealthCheck)) + out.KubeletHealthCheck = (*v1.Duration)(unsafe.Pointer(in.KubeletHealthCheck)) + out.KubernetesAPICall = (*v1.Duration)(unsafe.Pointer(in.KubernetesAPICall)) + out.EtcdAPICall = (*v1.Duration)(unsafe.Pointer(in.EtcdAPICall)) + out.TLSBootstrap = (*v1.Duration)(unsafe.Pointer(in.TLSBootstrap)) + out.Discovery = (*v1.Duration)(unsafe.Pointer(in.Discovery)) + out.UpgradeManifests = (*v1.Duration)(unsafe.Pointer(in.UpgradeManifests)) return nil } diff --git a/cmd/kubeadm/app/apis/kubeadm/v1beta4/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/kubeadm/v1beta4/zz_generated.deepcopy.go index 4580454b04d..b6cda236adc 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1beta4/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1beta4/zz_generated.deepcopy.go @@ -23,9 +23,9 @@ package v1beta4 import ( corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" - v1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" + bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -121,6 +121,16 @@ func (in *ClusterConfiguration) DeepCopyInto(out *ClusterConfiguration) { (*out)[key] = val } } + if in.CertificateValidityPeriod != nil { + in, out := &in.CertificateValidityPeriod, &out.CertificateValidityPeriod + *out = new(v1.Duration) + **out = **in + } + if in.CACertificateValidityPeriod != nil { + in, out := &in.CACertificateValidityPeriod, &out.CACertificateValidityPeriod + *out = new(v1.Duration) + **out = **in + } return } @@ -336,7 +346,7 @@ func (in *InitConfiguration) DeepCopyInto(out *InitConfiguration) { out.TypeMeta = in.TypeMeta if in.BootstrapTokens != nil { in, out := &in.BootstrapTokens, &out.BootstrapTokens - *out = make([]v1.BootstrapToken, len(*in)) + *out = make([]bootstraptokenv1.BootstrapToken, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -618,37 +628,37 @@ func (in *Timeouts) DeepCopyInto(out *Timeouts) { *out = *in if in.ControlPlaneComponentHealthCheck != nil { in, out := &in.ControlPlaneComponentHealthCheck, &out.ControlPlaneComponentHealthCheck - *out = new(metav1.Duration) + *out = new(v1.Duration) **out = **in } if in.KubeletHealthCheck != nil { in, out := &in.KubeletHealthCheck, &out.KubeletHealthCheck - *out = new(metav1.Duration) + *out = new(v1.Duration) **out = **in } if in.KubernetesAPICall != nil { in, out := &in.KubernetesAPICall, &out.KubernetesAPICall - *out = new(metav1.Duration) + *out = new(v1.Duration) **out = **in } if in.EtcdAPICall != nil { in, out := &in.EtcdAPICall, &out.EtcdAPICall - *out = new(metav1.Duration) + *out = new(v1.Duration) **out = **in } if in.TLSBootstrap != nil { in, out := &in.TLSBootstrap, &out.TLSBootstrap - *out = new(metav1.Duration) + *out = new(v1.Duration) **out = **in } if in.Discovery != nil { in, out := &in.Discovery, &out.Discovery - *out = new(metav1.Duration) + *out = new(v1.Duration) **out = **in } if in.UpgradeManifests != nil { in, out := &in.UpgradeManifests, &out.UpgradeManifests - *out = new(metav1.Duration) + *out = new(v1.Duration) **out = **in } return diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go index c30368e8c86..b060d140456 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go @@ -75,6 +75,9 @@ func ValidateClusterConfiguration(c *kubeadm.ClusterConfiguration) field.ErrorLi allErrs = append(allErrs, ValidateEtcd(&c.Etcd, field.NewPath("etcd"))...) allErrs = append(allErrs, ValidateEncryptionAlgorithm(c.EncryptionAlgorithm, field.NewPath("encryptionAlgorithm"))...) allErrs = append(allErrs, componentconfigs.Validate(c)...) + for _, certError := range ValidateCertValidity(c) { + klog.Warningf("WARNING: %s", certError.Error()) + } return allErrs } @@ -770,3 +773,19 @@ func ValidateUpgradeConfiguration(c *kubeadm.UpgradeConfiguration) field.ErrorLi } return allErrs } + +// ValidateCertValidity validates if the values for cert validity are too big +func ValidateCertValidity(cfg *kubeadm.ClusterConfiguration) []error { + var allErrs []error + if cfg.CertificateValidityPeriod != nil && cfg.CertificateValidityPeriod.Duration > constants.CertificateValidityPeriod { + allErrs = append(allErrs, + errors.Errorf("certificateValidityPeriod: the value %v is more than the recommended default for certificate expiration: %v", + cfg.CertificateValidityPeriod.Duration, constants.CertificateValidityPeriod)) + } + if cfg.CACertificateValidityPeriod != nil && cfg.CACertificateValidityPeriod.Duration > constants.CACertificateValidityPeriod { + allErrs = append(allErrs, + errors.Errorf("caCertificateValidityPeriod: the value %v is more than the recommended default for CA certificate expiration: %v", + cfg.CACertificateValidityPeriod.Duration, constants.CACertificateValidityPeriod)) + } + return allErrs +} diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go index e8deb24eaf5..12cf6309b98 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go @@ -25,11 +25,13 @@ import ( "github.com/spf13/pflag" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" + "k8s.io/kubernetes/cmd/kubeadm/app/constants" ) func TestValidateToken(t *testing.T) { @@ -1541,3 +1543,51 @@ func TestPullPolicy(t *testing.T) { } } } + +func TestValidateCertValidity(t *testing.T) { + var tests = []struct { + name string + cfg *kubeadmapi.ClusterConfiguration + expectedErrors int + }{ + { + name: "no errors from nil values", + cfg: &kubeadmapi.ClusterConfiguration{ + CertificateValidityPeriod: nil, + CACertificateValidityPeriod: nil, + }, + expectedErrors: 0, + }, + { + name: "no errors from defaults", + cfg: &kubeadmapi.ClusterConfiguration{ + CertificateValidityPeriod: &metav1.Duration{ + Duration: constants.CertificateValidityPeriod, + }, + CACertificateValidityPeriod: &metav1.Duration{ + Duration: constants.CACertificateValidityPeriod, + }, + }, + expectedErrors: 0, + }, + { + name: "two errors from long durations", + cfg: &kubeadmapi.ClusterConfiguration{ + CertificateValidityPeriod: &metav1.Duration{ + Duration: constants.CertificateValidityPeriod * 2, + }, + CACertificateValidityPeriod: &metav1.Duration{ + Duration: constants.CACertificateValidityPeriod * 2, + }, + }, + expectedErrors: 2, + }, + } + + for _, tc := range tests { + actual := ValidateCertValidity(tc.cfg) + if len(actual) != tc.expectedErrors { + t.Errorf("case %q:\n\t expected errors: %v\n\t got: %v\n\t errors: %v", tc.name, tc.expectedErrors, len(actual), actual) + } + } +} diff --git a/cmd/kubeadm/app/apis/kubeadm/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/kubeadm/zz_generated.deepcopy.go index bbcd32f6094..cdb5fff4f75 100644 --- a/cmd/kubeadm/app/apis/kubeadm/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/kubeadm/zz_generated.deepcopy.go @@ -133,6 +133,16 @@ func (in *ClusterConfiguration) DeepCopyInto(out *ClusterConfiguration) { (*out)[key] = val } } + if in.CertificateValidityPeriod != nil { + in, out := &in.CertificateValidityPeriod, &out.CertificateValidityPeriod + *out = new(v1.Duration) + **out = **in + } + if in.CACertificateValidityPeriod != nil { + in, out := &in.CACertificateValidityPeriod, &out.CACertificateValidityPeriod + *out = new(v1.Duration) + **out = **in + } return } diff --git a/cmd/kubeadm/app/cmd/certs.go b/cmd/kubeadm/app/cmd/certs.go index 3c12fd58e28..295e7d64ace 100644 --- a/cmd/kubeadm/app/cmd/certs.go +++ b/cmd/kubeadm/app/cmd/certs.go @@ -516,7 +516,7 @@ func (p *certTextPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { s := fmt.Sprintf("%s\t%s\t%s\t%s\t%-8v", cert.Name, cert.ExpirationDate.Format("Jan 02, 2006 15:04 MST"), - duration.ShortHumanDuration(time.Duration(cert.ResidualTimeSeconds)*time.Second), + duration.HumanDuration(time.Duration(cert.ResidualTimeSeconds)*time.Second), cert.CAName, yesNo(cert.ExternallyManaged), ) @@ -535,7 +535,7 @@ func (p *certTextPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { s := fmt.Sprintf("%s\t%s\t%s\t%-8v", ca.Name, ca.ExpirationDate.Format("Jan 02, 2006 15:04 MST"), - duration.ShortHumanDuration(time.Duration(ca.ResidualTimeSeconds)*time.Second), + duration.HumanDuration(time.Duration(ca.ResidualTimeSeconds)*time.Second), yesNo(ca.ExternallyManaged), ) fmt.Fprintln(tabw, s) diff --git a/cmd/kubeadm/app/cmd/kubeconfig.go b/cmd/kubeadm/app/cmd/kubeconfig.go index 14b592eb2ab..d60ac60ecbe 100644 --- a/cmd/kubeadm/app/cmd/kubeconfig.go +++ b/cmd/kubeadm/app/cmd/kubeconfig.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" @@ -29,6 +30,7 @@ import ( cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" ) @@ -89,20 +91,27 @@ func newCmdUserKubeConfig(out io.Writer) *cobra.Command { return err } - if validityPeriod > kubeadmconstants.CertificateValidity { + if validityPeriod > kubeadmconstants.CertificateValidityPeriod { klog.Warningf("WARNING: the specified certificate validity period %v is longer than the default duration %v, this may increase security risks.", - validityPeriod, kubeadmconstants.CertificateValidity) + validityPeriod, kubeadmconstants.CertificateValidityPeriod) + } + internalCfg.ClusterConfiguration.CertificateValidityPeriod = &metav1.Duration{ + Duration: validityPeriod, } - notAfter := time.Now().Add(validityPeriod).UTC() + startTime := kubeadmutil.StartTimeUTC() + notAfter := startTime.Add(kubeadmconstants.CertificateValidityPeriod) + if internalCfg.ClusterConfiguration.CertificateValidityPeriod != nil { + notAfter = startTime.Add(validityPeriod) + } // if the kubeconfig file for an additional user has to use a token, use it if token != "" { - return kubeconfigphase.WriteKubeConfigWithToken(out, internalCfg, clientName, token, ¬After) + return kubeconfigphase.WriteKubeConfigWithToken(out, internalCfg, clientName, token, notAfter) } // Otherwise, write a kubeconfig file with a generate client cert - return kubeconfigphase.WriteKubeConfigWithClientCert(out, internalCfg, clientName, organizations, ¬After) + return kubeconfigphase.WriteKubeConfigWithClientCert(out, internalCfg, clientName, organizations, notAfter) }, Args: cobra.NoArgs, } @@ -113,7 +122,7 @@ func newCmdUserKubeConfig(out io.Writer) *cobra.Command { cmd.Flags().StringVar(&token, options.TokenStr, token, "The token that should be used as the authentication mechanism for this kubeconfig, instead of client certificates") cmd.Flags().StringVar(&clientName, "client-name", clientName, "The name of user. It will be used as the CN if client certificates are created") cmd.Flags().StringSliceVar(&organizations, "org", organizations, "The organizations of the client certificate. It will be used as the O if client certificates are created") - cmd.Flags().DurationVar(&validityPeriod, "validity-period", kubeadmconstants.CertificateValidity, "The validity period of the client certificate. It is an offset from the current time.") + cmd.Flags().DurationVar(&validityPeriod, "validity-period", kubeadmconstants.CertificateValidityPeriod, "The validity period of the client certificate. It is an offset from the current time.") cmd.MarkFlagRequired("client-name") return cmd diff --git a/cmd/kubeadm/app/cmd/kubeconfig_test.go b/cmd/kubeadm/app/cmd/kubeconfig_test.go index 2b067d24ecb..372c9a4ba0e 100644 --- a/cmd/kubeadm/app/cmd/kubeconfig_test.go +++ b/cmd/kubeadm/app/cmd/kubeconfig_test.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" "testing" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd" @@ -29,6 +30,7 @@ import ( kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" testutil "k8s.io/kubernetes/cmd/kubeadm/test" kubeconfigtestutil "k8s.io/kubernetes/cmd/kubeadm/test/kubeconfig" @@ -89,12 +91,13 @@ func TestKubeConfigSubCommandsThatWritesToOut(t *testing.T) { } var tests = []struct { - name string - command string - clusterName string - withClientCert bool - withToken bool - additionalFlags []string + name string + command string + clusterName string + withClientCert bool + withToken bool + additionalFlags []string + expectedValidityPeriod time.Duration }{ { name: "user subCommand withClientCert", @@ -120,6 +123,13 @@ func TestKubeConfigSubCommandsThatWritesToOut(t *testing.T) { clusterName: "my-cluster-with-token", additionalFlags: []string{"--token=123456"}, }, + { + name: "user subCommand with validity period", + withClientCert: true, + command: "user", + additionalFlags: []string{"--validity-period=12h"}, + expectedValidityPeriod: 12 * time.Hour, + }, } for _, test := range tests { @@ -156,8 +166,13 @@ func TestKubeConfigSubCommandsThatWritesToOut(t *testing.T) { kubeconfigtestutil.AssertKubeConfigCurrentCluster(t, config, "https://1.2.3.4:1234", caCert) if test.withClientCert { + if test.expectedValidityPeriod == 0 { + test.expectedValidityPeriod = kubeadmconstants.CertificateValidityPeriod + } // checks that kubeconfig files have expected client cert - kubeconfigtestutil.AssertKubeConfigCurrentAuthInfoWithClientCert(t, config, caCert, "myUser") + startTime := kubeadmutil.StartTimeUTC() + notAfter := startTime.Add(test.expectedValidityPeriod) + kubeconfigtestutil.AssertKubeConfigCurrentAuthInfoWithClientCert(t, config, caCert, notAfter, "myUser") } if test.withToken { diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go index caa7bbe7ecc..98047f529de 100644 --- a/cmd/kubeadm/app/constants/constants.go +++ b/cmd/kubeadm/app/constants/constants.go @@ -44,8 +44,10 @@ const ( // CertificateBackdate defines the offset applied to notBefore for CA certificates generated by kubeadm CertificateBackdate = time.Minute * 5 - // CertificateValidity defines the validity for all the signed certificates generated by kubeadm - CertificateValidity = time.Hour * 24 * 365 + // CertificateValidityPeriod defines the validity period for all the signed certificates generated by kubeadm + CertificateValidityPeriod = time.Hour * 24 * 365 + // CACertificateValidityPeriod defines the validity period for all the signed CA certificates generated by kubeadm + CACertificateValidityPeriod = time.Hour * 24 * 365 * 10 // DefaultCertificateDir defines default certificate directory DefaultCertificateDir = "pki" diff --git a/cmd/kubeadm/app/phases/certs/certlist.go b/cmd/kubeadm/app/phases/certs/certlist.go index a795f4649f1..732868dfbce 100644 --- a/cmd/kubeadm/app/phases/certs/certlist.go +++ b/cmd/kubeadm/app/phases/certs/certlist.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "path/filepath" + "time" "github.com/pkg/errors" @@ -30,6 +31,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" ) @@ -50,6 +52,8 @@ type KubeadmCert struct { // These functions will be run in series, passed both the InitConfiguration and a cert Config. configMutators []configMutatorsFunc config pkiutil.CertConfig + // Used for unit tests. + creationTime time.Time } // GetConfig returns the definition for the given cert given the provided InitConfiguration @@ -60,6 +64,30 @@ func (k *KubeadmCert) GetConfig(ic *kubeadmapi.InitConfiguration) (*pkiutil.Cert } } + // creationTime should be set only during unit tests, otherwise the kubeadm start time + // should be + if k.creationTime.IsZero() { + k.creationTime = kubeadmutil.StartTimeUTC() + } + + // Backdate certificate to allow small time jumps. + k.config.NotBefore = k.creationTime.Add(-kubeadmconstants.CertificateBackdate) + + // Use the validity periods defined in the ClusterConfiguration. + // If CAName is empty this is a CA cert. + if len(k.CAName) != 0 { + if ic.ClusterConfiguration.CertificateValidityPeriod != nil { + k.config.NotAfter = k.creationTime. + Add(ic.ClusterConfiguration.CertificateValidityPeriod.Duration) + } + } else { + if ic.ClusterConfiguration.CACertificateValidityPeriod != nil { + k.config.NotAfter = k.creationTime. + Add(ic.ClusterConfiguration.CACertificateValidityPeriod.Duration) + } + } + + // Use the encryption algorithm from ClusterConfiguration. k.config.EncryptionAlgorithm = ic.ClusterConfiguration.EncryptionAlgorithmType() return &k.config, nil } diff --git a/cmd/kubeadm/app/phases/certs/certlist_test.go b/cmd/kubeadm/app/phases/certs/certlist_test.go index 556d205b5f1..0c233d754f3 100644 --- a/cmd/kubeadm/app/phases/certs/certlist_test.go +++ b/cmd/kubeadm/app/phases/certs/certlist_test.go @@ -23,7 +23,10 @@ import ( "os" "path/filepath" "testing" + "time" + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" certutil "k8s.io/client-go/util/cert" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" @@ -301,3 +304,85 @@ func TestCreateKeyAndCSR(t *testing.T) { }) } } + +func TestGetConfig(t *testing.T) { + var ( + now = time.Now() + backdate = kubeadmconstants.CertificateBackdate + ) + + tests := []struct { + name string + cert *KubeadmCert + cfg *kubeadmapi.InitConfiguration + expectedConfig *pkiutil.CertConfig + }{ + { + name: "encryption algorithm is set", + cert: &KubeadmCert{ + creationTime: now, + }, + cfg: &kubeadmapi.InitConfiguration{ + ClusterConfiguration: kubeadmapi.ClusterConfiguration{ + EncryptionAlgorithm: kubeadmapi.EncryptionAlgorithmECDSAP256, + }, + }, + expectedConfig: &pkiutil.CertConfig{ + Config: certutil.Config{ + NotBefore: now.Add(-backdate), + }, + EncryptionAlgorithm: kubeadmapi.EncryptionAlgorithmECDSAP256, + }, + }, + { + name: "cert validity is set", + cert: &KubeadmCert{ + CAName: "some-ca", + creationTime: now, + }, + cfg: &kubeadmapi.InitConfiguration{ + ClusterConfiguration: kubeadmapi.ClusterConfiguration{ + CertificateValidityPeriod: &metav1.Duration{ + Duration: time.Hour * 1, + }, + }, + }, + expectedConfig: &pkiutil.CertConfig{ + Config: certutil.Config{ + NotBefore: now.Add(-backdate), + }, + NotAfter: now.Add(time.Hour * 1)}, + }, + { + name: "CA cert validity is set", + cert: &KubeadmCert{ + CAName: "", + creationTime: now, + }, + cfg: &kubeadmapi.InitConfiguration{ + ClusterConfiguration: kubeadmapi.ClusterConfiguration{ + CACertificateValidityPeriod: &metav1.Duration{ + Duration: time.Hour * 10, + }, + }, + }, + expectedConfig: &pkiutil.CertConfig{ + Config: certutil.Config{ + NotBefore: now.Add(-backdate), + }, + NotAfter: now.Add(time.Hour * 10), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := tt.cert.GetConfig(tt.cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(actual, tt.expectedConfig); diff != "" { + t.Fatalf("GetConfig() returned diff (-want,+got):\n%s", diff) + } + }) + } +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/manager.go b/cmd/kubeadm/app/phases/certs/renewal/manager.go index 3f2247c95c6..2ee26771b78 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/manager.go +++ b/cmd/kubeadm/app/phases/certs/renewal/manager.go @@ -27,6 +27,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" ) @@ -250,6 +251,17 @@ func (rm *Manager) RenewUsingLocalCA(name string) (bool, error) { EncryptionAlgorithm: rm.cfg.EncryptionAlgorithmType(), } + startTime := kubeadmutil.StartTimeUTC() + + // Backdate certificate to allow small time jumps. + cfg.NotBefore = startTime.Add(-kubeadmconstants.CertificateBackdate) + + // Use the validity periods defined in the ClusterConfiguration. + // Only use CertificateValidityPeriod as CA renewal is not supported. + if rm.cfg.CertificateValidityPeriod != nil { + cfg.NotAfter = startTime.Add(rm.cfg.CertificateValidityPeriod.Duration) + } + // reads the CA caCert, caKey, err := certsphase.LoadCertificateAuthority(rm.cfg.CertificatesDir, handler.CABaseName) if err != nil { diff --git a/cmd/kubeadm/app/phases/certs/renewal/manager_test.go b/cmd/kubeadm/app/phases/certs/renewal/manager_test.go index e3a9783363e..780e4c2a572 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/manager_test.go +++ b/cmd/kubeadm/app/phases/certs/renewal/manager_test.go @@ -28,10 +28,13 @@ import ( "testing" "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" certutil "k8s.io/client-go/util/cert" netutils "k8s.io/utils/net" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" certtestutil "k8s.io/kubernetes/cmd/kubeadm/app/util/certs" "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" testutil "k8s.io/kubernetes/cmd/kubeadm/test" @@ -46,7 +49,7 @@ var ( testCertOrganization = []string{"sig-cluster-lifecycle"} - testCertCfg = makeTestCertConfig(testCertOrganization) + testCertCfg = makeTestCertConfig(testCertOrganization, time.Time{}, time.Time{}) ) type fakecertificateReadWriter struct { @@ -117,12 +120,23 @@ func TestRenewUsingLocalCA(t *testing.T) { cfg := &kubeadmapi.ClusterConfiguration{ CertificatesDir: dir, + CertificateValidityPeriod: &metav1.Duration{ + Duration: time.Hour * 10, + }, } rm, err := NewManager(cfg, dir) if err != nil { t.Fatalf("Failed to create the certificate renewal manager: %v", err) } + // Prepare test certs with a past validity. + startTime := kubeadmutil.StartTimeUTC() + + fmt.Println("START TIME TEST", startTime) + + notBefore := startTime.Add(-rm.cfg.CertificateValidityPeriod.Duration * 2) + notAfter := startTime.Add(-rm.cfg.CertificateValidityPeriod.Duration) + tests := []struct { name string certName string @@ -133,7 +147,7 @@ func TestRenewUsingLocalCA(t *testing.T) { name: "Certificate renewal for a PKI certificate", certName: "apiserver", createCertFunc: func() *x509.Certificate { - return writeTestCertificate(t, dir, "apiserver", testCACert, testCAKey, testCertOrganization) + return writeTestCertificate(t, dir, "apiserver", testCACert, testCAKey, testCertOrganization, notBefore, notAfter) }, expectedOrganization: testCertOrganization, }, @@ -141,7 +155,7 @@ func TestRenewUsingLocalCA(t *testing.T) { name: "Certificate renewal for a certificate embedded in a kubeconfig file", certName: "admin.conf", createCertFunc: func() *x509.Certificate { - return writeTestKubeconfig(t, dir, "admin.conf", testCACert, testCAKey) + return writeTestKubeconfig(t, dir, "admin.conf", testCACert, testCAKey, notBefore, notAfter) }, expectedOrganization: testCertOrganization, }, @@ -151,7 +165,9 @@ func TestRenewUsingLocalCA(t *testing.T) { t.Run(test.name, func(t *testing.T) { cert := test.createCertFunc() - time.Sleep(1 * time.Second) + notBefore := startTime.Add(-kubeadmconstants.CertificateBackdate) + notAfter := startTime.Add(rm.cfg.CertificateValidityPeriod.Duration) + testCertCfg := makeTestCertConfig(testCertOrganization, notBefore, notAfter) _, err := rm.RenewUsingLocalCA(test.certName) if err != nil { @@ -177,6 +193,8 @@ func TestRenewUsingLocalCA(t *testing.T) { certtestutil.AssertCertificateHasCommonName(t, newCert, testCertCfg.CommonName) certtestutil.AssertCertificateHasDNSNames(t, newCert, testCertCfg.AltNames.DNSNames...) certtestutil.AssertCertificateHasIPAddresses(t, newCert, testCertCfg.AltNames.IPs...) + certtestutil.AssertCertificateHasNotBefore(t, newCert, testCertCfg.NotBefore) + certtestutil.AssertCertificateHasNotAfter(t, newCert, testCertCfg.NotAfter) }) } } @@ -212,14 +230,14 @@ func TestCreateRenewCSR(t *testing.T) { name: "Creation of a CSR request for renewal of a PKI certificate", certName: "apiserver", createCertFunc: func() *x509.Certificate { - return writeTestCertificate(t, dir, "apiserver", testCACert, testCAKey, testCertOrganization) + return writeTestCertificate(t, dir, "apiserver", testCACert, testCAKey, testCertOrganization, time.Time{}, time.Time{}) }, }, { name: "Creation of a CSR request for renewal of a certificate embedded in a kubeconfig file", certName: "admin.conf", createCertFunc: func() *x509.Certificate { - return writeTestKubeconfig(t, dir, "admin.conf", testCACert, testCAKey) + return writeTestKubeconfig(t, dir, "admin.conf", testCACert, testCAKey, time.Time{}, time.Time{}) }, }, } @@ -294,7 +312,7 @@ func TestCertToConfig(t *testing.T) { } } -func makeTestCertConfig(organization []string) *pkiutil.CertConfig { +func makeTestCertConfig(organization []string, notBefore, notAfter time.Time) *pkiutil.CertConfig { return &pkiutil.CertConfig{ Config: certutil.Config{ CommonName: "test-common-name", @@ -303,8 +321,10 @@ func makeTestCertConfig(organization []string) *pkiutil.CertConfig { IPs: []net.IP{netutils.ParseIPSloppy("10.100.0.1")}, DNSNames: []string{"test-domain.space"}, }, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + NotBefore: notBefore, }, + NotAfter: notAfter, } } diff --git a/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go b/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go index cdcc9a1e284..af329a47fe5 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go +++ b/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go @@ -24,6 +24,7 @@ import ( "os" "path/filepath" "testing" + "time" "k8s.io/client-go/tools/clientcmd" certutil "k8s.io/client-go/util/cert" @@ -42,7 +43,7 @@ func TestPKICertificateReadWriter(t *testing.T) { defer os.RemoveAll(dir) // creates a certificate - cert := writeTestCertificate(t, dir, "test", testCACert, testCAKey, testCertOrganization) + cert := writeTestCertificate(t, dir, "test", testCACert, testCAKey, testCertOrganization, time.Time{}, time.Time{}) // Creates a pkiCertificateReadWriter pkiReadWriter := newPKICertificateReadWriter(dir, "test") @@ -100,7 +101,7 @@ func TestKubeconfigReadWriter(t *testing.T) { } // creates a certificate and then embeds it into a kubeconfig file - cert := writeTestKubeconfig(t, dirKubernetes, "test", testCACert, testCAKey) + cert := writeTestKubeconfig(t, dirKubernetes, "test", testCACert, testCAKey, time.Time{}, time.Time{}) // Creates a KubeconfigReadWriter kubeconfigReadWriter := newKubeconfigReadWriter(dirKubernetes, "test", dirPKI, caName) @@ -146,8 +147,8 @@ func TestKubeconfigReadWriter(t *testing.T) { } // writeTestCertificate is a utility for creating a test certificate -func writeTestCertificate(t *testing.T, dir, name string, caCert *x509.Certificate, caKey crypto.Signer, organization []string) *x509.Certificate { - cert, key, err := pkiutil.NewCertAndKey(caCert, caKey, makeTestCertConfig(organization)) +func writeTestCertificate(t *testing.T, dir, name string, caCert *x509.Certificate, caKey crypto.Signer, organization []string, notBefore, notAfter time.Time) *x509.Certificate { + cert, key, err := pkiutil.NewCertAndKey(caCert, caKey, makeTestCertConfig(organization, notBefore, notAfter)) if err != nil { t.Fatalf("couldn't generate certificate: %v", err) } @@ -160,7 +161,7 @@ func writeTestCertificate(t *testing.T, dir, name string, caCert *x509.Certifica } // writeTestKubeconfig is a utility for creating a test kubeconfig with an embedded certificate -func writeTestKubeconfig(t *testing.T, dir, name string, caCert *x509.Certificate, caKey crypto.Signer) *x509.Certificate { +func writeTestKubeconfig(t *testing.T, dir, name string, caCert *x509.Certificate, caKey crypto.Signer, notBefore, notAfter time.Time) *x509.Certificate { cfg := &pkiutil.CertConfig{ Config: certutil.Config{ @@ -171,7 +172,9 @@ func writeTestKubeconfig(t *testing.T, dir, name string, caCert *x509.Certificat IPs: []net.IP{netutils.ParseIPSloppy("10.100.0.1")}, DNSNames: []string{"test-domain.space"}, }, + NotBefore: notBefore, }, + NotAfter: notAfter, } cert, key, err := pkiutil.NewCertAndKey(caCert, caKey, cfg) if err != nil { diff --git a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go index 53d543310b1..4476354263b 100644 --- a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go +++ b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go @@ -66,11 +66,12 @@ type tokenAuth struct { // kubeConfigSpec struct holds info required to build a KubeConfig object type kubeConfigSpec struct { - CACert *x509.Certificate - APIServer string - ClientName string - TokenAuth *tokenAuth `datapolicy:"token"` - ClientCertAuth *clientCertAuth `datapolicy:"security-key"` + CACert *x509.Certificate + APIServer string + ClientName string + ClientCertNotAfter time.Time + TokenAuth *tokenAuth `datapolicy:"token"` + ClientCertAuth *clientCertAuth `datapolicy:"security-key"` } // CreateJoinControlPlaneKubeConfigFiles will create and write to disk the kubeconfig files required by kubeadm @@ -131,7 +132,7 @@ func createKubeConfigFiles(outDir string, cfg *kubeadmapi.InitConfiguration, kub } // builds the KubeConfig object - config, err := buildKubeConfigFromSpec(spec, cfg.ClusterName, nil) + config, err := buildKubeConfigFromSpec(spec, cfg.ClusterName) if err != nil { return err } @@ -170,7 +171,7 @@ func getKubeConfigSpecs(cfg *kubeadmapi.InitConfiguration) (map[string]*kubeConf } // buildKubeConfigFromSpec creates a kubeconfig object for the given kubeConfigSpec -func buildKubeConfigFromSpec(spec *kubeConfigSpec, clustername string, notAfter *time.Time) (*clientcmdapi.Config, error) { +func buildKubeConfigFromSpec(spec *kubeConfigSpec, clustername string) (*clientcmdapi.Config, error) { // If this kubeconfig should use token if spec.TokenAuth != nil { @@ -184,8 +185,8 @@ func buildKubeConfigFromSpec(spec *kubeConfigSpec, clustername string, notAfter ), nil } - // otherwise, create a client certs - clientCertConfig := newClientCertConfigFromKubeConfigSpec(spec, notAfter) + // otherwise, create a client cert + clientCertConfig := newClientCertConfigFromKubeConfigSpec(spec) clientCert, clientKey, err := pkiutil.NewCertAndKey(spec.CACert, spec.ClientCertAuth.CAKey, &clientCertConfig) if err != nil { @@ -207,14 +208,14 @@ func buildKubeConfigFromSpec(spec *kubeConfigSpec, clustername string, notAfter ), nil } -func newClientCertConfigFromKubeConfigSpec(spec *kubeConfigSpec, notAfter *time.Time) pkiutil.CertConfig { +func newClientCertConfigFromKubeConfigSpec(spec *kubeConfigSpec) pkiutil.CertConfig { return pkiutil.CertConfig{ Config: certutil.Config{ CommonName: spec.ClientName, Organization: spec.ClientCertAuth.Organizations, Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, }, - NotAfter: notAfter, + NotAfter: spec.ClientCertNotAfter, } } @@ -303,7 +304,7 @@ func createKubeConfigFileIfNotExists(outDir, filename string, config *clientcmda } // WriteKubeConfigWithClientCert writes a kubeconfig file - with a client certificate as authentication info - to the given writer. -func WriteKubeConfigWithClientCert(out io.Writer, cfg *kubeadmapi.InitConfiguration, clientName string, organizations []string, notAfter *time.Time) error { +func WriteKubeConfigWithClientCert(out io.Writer, cfg *kubeadmapi.InitConfiguration, clientName string, organizations []string, notAfter time.Time) error { // creates the KubeConfigSpecs, actualized for the current InitConfiguration caCert, caKey, err := pkiutil.TryLoadCertAndKeyFromDisk(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName) @@ -326,13 +327,14 @@ func WriteKubeConfigWithClientCert(out io.Writer, cfg *kubeadmapi.InitConfigurat CAKey: caKey, Organizations: organizations, }, + ClientCertNotAfter: notAfter, } - return writeKubeConfigFromSpec(out, spec, cfg.ClusterName, notAfter) + return writeKubeConfigFromSpec(out, spec, cfg.ClusterName) } // WriteKubeConfigWithToken writes a kubeconfig file - with a token as client authentication info - to the given writer. -func WriteKubeConfigWithToken(out io.Writer, cfg *kubeadmapi.InitConfiguration, clientName, token string, notAfter *time.Time) error { +func WriteKubeConfigWithToken(out io.Writer, cfg *kubeadmapi.InitConfiguration, clientName, token string, notAfter time.Time) error { // creates the KubeConfigSpecs, actualized for the current InitConfiguration caCert, _, err := pkiutil.TryLoadCertAndKeyFromDisk(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName) @@ -354,16 +356,17 @@ func WriteKubeConfigWithToken(out io.Writer, cfg *kubeadmapi.InitConfiguration, TokenAuth: &tokenAuth{ Token: token, }, + ClientCertNotAfter: notAfter, } - return writeKubeConfigFromSpec(out, spec, cfg.ClusterName, notAfter) + return writeKubeConfigFromSpec(out, spec, cfg.ClusterName) } // writeKubeConfigFromSpec creates a kubeconfig object from a kubeConfigSpec and writes it to the given writer. -func writeKubeConfigFromSpec(out io.Writer, spec *kubeConfigSpec, clustername string, notAfter *time.Time) error { +func writeKubeConfigFromSpec(out io.Writer, spec *kubeConfigSpec, clustername string) error { // builds the KubeConfig object - config, err := buildKubeConfigFromSpec(spec, clustername, notAfter) + config, err := buildKubeConfigFromSpec(spec, clustername) if err != nil { return err } @@ -439,6 +442,12 @@ func getKubeConfigSpecsBase(cfg *kubeadmapi.InitConfiguration) (map[string]*kube return nil, err } + startTime := kubeadmutil.StartTimeUTC() + notAfter := startTime.Add(kubeadmconstants.CertificateValidityPeriod) + if cfg.ClusterConfiguration.CertificateValidityPeriod != nil { + notAfter = startTime.Add(cfg.ClusterConfiguration.CertificateValidityPeriod.Duration) + } + return map[string]*kubeConfigSpec{ kubeadmconstants.AdminKubeConfigFileName: { APIServer: controlPlaneEndpoint, @@ -446,6 +455,7 @@ func getKubeConfigSpecsBase(cfg *kubeadmapi.InitConfiguration) (map[string]*kube ClientCertAuth: &clientCertAuth{ Organizations: []string{kubeadmconstants.ClusterAdminsGroupAndClusterRoleBinding}, }, + ClientCertNotAfter: notAfter, }, kubeadmconstants.SuperAdminKubeConfigFileName: { APIServer: controlPlaneEndpoint, @@ -453,6 +463,7 @@ func getKubeConfigSpecsBase(cfg *kubeadmapi.InitConfiguration) (map[string]*kube ClientCertAuth: &clientCertAuth{ Organizations: []string{kubeadmconstants.SystemPrivilegedGroup}, }, + ClientCertNotAfter: notAfter, }, kubeadmconstants.KubeletKubeConfigFileName: { APIServer: controlPlaneEndpoint, @@ -460,16 +471,19 @@ func getKubeConfigSpecsBase(cfg *kubeadmapi.InitConfiguration) (map[string]*kube ClientCertAuth: &clientCertAuth{ Organizations: []string{kubeadmconstants.NodesGroup}, }, + ClientCertNotAfter: notAfter, }, kubeadmconstants.ControllerManagerKubeConfigFileName: { - APIServer: localAPIEndpoint, - ClientName: kubeadmconstants.ControllerManagerUser, - ClientCertAuth: &clientCertAuth{}, + APIServer: localAPIEndpoint, + ClientName: kubeadmconstants.ControllerManagerUser, + ClientCertAuth: &clientCertAuth{}, + ClientCertNotAfter: notAfter, }, kubeadmconstants.SchedulerKubeConfigFileName: { - APIServer: localAPIEndpoint, - ClientName: kubeadmconstants.SchedulerUser, - ClientCertAuth: &clientCertAuth{}, + APIServer: localAPIEndpoint, + ClientName: kubeadmconstants.SchedulerUser, + ClientCertAuth: &clientCertAuth{}, + ClientCertNotAfter: notAfter, }, }, nil } @@ -497,7 +511,7 @@ func createKubeConfigAndCSR(kubeConfigDir string, kubeadmConfig *kubeadmapi.Init return errors.Errorf("%s: csr: %s", errExist, kubeConfigPath) } - clientCertConfig := newClientCertConfigFromKubeConfigSpec(spec, nil) + clientCertConfig := newClientCertConfigFromKubeConfigSpec(spec) clientKey, err := pkiutil.NewPrivateKey(clientCertConfig.EncryptionAlgorithm) if err != nil { diff --git a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go index fc2441fc0e0..8d5ba5cbbeb 100644 --- a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go +++ b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go @@ -34,6 +34,7 @@ import ( rbac "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" clientset "k8s.io/client-go/kubernetes" @@ -218,12 +219,14 @@ func TestBuildKubeConfigFromSpecWithClientAuth(t *testing.T) { // Creates a CA caCert, caKey := certstestutil.SetupCertificateAuthority(t) + notAfter, _ := time.Parse(time.RFC3339, "2026-01-02T15:04:05Z") + // Executes buildKubeConfigFromSpec passing a KubeConfigSpec with a ClientAuth - config := setupdKubeConfigWithClientAuth(t, caCert, caKey, "https://1.2.3.4:1234", "myClientName", "test-cluster", "myOrg1", "myOrg2") + config := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://1.2.3.4:1234", "myClientName", "test-cluster", "myOrg1", "myOrg2") // Asserts spec data are propagated to the kubeconfig kubeconfigtestutil.AssertKubeConfigCurrentCluster(t, config, "https://1.2.3.4:1234", caCert) - kubeconfigtestutil.AssertKubeConfigCurrentAuthInfoWithClientCert(t, config, caCert, "myClientName", "myOrg1", "myOrg2") + kubeconfigtestutil.AssertKubeConfigCurrentAuthInfoWithClientCert(t, config, caCert, notAfter, "myClientName", "myOrg1", "myOrg2") } func TestBuildKubeConfigFromSpecWithTokenAuth(t *testing.T) { @@ -231,7 +234,7 @@ func TestBuildKubeConfigFromSpecWithTokenAuth(t *testing.T) { caCert, _ := certstestutil.SetupCertificateAuthority(t) // Executes buildKubeConfigFromSpec passing a KubeConfigSpec with a Token - config := setupdKubeConfigWithTokenAuth(t, caCert, "https://1.2.3.4:1234", "myClientName", "123456", "test-cluster") + config := setupKubeConfigWithTokenAuth(t, caCert, "https://1.2.3.4:1234", "myClientName", "123456", "test-cluster") // Asserts spec data are propagated to the kubeconfig kubeconfigtestutil.AssertKubeConfigCurrentCluster(t, config, "https://1.2.3.4:1234", caCert) @@ -244,11 +247,13 @@ func TestCreateKubeConfigFileIfNotExists(t *testing.T) { caCert, caKey := certstestutil.SetupCertificateAuthority(t) anotherCaCert, anotherCaKey := certstestutil.SetupCertificateAuthority(t) + notAfter, _ := time.Parse(time.RFC3339, "2026-01-02T15:04:05Z") + // build kubeconfigs (to be used to test kubeconfigs equality/not equality) - config := setupdKubeConfigWithClientAuth(t, caCert, caKey, "https://1.2.3.4:1234", "test-cluster", "myOrg1", "myOrg2") - configWithAnotherClusterCa := setupdKubeConfigWithClientAuth(t, anotherCaCert, anotherCaKey, "https://1.2.3.4:1234", "test-cluster", "myOrg1", "myOrg2") - configWithAnotherClusterAddress := setupdKubeConfigWithClientAuth(t, caCert, caKey, "https://3.4.5.6:3456", "myOrg1", "test-cluster", "myOrg2") - invalidConfig := setupdKubeConfigWithClientAuth(t, caCert, caKey, "https://1.2.3.4:1234", "test-cluster", "myOrg1", "myOrg2") + config := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://1.2.3.4:1234", "test-cluster", "myOrg1", "myOrg2") + configWithAnotherClusterCa := setupKubeConfigWithClientAuth(t, anotherCaCert, anotherCaKey, notAfter, "https://1.2.3.4:1234", "test-cluster", "myOrg1", "myOrg2") + configWithAnotherClusterAddress := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://3.4.5.6:3456", "myOrg1", "test-cluster", "myOrg2") + invalidConfig := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://1.2.3.4:1234", "test-cluster", "myOrg1", "myOrg2") invalidConfig.CurrentContext = "invalid context" var tests = []struct { @@ -384,6 +389,8 @@ func TestWriteKubeConfigFailsIfCADoesntExists(t *testing.T) { }, } + notAfter, _ := time.Parse(time.RFC3339, "2026-01-02T15:04:05Z") + var tests = []struct { name string writeKubeConfigFunction func(out io.Writer) error @@ -391,13 +398,13 @@ func TestWriteKubeConfigFailsIfCADoesntExists(t *testing.T) { { name: "WriteKubeConfigWithClientCert", writeKubeConfigFunction: func(out io.Writer) error { - return WriteKubeConfigWithClientCert(out, cfg, "myUser", []string{"myOrg"}, nil) + return WriteKubeConfigWithClientCert(out, cfg, "myUser", []string{"myOrg"}, notAfter) }, }, { name: "WriteKubeConfigWithToken", writeKubeConfigFunction: func(out io.Writer) error { - return WriteKubeConfigWithToken(out, cfg, "myUser", "12345", nil) + return WriteKubeConfigWithToken(out, cfg, "myUser", "12345", notAfter) }, }, } @@ -433,9 +440,14 @@ func TestWriteKubeConfig(t *testing.T) { LocalAPIEndpoint: kubeadmapi.APIEndpoint{AdvertiseAddress: "1.2.3.4", BindPort: 1234}, ClusterConfiguration: kubeadmapi.ClusterConfiguration{ CertificatesDir: pkidir, + CertificateValidityPeriod: &metav1.Duration{ + Duration: time.Hour * 10, + }, }, } + notAfter, _ := time.Parse(time.RFC3339, "2026-01-02T15:04:05Z") + var tests = []struct { name string writeKubeConfigFunction func(out io.Writer) error @@ -445,14 +457,14 @@ func TestWriteKubeConfig(t *testing.T) { { name: "WriteKubeConfigWithClientCert", writeKubeConfigFunction: func(out io.Writer) error { - return WriteKubeConfigWithClientCert(out, cfg, "myUser", []string{"myOrg"}, nil) + return WriteKubeConfigWithClientCert(out, cfg, "myUser", []string{"myOrg"}, notAfter) }, withClientCert: true, }, { name: "WriteKubeConfigWithToken", writeKubeConfigFunction: func(out io.Writer) error { - return WriteKubeConfigWithToken(out, cfg, "myUser", "12345", nil) + return WriteKubeConfigWithToken(out, cfg, "myUser", "12345", notAfter) }, withToken: true, }, @@ -480,7 +492,7 @@ func TestWriteKubeConfig(t *testing.T) { if test.withClientCert { // checks that kubeconfig files have expected client cert - kubeconfigtestutil.AssertKubeConfigCurrentAuthInfoWithClientCert(t, config, caCert, "myUser", "myOrg") + kubeconfigtestutil.AssertKubeConfigCurrentAuthInfoWithClientCert(t, config, caCert, notAfter, "myUser", "myOrg") } if test.withToken { @@ -495,9 +507,11 @@ func TestValidateKubeConfig(t *testing.T) { caCert, caKey := certstestutil.SetupCertificateAuthority(t) anotherCaCert, anotherCaKey := certstestutil.SetupCertificateAuthority(t) - config := setupdKubeConfigWithClientAuth(t, caCert, caKey, "https://1.2.3.4:1234", "test-cluster", "myOrg1") - configWithAnotherClusterCa := setupdKubeConfigWithClientAuth(t, anotherCaCert, anotherCaKey, "https://1.2.3.4:1234", "test-cluster", "myOrg1") - configWithAnotherServerURL := setupdKubeConfigWithClientAuth(t, caCert, caKey, "https://4.3.2.1:4321", "test-cluster", "myOrg1") + notAfter, _ := time.Parse(time.RFC3339, "2026-01-02T15:04:05Z") + + config := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://1.2.3.4:1234", "test-cluster", "myOrg1") + configWithAnotherClusterCa := setupKubeConfigWithClientAuth(t, anotherCaCert, anotherCaKey, notAfter, "https://1.2.3.4:1234", "test-cluster", "myOrg1") + configWithAnotherServerURL := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://4.3.2.1:4321", "test-cluster", "myOrg1") configWithSameClusterCaByExternalFile := config.DeepCopy() currentCtx, exists := configWithSameClusterCaByExternalFile.Contexts[configWithSameClusterCaByExternalFile.CurrentContext] @@ -610,15 +624,17 @@ func TestValidateKubeconfigsForExternalCA(t *testing.T) { t.Fatalf("failure while deleting ca.key: %v", err) } + notAfter, _ := time.Parse(time.RFC3339, "2026-01-02T15:04:05Z") + // create a valid config - config := setupdKubeConfigWithClientAuth(t, caCert, caKey, "https://1.2.3.4:1234", "test-cluster", "myOrg1") + config := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://1.2.3.4:1234", "test-cluster", "myOrg1") // create a config with another CA anotherCaCert, anotherCaKey := certstestutil.SetupCertificateAuthority(t) - configWithAnotherClusterCa := setupdKubeConfigWithClientAuth(t, anotherCaCert, anotherCaKey, "https://1.2.3.4:1234", "test-cluster", "myOrg1") + configWithAnotherClusterCa := setupKubeConfigWithClientAuth(t, anotherCaCert, anotherCaKey, notAfter, "https://1.2.3.4:1234", "test-cluster", "myOrg1") // create a config with another server URL - configWithAnotherServerURL := setupdKubeConfigWithClientAuth(t, caCert, caKey, "https://4.3.2.1:4321", "test-cluster", "myOrg1") + configWithAnotherServerURL := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://4.3.2.1:4321", "test-cluster", "myOrg1") tests := map[string]struct { filesToWrite map[string]*clientcmdapi.Config @@ -697,19 +713,20 @@ func TestValidateKubeconfigsForExternalCA(t *testing.T) { } } -// setupdKubeConfigWithClientAuth is a test utility function that wraps buildKubeConfigFromSpec for building a KubeConfig object With ClientAuth -func setupdKubeConfigWithClientAuth(t *testing.T, caCert *x509.Certificate, caKey crypto.Signer, APIServer, clientName, clustername string, organizations ...string) *clientcmdapi.Config { +// setupKubeConfigWithClientAuth is a test utility function that wraps buildKubeConfigFromSpec for building a KubeConfig object With ClientAuth +func setupKubeConfigWithClientAuth(t *testing.T, caCert *x509.Certificate, caKey crypto.Signer, notAfter time.Time, apiServer, clientName, clustername string, organizations ...string) *clientcmdapi.Config { spec := &kubeConfigSpec{ CACert: caCert, - APIServer: APIServer, + APIServer: apiServer, ClientName: clientName, ClientCertAuth: &clientCertAuth{ CAKey: caKey, Organizations: organizations, }, + ClientCertNotAfter: notAfter, } - config, err := buildKubeConfigFromSpec(spec, clustername, nil) + config, err := buildKubeConfigFromSpec(spec, clustername) if err != nil { t.Fatal("buildKubeConfigFromSpec failed!") } @@ -717,18 +734,18 @@ func setupdKubeConfigWithClientAuth(t *testing.T, caCert *x509.Certificate, caKe return config } -// setupdKubeConfigWithClientAuth is a test utility function that wraps buildKubeConfigFromSpec for building a KubeConfig object With Token -func setupdKubeConfigWithTokenAuth(t *testing.T, caCert *x509.Certificate, APIServer, clientName, token, clustername string) *clientcmdapi.Config { +// setupKubeConfigWithClientAuth is a test utility function that wraps buildKubeConfigFromSpec for building a KubeConfig object With Token +func setupKubeConfigWithTokenAuth(t *testing.T, caCert *x509.Certificate, apiServer, clientName, token, clustername string) *clientcmdapi.Config { spec := &kubeConfigSpec{ CACert: caCert, - APIServer: APIServer, + APIServer: apiServer, ClientName: clientName, TokenAuth: &tokenAuth{ Token: token, }, } - config, err := buildKubeConfigFromSpec(spec, clustername, nil) + config, err := buildKubeConfigFromSpec(spec, clustername) if err != nil { t.Fatal("buildKubeConfigFromSpec failed!") } diff --git a/cmd/kubeadm/app/util/certs/util.go b/cmd/kubeadm/app/util/certs/util.go index 8d6fff4ffc7..78198f02f18 100644 --- a/cmd/kubeadm/app/util/certs/util.go +++ b/cmd/kubeadm/app/util/certs/util.go @@ -23,6 +23,7 @@ import ( "net" "path/filepath" "testing" + "time" certutil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" @@ -51,6 +52,26 @@ func AssertCertificateIsSignedByCa(t *testing.T, cert *x509.Certificate, signing } } +// AssertCertificateHasNotBefore is a utility function for kubeadm testing that asserts if a given certificate has +// the expected NotBefore. Truncate (round) expectedNotBefore to 1 second, since the certificate stores +// with seconds as the maximum precision. +func AssertCertificateHasNotBefore(t *testing.T, cert *x509.Certificate, expectedNotBefore time.Time) { + truncated := expectedNotBefore.Truncate(time.Second) + if !cert.NotBefore.Equal(truncated) { + t.Errorf("cert has NotBefore %v, expected %v", cert.NotBefore, truncated) + } +} + +// AssertCertificateHasNotAfter is a utility function for kubeadm testing that asserts if a given certificate has +// the expected NotAfter. Truncate (round) expectedNotAfter to 1 second, since the certificate stores +// with seconds as the maximum precision. +func AssertCertificateHasNotAfter(t *testing.T, cert *x509.Certificate, expectedNotAfter time.Time) { + truncated := expectedNotAfter.Truncate(time.Second) + if !cert.NotAfter.Equal(truncated) { + t.Errorf("cert has NotAfter %v, expected %v", cert.NotAfter, truncated) + } +} + // AssertCertificateHasCommonName is a utility function for kubeadm testing that asserts if a given certificate has // the expected SubjectCommonName func AssertCertificateHasCommonName(t *testing.T, cert *x509.Certificate, commonName string) { diff --git a/cmd/kubeadm/app/util/pkiutil/pki_helpers.go b/cmd/kubeadm/app/util/pkiutil/pki_helpers.go index 564ac20ddf7..694e0bc9ecd 100644 --- a/cmd/kubeadm/app/util/pkiutil/pki_helpers.go +++ b/cmd/kubeadm/app/util/pkiutil/pki_helpers.go @@ -62,7 +62,7 @@ const ( // CertConfig is a wrapper around certutil.Config extending it with EncryptionAlgorithm. type CertConfig struct { certutil.Config - NotAfter *time.Time + NotAfter time.Time EncryptionAlgorithm kubeadmapi.EncryptionAlgorithmType } @@ -72,10 +72,7 @@ func NewCertificateAuthority(config *CertConfig) (*x509.Certificate, crypto.Sign if err != nil { return nil, nil, errors.Wrap(err, "unable to create private key while generating CA certificate") } - - // backdate CA certificate to allow small time jumps - config.Config.NotBefore = time.Now().Add(-kubeadmconstants.CertificateBackdate) - cert, err := certutil.NewSelfSignedCACert(config.Config, key) + cert, err := NewSelfSignedCACert(config, key) if err != nil { return nil, nil, errors.Wrap(err, "unable to create self-signed CA certificate") } @@ -655,9 +652,14 @@ func NewSignedCert(cfg *CertConfig, key crypto.Signer, caCert *x509.Certificate, RemoveDuplicateAltNames(&cfg.AltNames) - notAfter := time.Now().Add(kubeadmconstants.CertificateValidity).UTC() - if cfg.NotAfter != nil { - notAfter = *cfg.NotAfter + notBefore := caCert.NotBefore + if !cfg.NotBefore.IsZero() { + notBefore = cfg.NotBefore + } + + notAfter := notBefore.Add(kubeadmconstants.CertificateValidityPeriod) + if !cfg.NotAfter.IsZero() { + notAfter = cfg.NotAfter } certTmpl := x509.Certificate{ @@ -668,7 +670,7 @@ func NewSignedCert(cfg *CertConfig, key crypto.Signer, caCert *x509.Certificate, DNSNames: cfg.AltNames.DNSNames, IPAddresses: cfg.AltNames.IPs, SerialNumber: serial, - NotBefore: caCert.NotBefore, + NotBefore: notBefore, NotAfter: notAfter, KeyUsage: keyUsage, ExtKeyUsage: cfg.Usages, @@ -682,6 +684,48 @@ func NewSignedCert(cfg *CertConfig, key crypto.Signer, caCert *x509.Certificate, return x509.ParseCertificate(certDERBytes) } +// NewSelfSignedCACert creates a new self-signed CA certificate +func NewSelfSignedCACert(cfg *CertConfig, key crypto.Signer) (*x509.Certificate, error) { + // returns a uniform random value in [0, max-1), then add 1 to serial to make it a uniform random value in [1, max). + serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64-1)) + if err != nil { + return nil, err + } + serial = new(big.Int).Add(serial, big.NewInt(1)) + + keyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign + + notBefore := time.Now().UTC() + if !cfg.NotBefore.IsZero() { + notBefore = cfg.NotBefore + } + + notAfter := notBefore.Add(kubeadmconstants.CACertificateValidityPeriod) + if !cfg.NotAfter.IsZero() { + notAfter = cfg.NotAfter + } + + tmpl := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + DNSNames: []string{cfg.CommonName}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: keyUsage, + BasicConstraintsValid: true, + IsCA: true, + } + + certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key) + if err != nil { + return nil, err + } + return x509.ParseCertificate(certDERBytes) +} + // RemoveDuplicateAltNames removes duplicate items in altNames. func RemoveDuplicateAltNames(altNames *certutil.AltNames) { if altNames == nil { @@ -707,7 +751,7 @@ func RemoveDuplicateAltNames(altNames *certutil.AltNames) { // (+/- offset) func ValidateCertPeriod(cert *x509.Certificate, offset time.Duration) error { period := fmt.Sprintf("NotBefore: %v, NotAfter: %v", cert.NotBefore, cert.NotAfter) - now := time.Now().Add(offset) + now := time.Now().Add(offset).UTC() if now.Before(cert.NotBefore) { return errors.Errorf("the certificate is not valid yet: %s", period) } diff --git a/cmd/kubeadm/app/util/starttime.go b/cmd/kubeadm/app/util/starttime.go new file mode 100644 index 00000000000..07eb9c5bcd9 --- /dev/null +++ b/cmd/kubeadm/app/util/starttime.go @@ -0,0 +1,36 @@ +/* +Copyright 2024 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 util contains kubeadm utilities. +package util + +import ( + "time" +) + +// startTime is a variable that represents the start time of the kubeadm process. +// It can be used to consistently use the same start time instead of calling time.Now() +// in multiple locations and ending up with minor time deviations. +var startTime time.Time + +func init() { + startTime = time.Now() +} + +// StartTimeUTC returns startTime with its location set to UTC. +func StartTimeUTC() time.Time { + return startTime.UTC() +} diff --git a/cmd/kubeadm/test/kubeconfig/util.go b/cmd/kubeadm/test/kubeconfig/util.go index 147a3b0a299..0e4313cd70b 100644 --- a/cmd/kubeadm/test/kubeconfig/util.go +++ b/cmd/kubeadm/test/kubeconfig/util.go @@ -20,6 +20,7 @@ import ( "crypto/x509" "encoding/pem" "testing" + "time" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" @@ -57,7 +58,7 @@ func AssertKubeConfigCurrentCluster(t *testing.T, config *clientcmdapi.Config, e // AssertKubeConfigCurrentAuthInfoWithClientCert is a utility function for kubeadm testing that asserts if the CurrentAuthInfo in // the given KubeConfig object contains a clientCert that refers to a specific client name, is signed by the expected CA, includes the expected organizations -func AssertKubeConfigCurrentAuthInfoWithClientCert(t *testing.T, config *clientcmdapi.Config, signinCa *x509.Certificate, expectedClientName string, expectedOrganizations ...string) { +func AssertKubeConfigCurrentAuthInfoWithClientCert(t *testing.T, config *clientcmdapi.Config, signinCa *x509.Certificate, expectedNotAfter time.Time, expectedClientName string, expectedOrganizations ...string) { currentContext := config.Contexts[config.CurrentContext] currentAuthInfo := config.AuthInfos[currentContext.AuthInfo] @@ -77,6 +78,9 @@ func AssertKubeConfigCurrentAuthInfoWithClientCert(t *testing.T, config *clientc // Asserts the clientCert is signed by the signinCa certstestutil.AssertCertificateIsSignedByCa(t, currentClientCert, signinCa) + // Assert the clientCert has expected NotAfter + certstestutil.AssertCertificateHasNotAfter(t, currentClientCert, expectedNotAfter) + // Asserts the clientCert has ClientAuth ExtKeyUsage certstestutil.AssertCertificateHasClientAuthUsage(t, currentClientCert)