diff --git a/cmd/kubeadm/app/phases/upgrade/BUILD b/cmd/kubeadm/app/phases/upgrade/BUILD index 74637b304ea..ddec71126b0 100644 --- a/cmd/kubeadm/app/phases/upgrade/BUILD +++ b/cmd/kubeadm/app/phases/upgrade/BUILD @@ -25,6 +25,7 @@ go_library( "//cmd/kubeadm/app/phases/bootstraptoken/clusterinfo:go_default_library", "//cmd/kubeadm/app/phases/bootstraptoken/node:go_default_library", "//cmd/kubeadm/app/phases/certs:go_default_library", + "//cmd/kubeadm/app/phases/certs/renewal:go_default_library", "//cmd/kubeadm/app/phases/controlplane:go_default_library", "//cmd/kubeadm/app/phases/etcd:go_default_library", "//cmd/kubeadm/app/phases/kubelet:go_default_library", @@ -79,12 +80,14 @@ go_test( "//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library", "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/phases/certs:go_default_library", + "//cmd/kubeadm/app/phases/certs/pkiutil:go_default_library", "//cmd/kubeadm/app/phases/controlplane:go_default_library", "//cmd/kubeadm/app/phases/etcd:go_default_library", "//cmd/kubeadm/app/util/apiclient:go_default_library", "//cmd/kubeadm/app/util/config:go_default_library", "//cmd/kubeadm/app/util/etcd:go_default_library", "//cmd/kubeadm/test:go_default_library", + "//cmd/kubeadm/test/certs:go_default_library", "//pkg/util/version:go_default_library", "//staging/src/k8s.io/api/apps/v1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", diff --git a/cmd/kubeadm/app/phases/upgrade/staticpods.go b/cmd/kubeadm/app/phases/upgrade/staticpods.go index 900b39c98bd..52cca578152 100644 --- a/cmd/kubeadm/app/phases/upgrade/staticpods.go +++ b/cmd/kubeadm/app/phases/upgrade/staticpods.go @@ -25,6 +25,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" "k8s.io/kubernetes/cmd/kubeadm/app/constants" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal" controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane" etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd" "k8s.io/kubernetes/cmd/kubeadm/app/util" @@ -185,31 +186,8 @@ func upgradeComponent(component string, waiter apiclient.Waiter, pathMgr StaticP } } - if cfg.Etcd.Local != nil { - // ensure etcd certs are generated for etcd and kube-apiserver - if component == constants.Etcd || component == constants.KubeAPIServer { - caCert, caKey, err := certsphase.KubeadmCertEtcdCA.CreateAsCA(cfg) - if err != nil { - return fmt.Errorf("failed to upgrade the %s CA certificate and key: %v", constants.Etcd, err) - } - - if component == constants.Etcd { - if err := certsphase.KubeadmCertEtcdServer.CreateFromCA(cfg, caCert, caKey); err != nil { - return fmt.Errorf("failed to upgrade the %s certificate and key: %v", constants.Etcd, err) - } - if err := certsphase.KubeadmCertEtcdPeer.CreateFromCA(cfg, caCert, caKey); err != nil { - return fmt.Errorf("failed to upgrade the %s peer certificate and key: %v", constants.Etcd, err) - } - if err := certsphase.KubeadmCertEtcdHealthcheck.CreateFromCA(cfg, caCert, caKey); err != nil { - return fmt.Errorf("failed to upgrade the %s healthcheck certificate and key: %v", constants.Etcd, err) - } - } - if component == constants.KubeAPIServer { - if err := certsphase.KubeadmCertEtcdAPIClient.CreateFromCA(cfg, caCert, caKey); err != nil { - return fmt.Errorf("failed to upgrade the %s %s-client certificate and key: %v", constants.KubeAPIServer, constants.Etcd, err) - } - } - } + if err := renewCerts(cfg, component); err != nil { + return fmt.Errorf("failed to renew certificates for component %q: %v", component, err) } // The old manifest is here; in the /etc/kubernetes/manifests/ @@ -524,3 +502,35 @@ func rollbackEtcdData(cfg *kubeadmapi.InitConfiguration, pathMgr StaticPodPathMa return nil } + +func renewCerts(cfg *kubeadmapi.InitConfiguration, component string) error { + if cfg.Etcd.Local != nil { + // ensure etcd certs are loaded for etcd and kube-apiserver + if component == constants.Etcd || component == constants.KubeAPIServer { + caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertEtcdCA.BaseName) + if err != nil { + return fmt.Errorf("failed to upgrade the %s CA certificate and key: %v", constants.Etcd, err) + } + renewer := renewal.NewFileRenewal(caCert, caKey) + + if component == constants.Etcd { + for _, cert := range []*certsphase.KubeadmCert{ + &certsphase.KubeadmCertEtcdServer, + &certsphase.KubeadmCertEtcdPeer, + &certsphase.KubeadmCertEtcdHealthcheck, + } { + if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil { + return fmt.Errorf("failed to renew %s certificate and key: %v", cert.Name, err) + } + } + } + if component == constants.KubeAPIServer { + cert := certsphase.KubeadmCertEtcdAPIClient + if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil { + return fmt.Errorf("failed to renew %s certificate and key: %v", cert.Name, err) + } + } + } + } + return nil +} diff --git a/cmd/kubeadm/app/phases/upgrade/staticpods_test.go b/cmd/kubeadm/app/phases/upgrade/staticpods_test.go index 1eb41f35cb2..615534c3a03 100644 --- a/cmd/kubeadm/app/phases/upgrade/staticpods_test.go +++ b/cmd/kubeadm/app/phases/upgrade/staticpods_test.go @@ -20,6 +20,7 @@ import ( "crypto/sha256" "fmt" "io/ioutil" + "math/big" "os" "path/filepath" "strings" @@ -32,11 +33,14 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation" "k8s.io/kubernetes/cmd/kubeadm/app/constants" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane" etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" etcdutil "k8s.io/kubernetes/cmd/kubeadm/app/util/etcd" + testutil "k8s.io/kubernetes/cmd/kubeadm/test" + certstestutil "k8s.io/kubernetes/cmd/kubeadm/test/certs" ) const ( @@ -456,6 +460,22 @@ func TestStaticPodControlPlane(t *testing.T) { t.Fatalf("couldn't create config: %v", err) } + // create the kubeadm etcd certs + caCert, caKey, err := certsphase.KubeadmCertEtcdCA.CreateAsCA(newcfg) + if err != nil { + t.Fatalf("couldn't create new CA certificate: %v", err) + } + for _, cert := range []*certsphase.KubeadmCert{ + &certsphase.KubeadmCertEtcdServer, + &certsphase.KubeadmCertEtcdPeer, + &certsphase.KubeadmCertEtcdHealthcheck, + &certsphase.KubeadmCertEtcdAPIClient, + } { + if err := cert.CreateFromCA(newcfg, caCert, caKey); err != nil { + t.Fatalf("couldn't create certificate %s: %v", cert.Name, err) + } + } + actualErr := StaticPodControlPlane( waiter, pathMgr, @@ -606,3 +626,113 @@ func TestCleanupDirs(t *testing.T) { }) } } + +func TestRenewCerts(t *testing.T) { + caCert, caKey := certstestutil.SetupCertificateAuthorithy(t) + t.Run("all certs exist, should be rotated", func(t *testing.T) { + }) + tests := []struct { + name string + component string + skipCreateCA bool + shouldErrorOnRenew bool + certsShouldExist []*certsphase.KubeadmCert + }{ + { + name: "all certs exist, should be rotated", + component: constants.Etcd, + certsShouldExist: []*certsphase.KubeadmCert{ + &certsphase.KubeadmCertEtcdServer, + &certsphase.KubeadmCertEtcdPeer, + &certsphase.KubeadmCertEtcdHealthcheck, + }, + }, + { + name: "just renew API cert", + component: constants.KubeAPIServer, + certsShouldExist: []*certsphase.KubeadmCert{ + &certsphase.KubeadmCertEtcdAPIClient, + }, + }, + { + name: "ignores other compnonents", + skipCreateCA: true, + component: constants.KubeScheduler, + }, + { + name: "missing a cert to renew", + component: constants.Etcd, + shouldErrorOnRenew: true, + certsShouldExist: []*certsphase.KubeadmCert{ + &certsphase.KubeadmCertEtcdServer, + &certsphase.KubeadmCertEtcdPeer, + }, + }, + { + name: "no CA, cannot continue", + component: constants.Etcd, + skipCreateCA: true, + shouldErrorOnRenew: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Setup up basic requities + tmpDir := testutil.SetupTempDir(t) + defer os.RemoveAll(tmpDir) + + cfg := testutil.GetDefaultInternalConfig(t) + cfg.CertificatesDir = tmpDir + + if !test.skipCreateCA { + if err := pkiutil.WriteCertAndKey(tmpDir, constants.EtcdCACertAndKeyBaseName, caCert, caKey); err != nil { + t.Fatalf("couldn't write out CA: %v", err) + } + } + + // Create expected certs + for _, kubeCert := range test.certsShouldExist { + if err := kubeCert.CreateFromCA(cfg, caCert, caKey); err != nil { + t.Fatalf("couldn't renew certificate %q: %v", kubeCert.Name, err) + } + } + + // Load expected certs to check if serial numbers changes + certMaps := make(map[*certsphase.KubeadmCert]big.Int) + for _, kubeCert := range test.certsShouldExist { + cert, err := pkiutil.TryLoadCertFromDisk(tmpDir, kubeCert.BaseName) + if err != nil { + t.Fatalf("couldn't load certificate %q: %v", kubeCert.Name, err) + } + certMaps[kubeCert] = *cert.SerialNumber + } + + // Renew everything + err := renewCerts(cfg, test.component) + if test.shouldErrorOnRenew { + if err == nil { + t.Fatal("expected renewal error, got nothing") + } + // expected error, got error + return + } + if err != nil { + t.Fatalf("couldn't renew certificates: %v", err) + } + + // See if the certificate serial numbers change + for kubeCert, cert := range certMaps { + newCert, err := pkiutil.TryLoadCertFromDisk(tmpDir, kubeCert.BaseName) + if err != nil { + t.Errorf("couldn't load new certificate %q: %v", kubeCert.Name, err) + continue + } + if cert.Cmp(newCert.SerialNumber) == 0 { + t.Errorf("certifitate %v was not reissued", kubeCert.Name) + } + } + }) + + } +}