diff --git a/cmd/kubeadm/app/cmd/alpha/certs.go b/cmd/kubeadm/app/cmd/alpha/certs.go index fbd3b007748..937b99df3ee 100644 --- a/cmd/kubeadm/app/cmd/alpha/certs.go +++ b/cmd/kubeadm/app/cmd/alpha/certs.go @@ -19,14 +19,16 @@ package alpha import ( "fmt" + "github.com/pkg/errors" "github.com/spf13/cobra" + + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmconstants "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" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" @@ -36,14 +38,16 @@ import ( var ( genericCertRenewLongDesc = normalizer.LongDesc(` - Renew the %[1]s, and save them into %[2]s.cert and %[2]s.key files. + Renew the %s. - Extra attributes such as SANs will be based on the existing certificates, there is no need to resupply them. -`) - genericCertRenewEmbeddedLongDesc = normalizer.LongDesc(` -Renew the certificate embedded in the kubeconfig file %s. + Renewals run unconditionally, regardless of certificate expiration date; extra attributes such as SANs will + be based on the existing file/certificates, there is no need to resupply them. -Kubeconfig attributes and certificate extra attributes such as SANs will be based on the existing kubeconfig/certificates, there is no need to resupply them. + Renewal by default tries to use the certificate authority in the local PKI managed by kubeadm; as alternative + it is possible to use K8s certificate API for certificate renewal, or as a last option, to generate a CSR request. + + After renewal, in order to make changes effective, is is required to restart control-plane components and + eventually re-distribute the renewed certificate in case the file is used elsewhere. `) allLongDesc = normalizer.LongDesc(` @@ -78,17 +82,17 @@ func newCmdCertsRenewal() *cobra.Command { return cmd } -type renewConfig struct { +type renewFlags struct { cfgPath string kubeconfigPath string cfg kubeadmapiv1beta2.InitConfiguration useAPI bool - useCSR bool + csrOnly bool csrPath string } func getRenewSubCommands(kdir string) []*cobra.Command { - cfg := &renewConfig{ + flags := &renewFlags{ cfg: kubeadmapiv1beta2.InitConfiguration{ ClusterConfiguration: kubeadmapiv1beta2.ClusterConfiguration{ // Setting kubernetes version to a default value in order to allow a not necessary internet lookup @@ -97,45 +101,28 @@ func getRenewSubCommands(kdir string) []*cobra.Command { }, } // Default values for the cobra help text - kubeadmscheme.Scheme.Default(&cfg.cfg) + kubeadmscheme.Scheme.Default(&flags.cfg) - certTree, err := certsphase.GetDefaultCertList().AsMap().CertTree() + // Get a renewal manager for a generic Cluster configuration, that is used only for getting + // the list of certificates for building subcommands + rm, err := renewal.NewManager(&kubeadmapi.ClusterConfiguration{}, "") kubeadmutil.CheckErr(err) cmdList := []*cobra.Command{} funcList := []func(){} - for caCert, certs := range certTree { - // Don't offer to renew CAs; would cause serious consequences - for _, cert := range certs { - // get the cobra.Command skeleton for this command - cmd := generateCertRenewalCommand(cert, cfg) - // get the implementation of renewing this certificate - renewalFunc := func(cert *certsphase.KubeadmCert, caCert *certsphase.KubeadmCert) func() { - return func() { renewCert(cert, caCert, cfg) } - }(cert, caCert) - // install the implementation into the command - cmd.Run = func(*cobra.Command, []string) { renewalFunc() } - cmdList = append(cmdList, cmd) - // Collect renewal functions for `renew all` - funcList = append(funcList, renewalFunc) - } - } - - kubeconfigs := []string{ - kubeadmconstants.AdminKubeConfigFileName, - kubeadmconstants.ControllerManagerKubeConfigFileName, - kubeadmconstants.SchedulerKubeConfigFileName, - //NB. we are escluding KubeletKubeConfig from renewal because management of this certificate is delegated to kubelet - } - - for _, k := range kubeconfigs { + for _, handler := range rm.Certificates() { // get the cobra.Command skeleton for this command - cmd := generateEmbeddedCertRenewalCommand(k, cfg) + cmd := &cobra.Command{ + Use: handler.Name, + Short: fmt.Sprintf("Renew the %s", handler.LongName), + Long: fmt.Sprintf(genericCertRenewLongDesc, handler.LongName), + } + addFlags(cmd, flags) // get the implementation of renewing this certificate - renewalFunc := func(kdir, k string) func() { - return func() { renewEmbeddedCert(kdir, k, cfg) } - }(kdir, k) + renewalFunc := func(handler *renewal.CertificateRenewHandler) func() { + return func() { renewCert(flags, kdir, handler) } + }(handler) // install the implementation into the command cmd.Run = func(*cobra.Command, []string) { renewalFunc() } cmdList = append(cmdList, cmd) @@ -153,134 +140,60 @@ func getRenewSubCommands(kdir string) []*cobra.Command { } }, } - addFlags(allCmd, cfg) + addFlags(allCmd, flags) cmdList = append(cmdList, allCmd) return cmdList } -func addFlags(cmd *cobra.Command, cfg *renewConfig) { - options.AddConfigFlag(cmd.Flags(), &cfg.cfgPath) - options.AddCertificateDirFlag(cmd.Flags(), &cfg.cfg.CertificatesDir) - options.AddKubeConfigFlag(cmd.Flags(), &cfg.kubeconfigPath) - options.AddCSRFlag(cmd.Flags(), &cfg.useCSR) - options.AddCSRDirFlag(cmd.Flags(), &cfg.csrPath) - cmd.Flags().BoolVar(&cfg.useAPI, "use-api", cfg.useAPI, "Use the Kubernetes certificate API to renew certificates") +func addFlags(cmd *cobra.Command, flags *renewFlags) { + options.AddConfigFlag(cmd.Flags(), &flags.cfgPath) + options.AddCertificateDirFlag(cmd.Flags(), &flags.cfg.CertificatesDir) + options.AddKubeConfigFlag(cmd.Flags(), &flags.kubeconfigPath) + options.AddCSRFlag(cmd.Flags(), &flags.csrOnly) + options.AddCSRDirFlag(cmd.Flags(), &flags.csrPath) + cmd.Flags().BoolVar(&flags.useAPI, "use-api", flags.useAPI, "Use the Kubernetes certificate API to renew certificates") } -func renewCert(cert *certsphase.KubeadmCert, caCert *certsphase.KubeadmCert, cfg *renewConfig) { - internalcfg, err := configutil.LoadOrDefaultInitConfiguration(cfg.cfgPath, &cfg.cfg) +func renewCert(flags *renewFlags, kdir string, handler *renewal.CertificateRenewHandler) { + internalcfg, err := configutil.LoadOrDefaultInitConfiguration(flags.cfgPath, &flags.cfg) kubeadmutil.CheckErr(err) - // if the renewal operation is set to generate only CSR request - if cfg.useCSR { - // trigger CSR generation in the csrPath, or if this one is missing, in the CertificateDir - path := cfg.csrPath - if path == "" { - path = cfg.cfg.CertificatesDir + // Get a renewal manager for the given cluster configuration + rm, err := renewal.NewManager(&internalcfg.ClusterConfiguration, kdir) + kubeadmutil.CheckErr(err) + + // if the renewal operation is set to generate CSR request only + if flags.csrOnly { + // checks a path for storing CSR request is given + if flags.csrPath == "" { + kubeadmutil.CheckErr(errors.New("please provide a path where CSR request should be stored")) } - err := certsphase.CreateCSR(cert, internalcfg, path) + err := rm.CreateRenewCSR(handler.Name, flags.csrPath) kubeadmutil.CheckErr(err) return } // otherwise, the renewal operation has to actually renew a certificate - var externalCA bool - switch caCert.BaseName { - case kubeadmconstants.CACertAndKeyBaseName: - // Check if an external CA is provided by the user (when the CA Cert is present but the CA Key is not) - externalCA, _ = certsphase.UsingExternalCA(&internalcfg.ClusterConfiguration) - case kubeadmconstants.FrontProxyCACertAndKeyBaseName: - // Check if an external Front-Proxy CA is provided by the user (when the Front-Proxy CA Cert is present but the Front-Proxy CA Key is not) - externalCA, _ = certsphase.UsingExternalFrontProxyCA(&internalcfg.ClusterConfiguration) - default: - externalCA = false - } - - if !externalCA { - renewer, err := getRenewer(cfg, caCert.BaseName) - kubeadmutil.CheckErr(err) - - err = renewal.RenewExistingCert(internalcfg.CertificatesDir, cert.BaseName, renewer) - kubeadmutil.CheckErr(err) - - fmt.Printf("Certificate %s renewed\n", cert.Name) - return - } - - fmt.Printf("Detected external %s, certificate %s can't be renewed\n", cert.CAName, cert.Name) -} - -func renewEmbeddedCert(kdir, k string, cfg *renewConfig) { - internalcfg, err := configutil.LoadOrDefaultInitConfiguration(cfg.cfgPath, &cfg.cfg) - kubeadmutil.CheckErr(err) - - // if the renewal operation is set to generate only CSR request - if cfg.useCSR { - // trigger CSR generation in the csrPath, or if this one is missing, in the CertificateDir - path := cfg.csrPath - if path == "" { - path = cfg.cfg.CertificatesDir - } - err := certsphase.CreateCSR(nil, internalcfg, path) - kubeadmutil.CheckErr(err) - return - } - - // otherwise, the renewal operation has to actually renew a certificate - - // Check if an external CA is provided by the user (when the CA Cert is present but the CA Key is not) - externalCA, _ := certsphase.UsingExternalCA(&internalcfg.ClusterConfiguration) - - if !externalCA { - renewer, err := getRenewer(cfg, certsphase.KubeadmCertRootCA.BaseName) - kubeadmutil.CheckErr(err) - - err = renewal.RenewEmbeddedClientCert(kdir, k, renewer) - kubeadmutil.CheckErr(err) - - fmt.Printf("Certificate embedded in %s renewed\n", k) - return - } - - fmt.Printf("Detected external CA, certificate embedded in %s can't be renewed\n", k) -} - -func generateCertRenewalCommand(cert *certsphase.KubeadmCert, cfg *renewConfig) *cobra.Command { - cmd := &cobra.Command{ - Use: cert.Name, - Short: fmt.Sprintf("Renew the %s", cert.LongName), - Long: fmt.Sprintf(genericCertRenewLongDesc, cert.LongName, cert.BaseName), - } - addFlags(cmd, cfg) - return cmd -} - -func generateEmbeddedCertRenewalCommand(k string, cfg *renewConfig) *cobra.Command { - cmd := &cobra.Command{ - Use: k, - Short: fmt.Sprintf("Renew the certificate embedded in %s", k), - Long: fmt.Sprintf(genericCertRenewEmbeddedLongDesc, k), - } - addFlags(cmd, cfg) - return cmd -} - -func getRenewer(cfg *renewConfig, caCertBaseName string) (renewal.Interface, error) { - if cfg.useAPI { - kubeConfigPath := cmdutil.GetKubeConfigPath(cfg.kubeconfigPath) + // renew the certificate using the requested renew method + if flags.useAPI { + // renew using K8s certificate API + kubeConfigPath := cmdutil.GetKubeConfigPath(flags.kubeconfigPath) client, err := kubeconfigutil.ClientSetFromFile(kubeConfigPath) - if err != nil { - return nil, err + kubeadmutil.CheckErr(err) + + err = rm.RenewUsingCSRAPI(handler.Name, client) + kubeadmutil.CheckErr(err) + } else { + // renew using local certificate authorities. + // this operation can't complete in case the certificate key is not provided (external CA) + renewed, err := rm.RenewUsingLocalCA(handler.Name) + kubeadmutil.CheckErr(err) + if !renewed { + fmt.Printf("Detected external %s, %s can't be renewed\n", handler.CABaseName, handler.LongName) + return } - return renewal.NewCertsAPIRenawal(client), nil } - - caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.cfg.CertificatesDir, caCertBaseName) - if err != nil { - return nil, err - } - - return renewal.NewFileRenewal(caCert, caKey), nil + fmt.Printf("%s renewed\n", handler.LongName) } diff --git a/cmd/kubeadm/app/cmd/alpha/certs_test.go b/cmd/kubeadm/app/cmd/alpha/certs_test.go index ed5a11b7f98..78227813b67 100644 --- a/cmd/kubeadm/app/cmd/alpha/certs_test.go +++ b/cmd/kubeadm/app/cmd/alpha/certs_test.go @@ -55,6 +55,10 @@ func TestCommandsGenerated(t *testing.T) { "renew etcd-server", "renew etcd-peer", "renew etcd-healthcheck-client", + + "renew admin.conf", + "renew scheduler.conf", + "renew controller-manager.conf", } renewCmd := newCmdCertsRenewal() @@ -79,19 +83,63 @@ func TestCommandsGenerated(t *testing.T) { } func TestRunRenewCommands(t *testing.T) { + tmpDir := testutil.SetupTempDir(t) + defer os.RemoveAll(tmpDir) + + cfg := testutil.GetDefaultInternalConfig(t) + cfg.CertificatesDir = tmpDir + + // Generate all the CA + CACerts := map[string]*x509.Certificate{} + CAKeys := map[string]crypto.Signer{} + for _, ca := range []*certsphase.KubeadmCert{ + &certsphase.KubeadmCertRootCA, + &certsphase.KubeadmCertFrontProxyCA, + &certsphase.KubeadmCertEtcdCA, + } { + caCert, caKey, err := ca.CreateAsCA(cfg) + if err != nil { + t.Fatalf("couldn't write out CA %s: %v", ca.Name, err) + } + CACerts[ca.Name] = caCert + CAKeys[ca.Name] = caKey + } + + // Generate all the signed certificates + for _, cert := range []*certsphase.KubeadmCert{ + &certsphase.KubeadmCertAPIServer, + &certsphase.KubeadmCertKubeletClient, + &certsphase.KubeadmCertFrontProxyClient, + &certsphase.KubeadmCertEtcdAPIClient, + &certsphase.KubeadmCertEtcdServer, + &certsphase.KubeadmCertEtcdPeer, + &certsphase.KubeadmCertEtcdHealthcheck, + } { + caCert := CACerts[cert.CAName] + caKey := CAKeys[cert.CAName] + if err := cert.CreateFromCA(cfg, caCert, caKey); err != nil { + t.Fatalf("couldn't write certificate %s: %v", cert.Name, err) + } + } + + // Generate all the kubeconfig files with embedded certs + for _, kubeConfig := range []string{ + kubeadmconstants.AdminKubeConfigFileName, + kubeadmconstants.SchedulerKubeConfigFileName, + kubeadmconstants.ControllerManagerKubeConfigFileName, + } { + if err := kubeconfigphase.CreateKubeConfigFile(kubeConfig, tmpDir, cfg); err != nil { + t.Fatalf("couldn't create kubeconfig %q: %v", kubeConfig, err) + } + } + tests := []struct { command string - CAs []*certsphase.KubeadmCert Certs []*certsphase.KubeadmCert KubeconfigFiles []string }{ { command: "all", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertRootCA, - &certsphase.KubeadmCertFrontProxyCA, - &certsphase.KubeadmCertEtcdCA, - }, Certs: []*certsphase.KubeadmCert{ &certsphase.KubeadmCertAPIServer, &certsphase.KubeadmCertKubeletClient, @@ -109,90 +157,60 @@ func TestRunRenewCommands(t *testing.T) { }, { command: "apiserver", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertRootCA, - }, Certs: []*certsphase.KubeadmCert{ &certsphase.KubeadmCertAPIServer, }, }, { command: "apiserver-kubelet-client", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertRootCA, - }, Certs: []*certsphase.KubeadmCert{ &certsphase.KubeadmCertKubeletClient, }, }, { command: "apiserver-etcd-client", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertEtcdCA, - }, Certs: []*certsphase.KubeadmCert{ &certsphase.KubeadmCertEtcdAPIClient, }, }, { command: "front-proxy-client", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertFrontProxyCA, - }, Certs: []*certsphase.KubeadmCert{ &certsphase.KubeadmCertFrontProxyClient, }, }, { command: "etcd-server", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertEtcdCA, - }, Certs: []*certsphase.KubeadmCert{ &certsphase.KubeadmCertEtcdServer, }, }, { command: "etcd-peer", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertEtcdCA, - }, Certs: []*certsphase.KubeadmCert{ &certsphase.KubeadmCertEtcdPeer, }, }, { command: "etcd-healthcheck-client", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertEtcdCA, - }, Certs: []*certsphase.KubeadmCert{ &certsphase.KubeadmCertEtcdHealthcheck, }, }, { command: "admin.conf", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertRootCA, - }, KubeconfigFiles: []string{ kubeadmconstants.AdminKubeConfigFileName, }, }, { command: "scheduler.conf", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertRootCA, - }, KubeconfigFiles: []string{ kubeadmconstants.SchedulerKubeConfigFileName, }, }, { command: "controller-manager.conf", - CAs: []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertRootCA, - }, KubeconfigFiles: []string{ kubeadmconstants.ControllerManagerKubeConfigFileName, }, @@ -201,74 +219,43 @@ func TestRunRenewCommands(t *testing.T) { for _, test := range tests { t.Run(test.command, func(t *testing.T) { - tmpDir := testutil.SetupTempDir(t) - defer os.RemoveAll(tmpDir) - - cfg := testutil.GetDefaultInternalConfig(t) - cfg.CertificatesDir = tmpDir - - // Generate all the CA - CACerts := map[string]*x509.Certificate{} - CAKeys := map[string]crypto.Signer{} - for _, ca := range test.CAs { - caCert, caKey, err := ca.CreateAsCA(cfg) - if err != nil { - t.Fatalf("couldn't write out CA %s: %v", ca.Name, err) - } - CACerts[ca.Name] = caCert - CAKeys[ca.Name] = caKey - } - - // Generate all the signed certificates (and store creation time) - createTime := map[string]time.Time{} + // Get file ModTime before renew + ModTime := map[string]time.Time{} for _, cert := range test.Certs { - caCert := CACerts[cert.CAName] - caKey := CAKeys[cert.CAName] - if err := cert.CreateFromCA(cfg, caCert, caKey); err != nil { - t.Fatalf("couldn't write certificate %s: %v", cert.Name, err) - } - file, err := os.Stat(filepath.Join(tmpDir, fmt.Sprintf("%s.crt", cert.BaseName))) if err != nil { t.Fatalf("couldn't get certificate %s: %v", cert.Name, err) } - createTime[cert.Name] = file.ModTime() + ModTime[cert.Name] = file.ModTime() } - - // Generate all the kubeconfig files with embedded certs(and store creation time) for _, kubeConfig := range test.KubeconfigFiles { - if err := kubeconfigphase.CreateKubeConfigFile(kubeConfig, tmpDir, cfg); err != nil { - t.Fatalf("couldn't create kubeconfig %q: %v", kubeConfig, err) - } file, err := os.Stat(filepath.Join(tmpDir, kubeConfig)) if err != nil { t.Fatalf("couldn't get kubeconfig %s: %v", kubeConfig, err) } - createTime[kubeConfig] = file.ModTime() + ModTime[kubeConfig] = file.ModTime() } // exec renew renewCmds := getRenewSubCommands(tmpDir) cmdtestutil.RunSubCommand(t, renewCmds, test.command, fmt.Sprintf("--cert-dir=%s", tmpDir)) - // read renewed certificates and check the file is modified + // check the file is modified for _, cert := range test.Certs { file, err := os.Stat(filepath.Join(tmpDir, fmt.Sprintf("%s.crt", cert.BaseName))) if err != nil { t.Fatalf("couldn't get certificate %s: %v", cert.Name, err) } - if createTime[cert.Name] == file.ModTime() { + if ModTime[cert.Name] == file.ModTime() { t.Errorf("certificate %s was not renewed as expected", cert.Name) } } - - // ead renewed kubeconfig files and check the file is modified for _, kubeConfig := range test.KubeconfigFiles { file, err := os.Stat(filepath.Join(tmpDir, kubeConfig)) if err != nil { t.Fatalf("couldn't get kubeconfig %s: %v", kubeConfig, err) } - if createTime[kubeConfig] == file.ModTime() { + if ModTime[kubeConfig] == file.ModTime() { t.Errorf("kubeconfig %s was not renewed as expected", kubeConfig) } } @@ -281,10 +268,22 @@ func TestRenewUsingCSR(t *testing.T) { defer os.RemoveAll(tmpDir) cert := &certs.KubeadmCertEtcdServer - renewCmds := getRenewSubCommands(tmpDir) - cmdtestutil.RunSubCommand(t, renewCmds, cert.Name, "--csr-only", "--csr-dir="+tmpDir) + cfg := testutil.GetDefaultInternalConfig(t) + cfg.CertificatesDir = tmpDir - if _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(tmpDir, cert.BaseName); err != nil { - t.Fatalf("couldn't load certificate %q: %v", cert.BaseName, err) + caCert, caKey, err := certsphase.KubeadmCertEtcdCA.CreateAsCA(cfg) + if err != nil { + t.Fatalf("couldn't write out CA %s: %v", certsphase.KubeadmCertEtcdCA.Name, err) + } + + if err := cert.CreateFromCA(cfg, caCert, caKey); err != nil { + t.Fatalf("couldn't write certificate %s: %v", cert.Name, err) + } + + renewCmds := getRenewSubCommands(tmpDir) + cmdtestutil.RunSubCommand(t, renewCmds, cert.Name, "--csr-only", "--csr-dir="+tmpDir, fmt.Sprintf("--cert-dir=%s", tmpDir)) + + if _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(tmpDir, cert.Name); err != nil { + t.Fatalf("couldn't load certificate %q: %v", cert.Name, err) } } diff --git a/cmd/kubeadm/app/phases/certs/certlist.go b/cmd/kubeadm/app/phases/certs/certlist.go index e58822f541e..65d58d3a88e 100644 --- a/cmd/kubeadm/app/phases/certs/certlist.go +++ b/cmd/kubeadm/app/phases/certs/certlist.go @@ -260,7 +260,7 @@ var ( // KubeadmCertKubeletClient is the definition of the cert used by the API server to access the kubelet. KubeadmCertKubeletClient = KubeadmCert{ Name: "apiserver-kubelet-client", - LongName: "Client certificate for the API server to connect to kubelet", + LongName: "certificate for the API server to connect to kubelet", BaseName: kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName, CAName: "ca", config: certutil.Config{ @@ -284,7 +284,7 @@ var ( KubeadmCertFrontProxyClient = KubeadmCert{ Name: "front-proxy-client", BaseName: kubeadmconstants.FrontProxyClientCertAndKeyBaseName, - LongName: "client for the front proxy", + LongName: "certificate for the front proxy client", CAName: "front-proxy-ca", config: certutil.Config{ CommonName: kubeadmconstants.FrontProxyClientCertCommonName, @@ -322,7 +322,7 @@ var ( // KubeadmCertEtcdPeer is the definition of the cert used by etcd peers to access each other. KubeadmCertEtcdPeer = KubeadmCert{ Name: "etcd-peer", - LongName: "credentials for etcd nodes to communicate with each other", + LongName: "certificate for etcd nodes to communicate with each other", BaseName: kubeadmconstants.EtcdPeerCertAndKeyBaseName, CAName: "etcd-ca", config: certutil.Config{ @@ -336,7 +336,7 @@ var ( // KubeadmCertEtcdHealthcheck is the definition of the cert used by Kubernetes to check the health of the etcd server. KubeadmCertEtcdHealthcheck = KubeadmCert{ Name: "etcd-healthcheck-client", - LongName: "client certificate for liveness probes to healtcheck etcd", + LongName: "certificate for liveness probes to healtcheck etcd", BaseName: kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName, CAName: "etcd-ca", config: certutil.Config{ @@ -348,7 +348,7 @@ var ( // KubeadmCertEtcdAPIClient is the definition of the cert used by the API server to access etcd. KubeadmCertEtcdAPIClient = KubeadmCert{ Name: "apiserver-etcd-client", - LongName: "client apiserver uses to access etcd", + LongName: "certificate the apiserver uses to access etcd", BaseName: kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName, CAName: "etcd-ca", config: certutil.Config{ diff --git a/cmd/kubeadm/app/phases/certs/renewal/certsapi.go b/cmd/kubeadm/app/phases/certs/renewal/apirenewer.go similarity index 88% rename from cmd/kubeadm/app/phases/certs/renewal/certsapi.go rename to cmd/kubeadm/app/phases/certs/renewal/apirenewer.go index bb61cdd6428..d118420ed15 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/certsapi.go +++ b/cmd/kubeadm/app/phases/certs/renewal/apirenewer.go @@ -27,7 +27,7 @@ import ( certsapi "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" + clientset "k8s.io/client-go/kubernetes" certstype "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" certutil "k8s.io/client-go/util/cert" csrutil "k8s.io/client-go/util/certificate/csr" @@ -38,20 +38,20 @@ const certAPIPrefixName = "kubeadm-cert" var watchTimeout = 5 * time.Minute -// CertsAPIRenewal creates new certificates using the certs API -type CertsAPIRenewal struct { +// APIRenewer define a certificate renewer implementation that uses the K8s certificate API +type APIRenewer struct { client certstype.CertificatesV1beta1Interface } -// NewCertsAPIRenawal takes a Kubernetes interface and returns a renewal Interface. -func NewCertsAPIRenawal(client kubernetes.Interface) Interface { - return &CertsAPIRenewal{ +// NewAPIRenewer a new certificate renewer implementation that uses the K8s certificate API +func NewAPIRenewer(client clientset.Interface) *APIRenewer { + return &APIRenewer{ client: client.CertificatesV1beta1(), } } -// Renew takes a certificate using the cert and key. -func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) { +// Renew a certificate using the K8s certificate API +func (r *APIRenewer) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) { reqTmp := &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: cfg.CommonName, diff --git a/cmd/kubeadm/app/phases/certs/renewal/apirenewer_test.go b/cmd/kubeadm/app/phases/certs/renewal/apirenewer_test.go new file mode 100644 index 00000000000..761db0bd04a --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/apirenewer_test.go @@ -0,0 +1,119 @@ +/* +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 renewal + +import ( + "crypto" + "crypto/x509" + "testing" + "time" + + certsapi "k8s.io/api/certificates/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + fakecerts "k8s.io/client-go/kubernetes/typed/certificates/v1beta1/fake" + k8stesting "k8s.io/client-go/testing" + certutil "k8s.io/client-go/util/cert" + pkiutil "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" +) + +func TestAPIRenewer(t *testing.T) { + caCertCfg := &certutil.Config{CommonName: "kubernetes"} + caCert, caKey, err := pkiutil.NewCertificateAuthority(caCertCfg) + if err != nil { + t.Fatalf("couldn't create CA: %v", err) + } + + client := &fakecerts.FakeCertificatesV1beta1{ + Fake: &k8stesting.Fake{}, + } + certReq := getCertReq(t, caCert, caKey) + certReqNoCert := certReq.DeepCopy() + certReqNoCert.Status.Certificate = nil + client.AddReactor("get", "certificatesigningrequests", defaultReactionFunc(certReq)) + watcher := watch.NewFakeWithChanSize(3, false) + watcher.Add(certReqNoCert) + watcher.Modify(certReqNoCert) + watcher.Modify(certReq) + client.AddWatchReactor("certificatesigningrequests", k8stesting.DefaultWatchReactor(watcher, nil)) + + // override the timeout so tests are faster + watchTimeout = time.Second + + certCfg := &certutil.Config{ + CommonName: "test-certs", + AltNames: certutil.AltNames{ + DNSNames: []string{"test-domain.space"}, + }, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + renewer := &APIRenewer{ + client: client, + } + + cert, _, err := renewer.Renew(certCfg) + if err != nil { + t.Fatalf("unexpected error renewing cert: %v", err) + } + + pool := x509.NewCertPool() + pool.AddCert(caCert) + + _, err = cert.Verify(x509.VerifyOptions{ + DNSName: "test-domain.space", + Roots: pool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + if err != nil { + t.Errorf("couldn't verify new cert: %v", err) + } +} + +func defaultReactionFunc(obj runtime.Object) k8stesting.ReactionFunc { + return func(act k8stesting.Action) (bool, runtime.Object, error) { + return true, obj, nil + } +} + +func getCertReq(t *testing.T, caCert *x509.Certificate, caKey crypto.Signer) *certsapi.CertificateSigningRequest { + cert, _, err := pkiutil.NewCertAndKey(caCert, caKey, &certutil.Config{ + CommonName: "testcert", + AltNames: certutil.AltNames{ + DNSNames: []string{"test-domain.space"}, + }, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + if err != nil { + t.Fatalf("couldn't generate cert: %v", err) + } + + return &certsapi.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testcert", + }, + Status: certsapi.CertificateSigningRequestStatus{ + Conditions: []certsapi.CertificateSigningRequestCondition{ + { + Type: certsapi.CertificateApproved, + }, + }, + Certificate: pkiutil.EncodeCertPEM(cert), + }, + } +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go b/cmd/kubeadm/app/phases/certs/renewal/filerenewer.go similarity index 64% rename from cmd/kubeadm/app/phases/certs/renewal/filerenewal.go rename to cmd/kubeadm/app/phases/certs/renewal/filerenewer.go index 66fd9b3a3c1..5a71393d136 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go +++ b/cmd/kubeadm/app/phases/certs/renewal/filerenewer.go @@ -24,21 +24,21 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" ) -// FileRenewal renews a certificate using local certs -type FileRenewal struct { +// FileRenewer define a certificate renewer implementation that uses given CA cert and key for generating new certficiates +type FileRenewer struct { caCert *x509.Certificate caKey crypto.Signer } -// NewFileRenewal takes a certificate pair to construct the Interface. -func NewFileRenewal(caCert *x509.Certificate, caKey crypto.Signer) Interface { - return &FileRenewal{ +// NewFileRenewer returns a new certificate renewer that uses given CA cert and key for generating new certficiates +func NewFileRenewer(caCert *x509.Certificate, caKey crypto.Signer) *FileRenewer { + return &FileRenewer{ caCert: caCert, caKey: caKey, } } -// Renew takes a certificate using the cert and key -func (r *FileRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) { +// Renew a certificate using a given CA cert and key +func (r *FileRenewer) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) { return pkiutil.NewCertAndKey(r.caCert, r.caKey, cfg) } diff --git a/cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go b/cmd/kubeadm/app/phases/certs/renewal/filerenewer_test.go similarity index 78% rename from cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go rename to cmd/kubeadm/app/phases/certs/renewal/filerenewer_test.go index 29d92e78c30..341e8cfc3bb 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go +++ b/cmd/kubeadm/app/phases/certs/renewal/filerenewer_test.go @@ -21,18 +21,13 @@ import ( "testing" certutil "k8s.io/client-go/util/cert" - "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" ) -func TestFileRenew(t *testing.T) { - caCertCfg := &certutil.Config{CommonName: "kubernetes"} - caCert, caKey, err := pkiutil.NewCertificateAuthority(caCertCfg) - if err != nil { - t.Fatalf("couldn't create CA: %v", err) - } - - fr := NewFileRenewal(caCert, caKey) +func TestFileRenewer(t *testing.T) { + // creates a File renewer using a test Certificate authority + fr := NewFileRenewer(testCACert, testCAKey) + // renews a certificate certCfg := &certutil.Config{ CommonName: "test-certs", AltNames: certutil.AltNames{ @@ -46,8 +41,9 @@ func TestFileRenew(t *testing.T) { t.Fatalf("unexpected error renewing cert: %v", err) } + // verify the renewed certificate pool := x509.NewCertPool() - pool.AddCert(caCert) + pool.AddCert(testCACert) _, err = cert.Verify(x509.VerifyOptions{ DNSName: "test-domain.space", diff --git a/cmd/kubeadm/app/phases/certs/renewal/interface.go b/cmd/kubeadm/app/phases/certs/renewal/interface.go deleted file mode 100644 index 3da747d40c5..00000000000 --- a/cmd/kubeadm/app/phases/certs/renewal/interface.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2018 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 renewal - -import ( - "crypto" - "crypto/x509" - - certutil "k8s.io/client-go/util/cert" -) - -// Interface represents a standard way to renew a certificate. -type Interface interface { - Renew(*certutil.Config) (*x509.Certificate, crypto.Signer, error) -} diff --git a/cmd/kubeadm/app/phases/certs/renewal/manager.go b/cmd/kubeadm/app/phases/certs/renewal/manager.go new file mode 100644 index 00000000000..88a0bec54d7 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/manager.go @@ -0,0 +1,288 @@ +/* +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 renewal + +import ( + "crypto/x509" + "sort" + + "github.com/pkg/errors" + clientset "k8s.io/client-go/kubernetes" + certutil "k8s.io/client-go/util/cert" + 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" + "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" +) + +// Manager can be used to coordinate certificate renewal and related processes, +// like CSR generation or checking certificate expiration +type Manager struct { + // cfg holds the kubeadm ClusterConfiguration + cfg *kubeadmapi.ClusterConfiguration + + // kubernetesDir holds the directory where kubeConfig files are stored + kubernetesDir string + + // certificates contains the certificateRenewHandler controlled by this manager + certificates map[string]*CertificateRenewHandler +} + +// CertificateRenewHandler defines required info for renewing a certificate +type CertificateRenewHandler struct { + // Name of the certificate to be used for UX. + // This value can be used to trigger operations on this certificate + Name string + + // LongName of the certificate to be used for UX + LongName string + + // FileName defines the name (or the BaseName) of the certificate file + FileName string + + // CABaseName define the base name for the CA that should be used for certificate renewal + CABaseName string + + // readwriter define a CertificateReadWriter to be used for certificate renewal + readwriter certificateReadWriter +} + +// NewManager return a new certificate renewal manager ready for handling certificates in the cluster +func NewManager(cfg *kubeadmapi.ClusterConfiguration, kubernetesDir string) (*Manager, error) { + rm := &Manager{ + cfg: cfg, + kubernetesDir: kubernetesDir, + certificates: map[string]*CertificateRenewHandler{}, + } + + // gets the list of certificates that are expected according to the current cluster configuration + certListFunc := certsphase.GetDefaultCertList + if cfg.Etcd.External != nil { + certListFunc = certsphase.GetCertsWithoutEtcd + } + certTree, err := certListFunc().AsMap().CertTree() + if err != nil { + return nil, err + } + + // create a CertificateRenewHandler for each signed certificate in the certificate tree; + // NB. we are not offering support for renewing CAs; this would cause serious consequences + for ca, certs := range certTree { + for _, cert := range certs { + // create a ReadWriter for certificates stored in the K8s local PKI + pkiReadWriter := newPKICertificateReadWriter(rm.cfg.CertificatesDir, cert.BaseName) + + // adds the certificateRenewHandler. + // PKI certificates are indexed by name, that is a well know constant defined + // in the certsphase package and that can be reused across all the kubeadm codebase + rm.certificates[cert.Name] = &CertificateRenewHandler{ + Name: cert.Name, + LongName: cert.LongName, + FileName: cert.BaseName, + CABaseName: ca.BaseName, //Nb. this is a path for etcd certs (they are stored in a subfolder) + readwriter: pkiReadWriter, + } + } + } + + // gets the list of certificates that should be considered for renewal + kubeConfigs := []struct { + longName string + fileName string + }{ + { + longName: "certificate embedded in the kubeconfig file for the admin to use and for kubeadm itself", + fileName: kubeadmconstants.AdminKubeConfigFileName, + }, + { + longName: "certificate embedded in the kubeconfig file for the controller manager to use", + fileName: kubeadmconstants.ControllerManagerKubeConfigFileName, + }, + { + longName: "certificate embedded in the kubeconfig file for the scheduler manager to use", + fileName: kubeadmconstants.SchedulerKubeConfigFileName, + }, + //NB. we are escluding KubeletKubeConfig from renewal because management of this certificate is delegated to kubelet + } + + // create a CertificateRenewHandler for each kubeConfig file + for _, kubeConfig := range kubeConfigs { + // create a ReadWriter for certificates embedded in kubeConfig files + kubeConfigReadWriter := newKubeconfigReadWriter(kubernetesDir, kubeConfig.fileName) + + // adds the certificateRenewHandler. + // Certificates embedded kubeConfig files in are indexed by fileName, that is a well know constant defined + // in the kubeadm constants package and that can be reused across all the kubeadm codebase + rm.certificates[kubeConfig.fileName] = &CertificateRenewHandler{ + Name: kubeConfig.fileName, // we are using fileName as name, because there is nothing similar outside + LongName: kubeConfig.longName, + FileName: kubeConfig.fileName, + CABaseName: kubeadmconstants.CACertAndKeyBaseName, // all certificates in kubeConfig files are signed by the Kubernetes CA + readwriter: kubeConfigReadWriter, + } + } + + return rm, nil +} + +// Certificates return the list of certificates controlled by this Manager +func (rm *Manager) Certificates() []*CertificateRenewHandler { + certificates := []*CertificateRenewHandler{} + for _, h := range rm.certificates { + certificates = append(certificates, h) + } + + sort.Slice(certificates, func(i, j int) bool { return certificates[i].Name < certificates[j].Name }) + + return certificates +} + +// RenewUsingLocalCA executes certificate renewal using local certificate authorities for generating new certs. +// For PKI certificates, use the name defined in the certsphase package, while for certificates +// embedded in the kubeConfig files, use the kubeConfig file name defined in the kubeadm constants package. +// If you use the CertificateRenewHandler returned by Certificates func, handler.Name already contains the right value. +func (rm *Manager) RenewUsingLocalCA(name string) (bool, error) { + handler, ok := rm.certificates[name] + if !ok { + return false, errors.Errorf("%s is not a valid certificate for this cluster", name) + } + + // checks if the we are in the external CA case (CA certificate provided without the certificate key) + var externalCA bool + switch handler.CABaseName { + case kubeadmconstants.CACertAndKeyBaseName: + externalCA, _ = certsphase.UsingExternalCA(rm.cfg) + case kubeadmconstants.FrontProxyCACertAndKeyBaseName: + externalCA, _ = certsphase.UsingExternalFrontProxyCA(rm.cfg) + case kubeadmconstants.EtcdCACertAndKeyBaseName: + externalCA = false + default: + return false, errors.Errorf("unknown certificate authority %s", handler.CABaseName) + } + + // in case of external CA it is not possible to renew certificates, then return early + if externalCA { + return false, nil + } + + // reads the current certificate + cert, err := handler.readwriter.Read() + if err != nil { + return false, err + } + + // extract the certificate config + cfg := certToConfig(cert) + + // reads the CA + caCert, caKey, err := certsphase.LoadCertificateAuthority(rm.cfg.CertificatesDir, handler.CABaseName) + if err != nil { + return false, err + } + + // create a new certificate with the same config + newCert, newKey, err := NewFileRenewer(caCert, caKey).Renew(cfg) + if err != nil { + return false, errors.Wrapf(err, "failed to renew certificate %s", name) + } + + // writes the new certificate to disk + err = handler.readwriter.Write(newCert, newKey) + if err != nil { + return false, err + } + + return true, nil +} + +// RenewUsingCSRAPI executes certificate renewal uses the K8s certificate API. +// For PKI certificates, use the name defined in the certsphase package, while for certificates +// embedded in the kubeConfig files, use the kubeConfig file name defined in the kubeadm constants package. +// If you use the CertificateRenewHandler returned by Certificates func, handler.Name already contains the right value. +func (rm *Manager) RenewUsingCSRAPI(name string, client clientset.Interface) error { + handler, ok := rm.certificates[name] + if !ok { + return errors.Errorf("%s is not a valid certificate for this cluster", name) + } + + // reads the current certificate + cert, err := handler.readwriter.Read() + if err != nil { + return err + } + + // extract the certificate config + cfg := certToConfig(cert) + + // create a new certificate with the same config + newCert, newKey, err := NewAPIRenewer(client).Renew(cfg) + if err != nil { + return errors.Wrapf(err, "failed to renew certificate %s", name) + } + + // writes the new certificate to disk + err = handler.readwriter.Write(newCert, newKey) + if err != nil { + return err + } + + return nil +} + +// CreateRenewCSR generates CSR request for certificate renewal. +// For PKI certificates, use the name defined in the certsphase package, while for certificates +// embedded in the kubeConfig files, use the kubeConfig file name defined in the kubeadm constants package. +// If you use the CertificateRenewHandler returned by Certificates func, handler.Name already contains the right value. +func (rm *Manager) CreateRenewCSR(name, outdir string) error { + handler, ok := rm.certificates[name] + if !ok { + return errors.Errorf("%s is not a known certificate", name) + } + + // reads the current certificate + cert, err := handler.readwriter.Read() + if err != nil { + return err + } + + // extracts the certificate config + cfg := certToConfig(cert) + + // generates the CSR request and save it + csr, key, err := pkiutil.NewCSRAndKey(cfg) + if err := pkiutil.WriteKey(outdir, name, key); err != nil { + return errors.Wrapf(err, "failure while saving %s key", name) + } + + if err := pkiutil.WriteCSR(outdir, name, csr); err != nil { + return errors.Wrapf(err, "failure while saving %s CSR", name) + } + + return nil +} + +func certToConfig(cert *x509.Certificate) *certutil.Config { + return &certutil.Config{ + CommonName: cert.Subject.CommonName, + Organization: cert.Subject.Organization, + AltNames: certutil.AltNames{ + IPs: cert.IPAddresses, + DNSNames: cert.DNSNames, + }, + Usages: cert.ExtKeyUsage, + } +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/manager_test.go b/cmd/kubeadm/app/phases/certs/renewal/manager_test.go new file mode 100644 index 00000000000..ff2cb3572e8 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/manager_test.go @@ -0,0 +1,270 @@ +/* +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 renewal + +import ( + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "net" + "os" + "path/filepath" + "testing" + "time" + + certutil "k8s.io/client-go/util/cert" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + certtestutil "k8s.io/kubernetes/cmd/kubeadm/app/util/certs" + "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" + testutil "k8s.io/kubernetes/cmd/kubeadm/test" +) + +var ( + testCACertCfg = &certutil.Config{CommonName: "kubernetes"} + + testCACert, testCAKey, _ = pkiutil.NewCertificateAuthority(testCACertCfg) + + testCertCfg = &certutil.Config{ + CommonName: "test-common-name", + Organization: []string{"sig-cluster-lifecycle"}, + AltNames: certutil.AltNames{ + IPs: []net.IP{net.ParseIP("10.100.0.1")}, + DNSNames: []string{"test-domain.space"}, + }, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } +) + +func TestNewManager(t *testing.T) { + tests := []struct { + name string + cfg *kubeadmapi.ClusterConfiguration + expectedCertificates int + }{ + { + name: "cluster with local etcd", + cfg: &kubeadmapi.ClusterConfiguration{}, + expectedCertificates: 10, //[admin apiserver apiserver-etcd-client apiserver-kubelet-client controller-manager etcd/healthcheck-client etcd/peer etcd/server front-proxy-client scheduler] + }, + { + name: "cluster with external etcd", + cfg: &kubeadmapi.ClusterConfiguration{ + Etcd: kubeadmapi.Etcd{ + External: &kubeadmapi.ExternalEtcd{}, + }, + }, + expectedCertificates: 6, // [admin apiserver apiserver-kubelet-client controller-manager front-proxy-client scheduler] + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rm, err := NewManager(test.cfg, "") + if err != nil { + t.Fatalf("Failed to create the certificate renewal manager: %v", err) + } + + if len(rm.Certificates()) != test.expectedCertificates { + t.Errorf("Expected %d certificates, saw %d", test.expectedCertificates, len(rm.Certificates())) + } + }) + } +} + +func TestRenewUsingLocalCA(t *testing.T) { + dir := testutil.SetupTempDir(t) + defer os.RemoveAll(dir) + + if err := pkiutil.WriteCertAndKey(dir, "ca", testCACert, testCAKey); err != nil { + t.Fatalf("couldn't write out CA certificate to %s", dir) + } + + cfg := &kubeadmapi.ClusterConfiguration{ + CertificatesDir: dir, + } + rm, err := NewManager(cfg, dir) + if err != nil { + t.Fatalf("Failed to create the certificate renewal manager: %v", err) + } + + tests := []struct { + name string + certName string + createCertFunc func() *x509.Certificate + }{ + { + name: "Certificate renewal for a PKI certificate", + certName: "apiserver", + createCertFunc: func() *x509.Certificate { + return writeTestCertificate(t, dir, "apiserver", testCACert, testCAKey) + }, + }, + { + 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) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cert := test.createCertFunc() + + time.Sleep(1 * time.Second) + + _, err := rm.RenewUsingLocalCA(test.certName) + if err != nil { + t.Fatalf("error renewing certificate: %v", err) + } + + newCert, err := rm.certificates[test.certName].readwriter.Read() + if err != nil { + t.Fatalf("error reading renewed certificate: %v", err) + } + + if newCert.SerialNumber.Cmp(cert.SerialNumber) == 0 { + t.Fatal("expected new certificate, but renewed certificate has same serial number") + } + + if !newCert.NotAfter.After(cert.NotAfter) { + t.Fatalf("expected new certificate with updated expiration, but renewed certificate has same NotAfter value: saw %s, expected greather than %s", newCert.NotAfter, cert.NotAfter) + } + + certtestutil.AssertCertificateIsSignedByCa(t, newCert, testCACert) + certtestutil.AssertCertificateHasClientAuthUsage(t, newCert) + certtestutil.AssertCertificateHasOrganizations(t, newCert, testCertCfg.Organization...) + certtestutil.AssertCertificateHasCommonName(t, newCert, testCertCfg.CommonName) + certtestutil.AssertCertificateHasDNSNames(t, newCert, testCertCfg.AltNames.DNSNames...) + certtestutil.AssertCertificateHasIPAddresses(t, newCert, testCertCfg.AltNames.IPs...) + }) + } +} + +func TestCreateRenewCSR(t *testing.T) { + dir := testutil.SetupTempDir(t) + defer os.RemoveAll(dir) + + outdir := filepath.Join(dir, "out") + + if err := os.MkdirAll(outdir, 0755); err != nil { + t.Fatalf("couldn't create %s", outdir) + } + + if err := pkiutil.WriteCertAndKey(dir, "ca", testCACert, testCAKey); err != nil { + t.Fatalf("couldn't write out CA certificate to %s", dir) + } + + cfg := &kubeadmapi.ClusterConfiguration{ + CertificatesDir: dir, + } + rm, err := NewManager(cfg, dir) + if err != nil { + t.Fatalf("Failed to create the certificate renewal manager: %v", err) + } + + tests := []struct { + name string + certName string + createCertFunc func() *x509.Certificate + }{ + { + 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) + }, + }, + { + 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) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.createCertFunc() + + time.Sleep(1 * time.Second) + + err := rm.CreateRenewCSR(test.certName, outdir) + if err != nil { + t.Fatalf("error renewing certificate: %v", err) + } + + file := fmt.Sprintf("%s.key", test.certName) + if _, err := os.Stat(filepath.Join(outdir, file)); os.IsNotExist(err) { + t.Errorf("Expected file %s does not exist", file) + } + + file = fmt.Sprintf("%s.csr", test.certName) + if _, err := os.Stat(filepath.Join(outdir, file)); os.IsNotExist(err) { + t.Errorf("Expected file %s does not exist", file) + } + }) + } + +} + +func TestCertToConfig(t *testing.T) { + expectedConfig := &certutil.Config{ + CommonName: "test-common-name", + Organization: []string{"sig-cluster-lifecycle"}, + AltNames: certutil.AltNames{ + IPs: []net.IP{net.ParseIP("10.100.0.1")}, + DNSNames: []string{"test-domain.space"}, + }, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + cert := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "test-common-name", + Organization: []string{"sig-cluster-lifecycle"}, + }, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + DNSNames: []string{"test-domain.space"}, + IPAddresses: []net.IP{net.ParseIP("10.100.0.1")}, + } + + cfg := certToConfig(cert) + + if cfg.CommonName != expectedConfig.CommonName { + t.Errorf("expected common name %q, got %q", expectedConfig.CommonName, cfg.CommonName) + } + + if len(cfg.Organization) != 1 || cfg.Organization[0] != expectedConfig.Organization[0] { + t.Errorf("expected organization %v, got %v", expectedConfig.Organization, cfg.Organization) + + } + + if len(cfg.Usages) != 1 || cfg.Usages[0] != expectedConfig.Usages[0] { + t.Errorf("expected ext key usage %v, got %v", expectedConfig.Usages, cfg.Usages) + } + + if len(cfg.AltNames.IPs) != 1 || cfg.AltNames.IPs[0].String() != expectedConfig.AltNames.IPs[0].String() { + t.Errorf("expected SAN IPs %v, got %v", expectedConfig.AltNames.IPs, cfg.AltNames.IPs) + } + + if len(cfg.AltNames.DNSNames) != 1 || cfg.AltNames.DNSNames[0] != expectedConfig.AltNames.DNSNames[0] { + t.Errorf("expected SAN DNSNames %v, got %v", expectedConfig.AltNames.DNSNames, cfg.AltNames.DNSNames) + } +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/readwriter.go b/cmd/kubeadm/app/phases/certs/renewal/readwriter.go new file mode 100644 index 00000000000..c6040793d71 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/readwriter.go @@ -0,0 +1,173 @@ +/* +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 renewal + +import ( + "crypto" + "crypto/x509" + "path/filepath" + + "github.com/pkg/errors" + + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + certutil "k8s.io/client-go/util/cert" + "k8s.io/client-go/util/keyutil" + pkiutil "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" +) + +// certificateReadWriter defines the behavior of a component that +// read or write a certificate stored/embedded in a file +type certificateReadWriter interface { + // Read a certificate stored/embedded in a file + Read() (*x509.Certificate, error) + + // Write (update) a certificate stored/embedded in a file + Write(*x509.Certificate, crypto.Signer) error +} + +// pkiCertificateReadWriter defines a certificateReadWriter for certificate files +// in the K8s pki managed by kubeadm +type pkiCertificateReadWriter struct { + baseName string + certificateDir string +} + +// newPKICertificateReadWriter return a new pkiCertificateReadWriter +func newPKICertificateReadWriter(certificateDir string, baseName string) *pkiCertificateReadWriter { + return &pkiCertificateReadWriter{ + baseName: baseName, + certificateDir: certificateDir, + } +} + +// Read a certificate from a file the K8s pki managed by kubeadm +func (rw *pkiCertificateReadWriter) Read() (*x509.Certificate, error) { + certificatePath, _ := pkiutil.PathsForCertAndKey(rw.certificateDir, rw.baseName) + certs, err := certutil.CertsFromFile(certificatePath) + if err != nil { + return nil, errors.Wrapf(err, "failed to load existing certificate %s", rw.baseName) + } + + if len(certs) != 1 { + return nil, errors.Errorf("wanted exactly one certificate, got %d", len(certs)) + } + + return certs[0], nil +} + +// Write a certificate to files in the K8s pki managed by kubeadm +func (rw *pkiCertificateReadWriter) Write(newCert *x509.Certificate, newKey crypto.Signer) error { + if err := pkiutil.WriteCertAndKey(rw.certificateDir, rw.baseName, newCert, newKey); err != nil { + return errors.Wrapf(err, "failed to write new certificate %s", rw.baseName) + } + return nil +} + +// kubeConfigReadWriter defines a certificateReadWriter for certificate files +// embedded in the kubeConfig files managed by kubeadm, and more specifically +// for the client certificate of the AuthInfo +type kubeConfigReadWriter struct { + kubernetesDir string + kubeConfigFileName string + kubeConfigFilePath string + kubeConfig *clientcmdapi.Config +} + +// newKubeconfigReadWriter return a new kubeConfigReadWriter +func newKubeconfigReadWriter(kubernetesDir string, kubeConfigFileName string) *kubeConfigReadWriter { + return &kubeConfigReadWriter{ + kubernetesDir: kubernetesDir, + kubeConfigFileName: kubeConfigFileName, + kubeConfigFilePath: filepath.Join(kubernetesDir, kubeConfigFileName), + } +} + +// Read a certificate embedded in kubeConfig file managed by kubeadm. +// Please note that the kubeConfig file itself is kept in the ReadWriter state thus allowing +// to preserve the attributes (Context, Servers, AuthInfo etc.) +func (rw *kubeConfigReadWriter) Read() (*x509.Certificate, error) { + // try to load the kubeConfig file + kubeConfig, err := clientcmd.LoadFromFile(rw.kubeConfigFilePath) + if err != nil { + return nil, errors.Wrapf(err, "failed to load kubeConfig file %s", rw.kubeConfigFilePath) + } + + // get current context + if _, ok := kubeConfig.Contexts[kubeConfig.CurrentContext]; !ok { + return nil, errors.Errorf("invalid kubeConfig file %s: missing context %s", rw.kubeConfigFilePath, kubeConfig.CurrentContext) + } + + // get cluster info for current context and ensure a server certificate is embedded in it + clusterName := kubeConfig.Contexts[kubeConfig.CurrentContext].Cluster + if _, ok := kubeConfig.Clusters[clusterName]; !ok { + return nil, errors.Errorf("invalid kubeConfig file %s: missing cluster %s", rw.kubeConfigFilePath, clusterName) + } + + cluster := kubeConfig.Clusters[clusterName] + if len(cluster.CertificateAuthorityData) == 0 { + return nil, errors.Errorf("kubeConfig file %s does not have and embedded server certificate", rw.kubeConfigFilePath) + } + + // get auth info for current context and ensure a client certificate is embedded in it + authInfoName := kubeConfig.Contexts[kubeConfig.CurrentContext].AuthInfo + if _, ok := kubeConfig.AuthInfos[authInfoName]; !ok { + return nil, errors.Errorf("invalid kubeConfig file %s: missing authInfo %s", rw.kubeConfigFilePath, authInfoName) + } + + authInfo := kubeConfig.AuthInfos[authInfoName] + if len(authInfo.ClientCertificateData) == 0 { + return nil, errors.Errorf("kubeConfig file %s does not have and embedded client certificate", rw.kubeConfigFilePath) + } + + // parse the client certificate, retrive the cert config and then renew it + certs, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData) + if err != nil { + return nil, errors.Wrapf(err, "kubeConfig file %s does not contain a valid client certificate", rw.kubeConfigFilePath) + } + + rw.kubeConfig = kubeConfig + + return certs[0], nil +} + +// Write a certificate embedded in kubeConfig file managed by kubeadm +// Please note that all the other attribute of the kubeConfig file are preserved, but this +// requires to call Read before Write +func (rw *kubeConfigReadWriter) Write(newCert *x509.Certificate, newKey crypto.Signer) error { + // check if Read was called before Write + if rw.kubeConfig == nil { + return errors.Errorf("failed to Write kubeConfig file with renewd certs. It is necessary to call Read before Write") + } + + // encodes the new key + encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(newKey) + if err != nil { + return errors.Wrapf(err, "failed to marshal private key to PEM") + } + + // get auth info for current context and ensure a client certificate is embedded in it + authInfoName := rw.kubeConfig.Contexts[rw.kubeConfig.CurrentContext].AuthInfo + + // create a kubeConfig copy with the new client certs + newConfig := rw.kubeConfig.DeepCopy() + newConfig.AuthInfos[authInfoName].ClientKeyData = encodedClientKey + newConfig.AuthInfos[authInfoName].ClientCertificateData = pkiutil.EncodeCertPEM(newCert) + + // writes the kubeConfig to disk + return clientcmd.WriteToFile(*newConfig, rw.kubeConfigFilePath) +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go b/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go new file mode 100644 index 00000000000..cccef9ec3dc --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go @@ -0,0 +1,179 @@ +/* +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 renewal + +import ( + "crypto" + "crypto/x509" + "net" + "os" + "path/filepath" + "testing" + + "k8s.io/client-go/tools/clientcmd" + certutil "k8s.io/client-go/util/cert" + "k8s.io/client-go/util/keyutil" + kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + pkiutil "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" + testutil "k8s.io/kubernetes/cmd/kubeadm/test" +) + +func TestPKICertificateReadWriter(t *testing.T) { + // creates a tmp folder + dir := testutil.SetupTempDir(t) + defer os.RemoveAll(dir) + + // creates a certificate + cert := writeTestCertificate(t, dir, "test", testCACert, testCAKey) + + // Creates a pkiCertificateReadWriter + pkiReadWriter := newPKICertificateReadWriter(dir, "test") + + // Reads the certificate + readCert, err := pkiReadWriter.Read() + if err != nil { + t.Fatalf("couldn't read certificate: %v", err) + } + + // Check if the certificate read from disk is equal to the original one + if !cert.Equal(readCert) { + t.Errorf("read cert does not match with expected cert") + } + + // Create a new cert + newCert, newkey, err := pkiutil.NewCertAndKey(testCACert, testCAKey, testCertCfg) + if err != nil { + t.Fatalf("couldn't generate certificate: %v", err) + } + + // Writes the new certificate + err = pkiReadWriter.Write(newCert, newkey) + if err != nil { + t.Fatalf("couldn't write new certificate: %v", err) + } + + // Reads back the new certificate + readCert, err = pkiReadWriter.Read() + if err != nil { + t.Fatalf("couldn't read new certificate: %v", err) + } + + // Check if the new certificate read from disk is equal to the original one + if !newCert.Equal(readCert) { + t.Error("read cert does not match with expected new cert") + } +} + +func TestKubeconfigReadWriter(t *testing.T) { + // creates a tmp folder + dir := testutil.SetupTempDir(t) + defer os.RemoveAll(dir) + + // creates a certificate and then embeds it into a kubeconfig file + cert := writeTestKubeconfig(t, dir, "test", testCACert, testCAKey) + + // Creates a KubeconfigReadWriter + kubeconfigReadWriter := newKubeconfigReadWriter(dir, "test") + + // Reads the certificate embedded in a kubeconfig + readCert, err := kubeconfigReadWriter.Read() + if err != nil { + t.Fatalf("couldn't read embedded certificate: %v", err) + } + + // Check if the certificate read from disk is equal to the original one + if !cert.Equal(readCert) { + t.Errorf("read cert does not match with expected cert") + } + + // Create a new cert + newCert, newkey, err := pkiutil.NewCertAndKey(testCACert, testCAKey, testCertCfg) + if err != nil { + t.Fatalf("couldn't generate certificate: %v", err) + } + + // Writes the new certificate embedded in a kubeconfig + err = kubeconfigReadWriter.Write(newCert, newkey) + if err != nil { + t.Fatalf("couldn't write new embedded certificate: %v", err) + } + + // Reads back the new certificate embedded in a kubeconfig writer + readCert, err = kubeconfigReadWriter.Read() + if err != nil { + t.Fatalf("couldn't read new embedded certificate: %v", err) + } + + // Check if the new certificate read from disk is equal to the original one + if !newCert.Equal(readCert) { + t.Errorf("read cert does not match with expected new cert") + } +} + +// writeTestCertificate is a utility for creating a test certificate +func writeTestCertificate(t *testing.T, dir, name string, caCert *x509.Certificate, caKey crypto.Signer) *x509.Certificate { + cert, key, err := pkiutil.NewCertAndKey(caCert, caKey, testCertCfg) + if err != nil { + t.Fatalf("couldn't generate certificate: %v", err) + } + + if err := pkiutil.WriteCertAndKey(dir, name, cert, key); err != nil { + t.Fatalf("couldn't write out certificate %s to %s", name, dir) + } + + return cert +} + +// 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 { + + cfg := &certutil.Config{ + CommonName: "test-common-name", + Organization: []string{"sig-cluster-lifecycle"}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + AltNames: certutil.AltNames{ + IPs: []net.IP{net.ParseIP("10.100.0.1")}, + DNSNames: []string{"test-domain.space"}, + }, + } + cert, key, err := pkiutil.NewCertAndKey(caCert, caKey, cfg) + if err != nil { + t.Fatalf("couldn't generate certificate: %v", err) + } + + encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(key) + if err != nil { + t.Fatalf("failed to marshal private key to PEM: %v", err) + } + + certificateAuthorityData := pkiutil.EncodeCertPEM(caCert) + + config := kubeconfigutil.CreateWithCerts( + "https://localhost:1234", + "kubernetes-test", + "user-test", + certificateAuthorityData, + encodedClientKey, + pkiutil.EncodeCertPEM(cert), + ) + + if err := clientcmd.WriteToFile(*config, filepath.Join(dir, name)); err != nil { + t.Fatalf("couldn't write out certificate") + } + + return cert +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal.go b/cmd/kubeadm/app/phases/certs/renewal/renewal.go deleted file mode 100644 index 37dbbfb902e..00000000000 --- a/cmd/kubeadm/app/phases/certs/renewal/renewal.go +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2018 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 renewal - -import ( - "crypto/x509" - "path/filepath" - - "github.com/pkg/errors" - "k8s.io/client-go/tools/clientcmd" - certutil "k8s.io/client-go/util/cert" - "k8s.io/client-go/util/keyutil" - "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" -) - -// RenewExistingCert loads a certificate file, uses the renew interface to renew it, -// and saves the resulting certificate and key over the old one. -func RenewExistingCert(certsDir, baseName string, impl Interface) error { - certificatePath, _ := pkiutil.PathsForCertAndKey(certsDir, baseName) - certs, err := certutil.CertsFromFile(certificatePath) - if err != nil { - return errors.Wrapf(err, "failed to load existing certificate %s", baseName) - } - - if len(certs) != 1 { - return errors.Errorf("wanted exactly one certificate, got %d", len(certs)) - } - - cfg := certToConfig(certs[0]) - newCert, newKey, err := impl.Renew(cfg) - if err != nil { - return errors.Wrapf(err, "failed to renew certificate %s", baseName) - } - - if err := pkiutil.WriteCertAndKey(certsDir, baseName, newCert, newKey); err != nil { - return errors.Wrapf(err, "failed to write new certificate %s", baseName) - } - return nil -} - -// RenewEmbeddedClientCert loads a kubeconfig file, uses the renew interface to renew the client certificate -// embedded in it, and then saves the resulting kubeconfig and key over the old one. -func RenewEmbeddedClientCert(kubeConfigFileDir, kubeConfigFileName string, impl Interface) error { - kubeConfigFilePath := filepath.Join(kubeConfigFileDir, kubeConfigFileName) - - // try to load the kubeconfig file - kubeconfig, err := clientcmd.LoadFromFile(kubeConfigFilePath) - if err != nil { - return errors.Wrapf(err, "failed to load kubeconfig file %s", kubeConfigFilePath) - } - - // get current context - if _, ok := kubeconfig.Contexts[kubeconfig.CurrentContext]; !ok { - return errors.Errorf("invalid kubeconfig file %s: missing context %s", kubeConfigFilePath, kubeconfig.CurrentContext) - } - - // get cluster info for current context and ensure a server certificate is embedded in it - clusterName := kubeconfig.Contexts[kubeconfig.CurrentContext].Cluster - if _, ok := kubeconfig.Clusters[clusterName]; !ok { - return errors.Errorf("invalid kubeconfig file %s: missing cluster %s", kubeConfigFilePath, clusterName) - } - - cluster := kubeconfig.Clusters[clusterName] - if len(cluster.CertificateAuthorityData) == 0 { - return errors.Errorf("kubeconfig file %s does not have and embedded server certificate", kubeConfigFilePath) - } - - // get auth info for current context and ensure a client certificate is embedded in it - authInfoName := kubeconfig.Contexts[kubeconfig.CurrentContext].AuthInfo - if _, ok := kubeconfig.AuthInfos[authInfoName]; !ok { - return errors.Errorf("invalid kubeconfig file %s: missing authInfo %s", kubeConfigFilePath, authInfoName) - } - - authInfo := kubeconfig.AuthInfos[authInfoName] - if len(authInfo.ClientCertificateData) == 0 { - return errors.Errorf("kubeconfig file %s does not have and embedded client certificate", kubeConfigFilePath) - } - - // parse the client certificate, retrive the cert config and then renew it - certs, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData) - if err != nil { - return errors.Wrapf(err, "kubeconfig file %s does not contain a valid client certificate", kubeConfigFilePath) - } - - cfg := certToConfig(certs[0]) - - newCert, newKey, err := impl.Renew(cfg) - if err != nil { - return errors.Wrapf(err, "failed to renew certificate embedded in %s", kubeConfigFilePath) - } - - // encodes the new key - encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(newKey) - if err != nil { - return errors.Wrapf(err, "failed to marshal private key to PEM") - } - - // create a kubeconfig copy with the new client certs - newConfig := kubeconfig.DeepCopy() - newConfig.AuthInfos[authInfoName].ClientKeyData = encodedClientKey - newConfig.AuthInfos[authInfoName].ClientCertificateData = pkiutil.EncodeCertPEM(newCert) - - // writes the kubeconfig to disk - return clientcmd.WriteToFile(*newConfig, kubeConfigFilePath) -} - -func certToConfig(cert *x509.Certificate) *certutil.Config { - return &certutil.Config{ - CommonName: cert.Subject.CommonName, - Organization: cert.Subject.Organization, - AltNames: certutil.AltNames{ - IPs: cert.IPAddresses, - DNSNames: cert.DNSNames, - }, - Usages: cert.ExtKeyUsage, - } -} diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go deleted file mode 100644 index 1418e26254a..00000000000 --- a/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go +++ /dev/null @@ -1,359 +0,0 @@ -/* -Copyright 2018 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 renewal - -import ( - "bytes" - "crypto" - "crypto/x509" - "crypto/x509/pkix" - "net" - "os" - "path/filepath" - "testing" - "time" - - certsapi "k8s.io/api/certificates/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - fakecerts "k8s.io/client-go/kubernetes/typed/certificates/v1beta1/fake" - k8stesting "k8s.io/client-go/testing" - "k8s.io/client-go/tools/clientcmd" - certutil "k8s.io/client-go/util/cert" - "k8s.io/client-go/util/keyutil" - certtestutil "k8s.io/kubernetes/cmd/kubeadm/app/util/certs" - kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" - "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" - testutil "k8s.io/kubernetes/cmd/kubeadm/test" -) - -func TestRenewImplementations(t *testing.T) { - caCertCfg := &certutil.Config{CommonName: "kubernetes"} - caCert, caKey, err := pkiutil.NewCertificateAuthority(caCertCfg) - if err != nil { - t.Fatalf("couldn't create CA: %v", err) - } - - client := &fakecerts.FakeCertificatesV1beta1{ - Fake: &k8stesting.Fake{}, - } - certReq := getCertReq(t, caCert, caKey) - certReqNoCert := certReq.DeepCopy() - certReqNoCert.Status.Certificate = nil - client.AddReactor("get", "certificatesigningrequests", defaultReactionFunc(certReq)) - watcher := watch.NewFakeWithChanSize(3, false) - watcher.Add(certReqNoCert) - watcher.Modify(certReqNoCert) - watcher.Modify(certReq) - client.AddWatchReactor("certificatesigningrequests", k8stesting.DefaultWatchReactor(watcher, nil)) - - // override the timeout so tests are faster - watchTimeout = time.Second - - tests := []struct { - name string - impl Interface - }{ - { - name: "filerenewal", - impl: NewFileRenewal(caCert, caKey), - }, - { - name: "certs api", - impl: &CertsAPIRenewal{ - client: client, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - - certCfg := &certutil.Config{ - CommonName: "test-certs", - AltNames: certutil.AltNames{ - DNSNames: []string{"test-domain.space"}, - }, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - } - - cert, _, err := test.impl.Renew(certCfg) - if err != nil { - t.Fatalf("unexpected error renewing cert: %v", err) - } - - pool := x509.NewCertPool() - pool.AddCert(caCert) - - _, err = cert.Verify(x509.VerifyOptions{ - DNSName: "test-domain.space", - Roots: pool, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - }) - if err != nil { - t.Errorf("couldn't verify new cert: %v", err) - } - }) - } -} - -func defaultReactionFunc(obj runtime.Object) k8stesting.ReactionFunc { - return func(act k8stesting.Action) (bool, runtime.Object, error) { - return true, obj, nil - } -} - -func getCertReq(t *testing.T, caCert *x509.Certificate, caKey crypto.Signer) *certsapi.CertificateSigningRequest { - cert, _, err := pkiutil.NewCertAndKey(caCert, caKey, &certutil.Config{ - CommonName: "testcert", - AltNames: certutil.AltNames{ - DNSNames: []string{"test-domain.space"}, - }, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - }) - if err != nil { - t.Fatalf("couldn't generate cert: %v", err) - } - - return &certsapi.CertificateSigningRequest{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testcert", - }, - Status: certsapi.CertificateSigningRequestStatus{ - Conditions: []certsapi.CertificateSigningRequestCondition{ - { - Type: certsapi.CertificateApproved, - }, - }, - Certificate: pkiutil.EncodeCertPEM(cert), - }, - } -} - -func TestCertToConfig(t *testing.T) { - expectedConfig := &certutil.Config{ - CommonName: "test-common-name", - Organization: []string{"sig-cluster-lifecycle"}, - AltNames: certutil.AltNames{ - IPs: []net.IP{net.ParseIP("10.100.0.1")}, - DNSNames: []string{"test-domain.space"}, - }, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - } - - cert := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "test-common-name", - Organization: []string{"sig-cluster-lifecycle"}, - }, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - DNSNames: []string{"test-domain.space"}, - IPAddresses: []net.IP{net.ParseIP("10.100.0.1")}, - } - - cfg := certToConfig(cert) - - if cfg.CommonName != expectedConfig.CommonName { - t.Errorf("expected common name %q, got %q", expectedConfig.CommonName, cfg.CommonName) - } - - if len(cfg.Organization) != 1 || cfg.Organization[0] != expectedConfig.Organization[0] { - t.Errorf("expected organization %v, got %v", expectedConfig.Organization, cfg.Organization) - - } - - if len(cfg.Usages) != 1 || cfg.Usages[0] != expectedConfig.Usages[0] { - t.Errorf("expected ext key usage %v, got %v", expectedConfig.Usages, cfg.Usages) - } - - if len(cfg.AltNames.IPs) != 1 || cfg.AltNames.IPs[0].String() != expectedConfig.AltNames.IPs[0].String() { - t.Errorf("expected SAN IPs %v, got %v", expectedConfig.AltNames.IPs, cfg.AltNames.IPs) - } - - if len(cfg.AltNames.DNSNames) != 1 || cfg.AltNames.DNSNames[0] != expectedConfig.AltNames.DNSNames[0] { - t.Errorf("expected SAN DNSNames %v, got %v", expectedConfig.AltNames.DNSNames, cfg.AltNames.DNSNames) - } -} - -func TestRenewExistingCert(t *testing.T) { - // creates a CA, a certificate, and save it to a file - cfg := &certutil.Config{ - CommonName: "test-common-name", - Organization: []string{"sig-cluster-lifecycle"}, - AltNames: certutil.AltNames{ - IPs: []net.IP{net.ParseIP("10.100.0.1")}, - DNSNames: []string{"test-domain.space"}, - }, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - } - - caCertCfg := &certutil.Config{CommonName: "kubernetes"} - caCert, caKey, err := pkiutil.NewCertificateAuthority(caCertCfg) - if err != nil { - t.Fatalf("couldn't create CA: %v", err) - } - - cert, key, err := pkiutil.NewCertAndKey(caCert, caKey, cfg) - if err != nil { - t.Fatalf("couldn't generate certificate: %v", err) - } - - dir := testutil.SetupTempDir(t) - defer os.RemoveAll(dir) - - if err := pkiutil.WriteCertAndKey(dir, "server", cert, key); err != nil { - t.Fatalf("couldn't write out certificate") - } - - // makes some time pass - time.Sleep(1 * time.Second) - - // renew the certificate - renewer := NewFileRenewal(caCert, caKey) - - if err := RenewExistingCert(dir, "server", renewer); err != nil { - t.Fatalf("couldn't renew certificate: %v", err) - } - - // reads the renewed certificate - newCert, err := pkiutil.TryLoadCertFromDisk(dir, "server") - if err != nil { - t.Fatalf("couldn't load created certificate: %v", err) - } - - // check the new certificate is changed, has an newer expiration date, but preserve all the - // other attributes - - if newCert.SerialNumber.Cmp(cert.SerialNumber) == 0 { - t.Fatal("expected new certificate, but renewed certificate has same serial number") - } - - if !newCert.NotAfter.After(cert.NotAfter) { - t.Fatalf("expected new certificate with updated expiration, but renewed certificate has the same serial number: saw %s, expected greather than %s", newCert.NotAfter, cert.NotAfter) - } - - certtestutil.AssertCertificateIsSignedByCa(t, newCert, caCert) - certtestutil.AssertCertificateHasClientAuthUsage(t, newCert) - certtestutil.AssertCertificateHasOrganizations(t, newCert, cfg.Organization...) - certtestutil.AssertCertificateHasCommonName(t, newCert, cfg.CommonName) - certtestutil.AssertCertificateHasDNSNames(t, newCert, cfg.AltNames.DNSNames...) - certtestutil.AssertCertificateHasIPAddresses(t, newCert, cfg.AltNames.IPs...) -} - -func TestRenewEmbeddedClientCert(t *testing.T) { - // creates a CA, a client certificate, and then embeds it into a kubeconfig file - caCertCfg := &certutil.Config{CommonName: "kubernetes"} - caCert, caKey, err := pkiutil.NewCertificateAuthority(caCertCfg) - if err != nil { - t.Fatalf("couldn't create CA: %v", err) - } - - cfg := &certutil.Config{ - CommonName: "test-common-name", - Organization: []string{"sig-cluster-lifecycle"}, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - AltNames: certutil.AltNames{ - IPs: []net.IP{net.ParseIP("10.100.0.1")}, - DNSNames: []string{"test-domain.space"}, - }, - } - cert, key, err := pkiutil.NewCertAndKey(caCert, caKey, cfg) - if err != nil { - t.Fatalf("couldn't generate certificate: %v", err) - } - - encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(key) - if err != nil { - t.Fatalf("failed to marshal private key to PEM: %v", err) - } - - certificateAuthorityData := pkiutil.EncodeCertPEM(caCert) - - config := kubeconfigutil.CreateWithCerts( - "https://localhost:1234", - "kubernetes-test", - "user-test", - certificateAuthorityData, - encodedClientKey, - pkiutil.EncodeCertPEM(cert), - ) - - dir := testutil.SetupTempDir(t) - defer os.RemoveAll(dir) - - kubeconfigPath := filepath.Join(dir, "k.conf") - - if err := clientcmd.WriteToFile(*config, kubeconfigPath); err != nil { - t.Fatalf("couldn't write out certificate") - } - - // makes some time pass - time.Sleep(1 * time.Second) - - // renew the embedded certificate - renewer := NewFileRenewal(caCert, caKey) - - if err := RenewEmbeddedClientCert(dir, "k.conf", renewer); err != nil { - t.Fatalf("couldn't renew embedded certificate: %v", err) - } - - // reads the kubeconfig file and gets the renewed certificate - newConfig, err := clientcmd.LoadFromFile(kubeconfigPath) - if err != nil { - t.Fatalf("failed to load kubeconfig file %s: %v", kubeconfigPath, err) - } - - if newConfig.Contexts[config.CurrentContext].Cluster != "kubernetes-test" { - t.Fatalf("invalid cluster. expected kubernetes-test, saw %s", newConfig.Contexts[config.CurrentContext].Cluster) - } - - cluster := newConfig.Clusters["kubernetes-test"] - if !bytes.Equal(cluster.CertificateAuthorityData, certificateAuthorityData) { - t.Fatalf("invalid cluster. CertificateAuthorityData does not contain expected value") - } - - if newConfig.Contexts[config.CurrentContext].AuthInfo != "user-test" { - t.Fatalf("invalid AuthInfo. expected user-test, saw %s", newConfig.Contexts[config.CurrentContext].AuthInfo) - } - - authInfo := newConfig.AuthInfos["user-test"] - - newCerts, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData) - if err != nil { - t.Fatalf("couldn't load created certificate: %v", err) - } - - // check the new certificate is changed, has an newer expiration date, but preserve all the - // other attributes - - newCert := newCerts[0] - if newCert.SerialNumber.Cmp(cert.SerialNumber) == 0 { - t.Fatal("expected new certificate, but renewed certificate has same serial number") - } - - if !newCert.NotAfter.After(cert.NotAfter) { - t.Fatalf("expected new certificate with updated expiration, but renewed certificate has same serial number: saw %s, expected greather than %s", newCert.NotAfter, cert.NotAfter) - } - - certtestutil.AssertCertificateIsSignedByCa(t, newCert, caCert) - certtestutil.AssertCertificateHasClientAuthUsage(t, newCert) - certtestutil.AssertCertificateHasOrganizations(t, newCert, cfg.Organization...) - certtestutil.AssertCertificateHasCommonName(t, newCert, cfg.CommonName) - certtestutil.AssertCertificateHasDNSNames(t, newCert, cfg.AltNames.DNSNames...) - certtestutil.AssertCertificateHasIPAddresses(t, newCert, cfg.AltNames.IPs...) -} diff --git a/cmd/kubeadm/app/phases/upgrade/staticpods.go b/cmd/kubeadm/app/phases/upgrade/staticpods.go index 754ee78ca95..a50fb091019 100644 --- a/cmd/kubeadm/app/phases/upgrade/staticpods.go +++ b/cmd/kubeadm/app/phases/upgrade/staticpods.go @@ -178,7 +178,7 @@ func (spm *KubeStaticPodPathManager) CleanupDirs() error { return utilerrors.NewAggregate(errlist) } -func upgradeComponent(component string, renewCerts bool, waiter apiclient.Waiter, pathMgr StaticPodPathManager, cfg *kubeadmapi.InitConfiguration, beforePodHash string, recoverManifests map[string]string) error { +func upgradeComponent(component string, certsRenewMgr *renewal.Manager, waiter apiclient.Waiter, pathMgr StaticPodPathManager, cfg *kubeadmapi.InitConfiguration, beforePodHash string, recoverManifests map[string]string) error { // Special treatment is required for etcd case, when rollbackOldManifests should roll back etcd // manifests only for the case when component is Etcd recoverEtcd := false @@ -211,9 +211,9 @@ func upgradeComponent(component string, renewCerts bool, waiter apiclient.Waiter } // if certificate renewal should be performed - if renewCerts { + if certsRenewMgr != nil { // renew all the certificates used by the current component - if err := renewCertsByComponent(cfg, pathMgr.KubernetesDir(), component); err != nil { + if err := renewCertsByComponent(cfg, component, certsRenewMgr); err != nil { return rollbackOldManifests(recoverManifests, errors.Wrapf(err, "failed to renew certificates for component %q", component), pathMgr, recoverEtcd) } } @@ -256,7 +256,7 @@ func upgradeComponent(component string, renewCerts bool, waiter apiclient.Waiter } // performEtcdStaticPodUpgrade performs upgrade of etcd, it returns bool which indicates fatal error or not and the actual error. -func performEtcdStaticPodUpgrade(renewCerts bool, client clientset.Interface, waiter apiclient.Waiter, pathMgr StaticPodPathManager, cfg *kubeadmapi.InitConfiguration, recoverManifests map[string]string, oldEtcdClient, newEtcdClient etcdutil.ClusterInterrogator) (bool, error) { +func performEtcdStaticPodUpgrade(certsRenewMgr *renewal.Manager, client clientset.Interface, waiter apiclient.Waiter, pathMgr StaticPodPathManager, cfg *kubeadmapi.InitConfiguration, recoverManifests map[string]string, oldEtcdClient, newEtcdClient etcdutil.ClusterInterrogator) (bool, error) { // Add etcd static pod spec only if external etcd is not configured if cfg.Etcd.External != nil { return false, errors.New("external etcd detected, won't try to change any etcd state") @@ -320,7 +320,7 @@ func performEtcdStaticPodUpgrade(renewCerts bool, client clientset.Interface, wa retryInterval := 15 * time.Second // Perform etcd upgrade using common to all control plane components function - if err := upgradeComponent(constants.Etcd, renewCerts, waiter, pathMgr, cfg, beforeEtcdPodHash, recoverManifests); err != nil { + if err := upgradeComponent(constants.Etcd, certsRenewMgr, waiter, pathMgr, cfg, beforeEtcdPodHash, recoverManifests); err != nil { fmt.Printf("[upgrade/etcd] Failed to upgrade etcd: %v\n", err) // Since upgrade component failed, the old etcd manifest has either been restored or was never touched // Now we need to check the health of etcd cluster if it is up with old manifest @@ -433,13 +433,21 @@ func StaticPodControlPlane(client clientset.Interface, waiter apiclient.Waiter, } } + var certsRenewMgr *renewal.Manager + if renewCerts { + certsRenewMgr, err = renewal.NewManager(&cfg.ClusterConfiguration, pathMgr.KubernetesDir()) + if err != nil { + return errors.Wrap(err, "failed to create the certificate renewal manager") + } + } + // etcd upgrade is done prior to other control plane components if !isExternalEtcd && etcdUpgrade { // set the TLS upgrade flag for all components fmt.Printf("[upgrade/etcd] Upgrading to TLS for %s\n", constants.Etcd) // Perform etcd upgrade using common to all control plane components function - fatal, err := performEtcdStaticPodUpgrade(renewCerts, client, waiter, pathMgr, cfg, recoverManifests, oldEtcdClient, newEtcdClient) + fatal, err := performEtcdStaticPodUpgrade(certsRenewMgr, client, waiter, pathMgr, cfg, recoverManifests, oldEtcdClient, newEtcdClient) if err != nil { if fatal { return err @@ -456,17 +464,22 @@ func StaticPodControlPlane(client clientset.Interface, waiter apiclient.Waiter, } for _, component := range constants.ControlPlaneComponents { - if err = upgradeComponent(component, renewCerts, waiter, pathMgr, cfg, beforePodHashMap[component], recoverManifests); err != nil { + if err = upgradeComponent(component, certsRenewMgr, waiter, pathMgr, cfg, beforePodHashMap[component], recoverManifests); err != nil { return err } } if renewCerts { // renew the certificate embedded in the admin.conf file - err := renewEmbeddedCertsByName(cfg, pathMgr.KubernetesDir(), constants.AdminKubeConfigFileName) + renewed, err := certsRenewMgr.RenewUsingLocalCA(constants.AdminKubeConfigFileName) if err != nil { return rollbackOldManifests(recoverManifests, errors.Wrapf(err, "failed to upgrade the %s certificates", constants.AdminKubeConfigFileName), pathMgr, false) } + + if !renewed { + // if not error, but not renewed because of external CA detected, inform the user + fmt.Printf("[upgrade/staticpods] External CA detected, %s certificate can't be renewed\n", constants.AdminKubeConfigFileName) + } } // Remove the temporary directories used on a best-effort (don't fail if the calls error out) @@ -514,121 +527,57 @@ func rollbackEtcdData(cfg *kubeadmapi.InitConfiguration, pathMgr StaticPodPathMa // renewCertsByComponent takes charge of renewing certificates used by a specific component before // the static pod of the component is upgraded -func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, kubernetesDir, component string) error { - // if the cluster is using a local etcd - if cfg.Etcd.Local != nil { - if component == constants.Etcd || component == constants.KubeAPIServer { - // try to load the etcd CA - caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertEtcdCA.BaseName) - if err != nil { - return errors.Wrapf(err, "failed to upgrade the %s CA certificate and key", constants.Etcd) - } - // create a renewer for certificates signed by etcd CA - renewer := renewal.NewFileRenewal(caCert, caKey) - // then, if upgrading the etcd component, renew all the certificates signed by etcd CA and used - // by etcd itself (the etcd-server, the etcd-peer and the etcd-healthcheck-client certificate) - if component == constants.Etcd { - for _, cert := range []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertEtcdServer, - &certsphase.KubeadmCertEtcdPeer, - &certsphase.KubeadmCertEtcdHealthcheck, - } { - fmt.Printf("[upgrade/staticpods] Renewing %q certificate\n", cert.BaseName) - if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil { - return errors.Wrapf(err, "failed to renew %s certificates", cert.Name) - } - } - } - // if upgrading the apiserver component, renew the certificate signed by etcd CA and used - // by the apiserver (the apiserver-etcd-client certificate) - if component == constants.KubeAPIServer { - cert := certsphase.KubeadmCertEtcdAPIClient - fmt.Printf("[upgrade/staticpods] Renewing %q certificate\n", cert.BaseName) - if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil { - return errors.Wrapf(err, "failed to renew %s certificate and key", cert.Name) - } +func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, component string, certsRenewMgr *renewal.Manager) error { + var certificates []string + + // if etcd, only in case of local etcd, renew server, peer and health check certificate + if component == constants.Etcd { + if cfg.Etcd.Local != nil { + certificates = []string{ + certsphase.KubeadmCertEtcdServer.Name, + certsphase.KubeadmCertEtcdPeer.Name, + certsphase.KubeadmCertEtcdHealthcheck.Name, } } } + + // if apiserver, renew apiserver serving certificate, kubelet and front-proxy client certificate. + //if local etcd, renew also the etcd client certificate if component == constants.KubeAPIServer { - // Checks if an external CA is provided by the user (when the CA Cert is present but the CA Key is not) - // if not, then CA is managed by kubeadm, so it is possible to renew all the certificates signed by ca - // and used the apis server (the apiserver certificate and the apiserver-kubelet-client certificate) - externalCA, _ := certsphase.UsingExternalCA(&cfg.ClusterConfiguration) - if !externalCA { - // try to load ca - caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertRootCA.BaseName) - if err != nil { - return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.KubeAPIServer) - } - // create a renewer for certificates signed by CA - renewer := renewal.NewFileRenewal(caCert, caKey) - // renew the certificates - for _, cert := range []*certsphase.KubeadmCert{ - &certsphase.KubeadmCertAPIServer, - &certsphase.KubeadmCertKubeletClient, - } { - fmt.Printf("[upgrade/staticpods] Renewing %q certificate\n", cert.BaseName) - if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil { - return errors.Wrapf(err, "failed to renew %s certificate and key", cert.Name) - } - } + certificates = []string{ + certsphase.KubeadmCertAPIServer.Name, + certsphase.KubeadmCertKubeletClient.Name, + certsphase.KubeadmCertFrontProxyClient.Name, } - - // Checks if an external Front-Proxy CA is provided by the user (when the Front-Proxy CA Cert is present but the Front-Proxy CA Key is not) - // if not, then Front-Proxy CA is managed by kubeadm, so it is possible to renew all the certificates signed by ca - // and used the apis server (the front-proxy-client certificate) - externalFrontProxyCA, _ := certsphase.UsingExternalFrontProxyCA(&cfg.ClusterConfiguration) - if !externalFrontProxyCA { - // try to load front-proxy-ca - caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertFrontProxyCA.BaseName) - if err != nil { - return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.KubeAPIServer) - } - // create a renewer for certificates signed by Front-Proxy CA - renewer := renewal.NewFileRenewal(caCert, caKey) - // renew the certificates - cert := certsphase.KubeadmCertFrontProxyClient - fmt.Printf("[upgrade/staticpods] Renewing %q certificate\n", cert.BaseName) - if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil { - return errors.Wrapf(err, "failed to renew %s certificate and key", cert.Name) - } + if cfg.Etcd.Local != nil { + certificates = append(certificates, certsphase.KubeadmCertEtcdAPIClient.Name) } } + + // if controller-manager, renew the certificate embedded in the controller-manager kubeConfig file if component == constants.KubeControllerManager { - // renew the certificate embedded in the controller-manager.conf file - err := renewEmbeddedCertsByName(cfg, kubernetesDir, constants.ControllerManagerKubeConfigFileName) - if err != nil { - return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.ControllerManagerKubeConfigFileName) + certificates = []string{ + constants.ControllerManagerKubeConfigFileName, } } + + // if scheduler, renew the certificate embedded in the scheduler kubeConfig file if component == constants.KubeScheduler { - // renew the certificate embedded in the scheduler.conf file - err := renewEmbeddedCertsByName(cfg, kubernetesDir, constants.SchedulerKubeConfigFileName) - if err != nil { - return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.SchedulerKubeConfigFileName) + certificates = []string{ + constants.SchedulerKubeConfigFileName, } } - return nil -} -func renewEmbeddedCertsByName(cfg *kubeadmapi.InitConfiguration, kubernetesDir, kubeConfigFile string) error { - // Checks if an external CA is provided by the user (when the CA Cert is present but the CA Key is not) - // if not, then CA is managed by kubeadm, so it is possible to renew all the certificates signed by ca - // and used by the apis server (the apiserver certificate and the apiserver-kubelet-client certificate) - externalCA, _ := certsphase.UsingExternalCA(&cfg.ClusterConfiguration) - if !externalCA { - // try to load ca - caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertRootCA.BaseName) + // renew the selected components + for _, cert := range certificates { + fmt.Printf("[upgrade/staticpods] Renewing %s certificate\n", cert) + renewed, err := certsRenewMgr.RenewUsingLocalCA(cert) if err != nil { - return errors.Wrapf(err, "failed to upgrade the %s certificates", kubeConfigFile) + return err } - // create a renewer for certificates signed by CA - renewer := renewal.NewFileRenewal(caCert, caKey) - // renew the certificate embedded in the controller-manager.conf file - fmt.Printf("[upgrade/staticpods] Renewing certificate embedded in %q \n", kubeConfigFile) - if err := renewal.RenewEmbeddedClientCert(kubernetesDir, kubeConfigFile, renewer); err != nil { - return errors.Wrapf(err, "failed to renew certificate embedded in %s", kubeConfigFile) + if !renewed { + // if not error, but not renewed because of external CA detected, inform the user + fmt.Printf("[upgrade/staticpods] External CA detected, %s certificate can't be renewed\n", cert) } } diff --git a/cmd/kubeadm/app/phases/upgrade/staticpods_test.go b/cmd/kubeadm/app/phases/upgrade/staticpods_test.go index 33406c0450c..6c106f9d89d 100644 --- a/cmd/kubeadm/app/phases/upgrade/staticpods_test.go +++ b/cmd/kubeadm/app/phases/upgrade/staticpods_test.go @@ -38,6 +38,7 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmconstants "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" kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" @@ -823,7 +824,12 @@ func TestRenewCertsByComponent(t *testing.T) { } // Renew everything - err := renewCertsByComponent(cfg, tmpDir, test.component) + rm, err := renewal.NewManager(&cfg.ClusterConfiguration, tmpDir) + if err != nil { + t.Fatalf("Failed to create the certificate renewal manager: %v", err) + } + + err = renewCertsByComponent(cfg, test.component, rm) if test.shouldErrorOnRenew { if err == nil { t.Fatal("expected renewal error, got nothing")