From 0ba5891519419bbab22dd2db69d32475733a070c Mon Sep 17 00:00:00 2001 From: "Lubomir I. Ivanov" Date: Wed, 12 Feb 2020 05:03:20 +0200 Subject: [PATCH] kubeadm: update embedded CA in kubeconfig files on renewal While kubeadm does not support CA rotation, the users might still attempt to perform this manually. For kubeconfig files, updating to a new CA is not reflected and users need to embed new CA PEM manually. On kubeconfig cert renewal, always keep the embedded CA in sync with the one on disk. Includes a couple of typo fixes. --- cmd/kubeadm/app/phases/certs/renewal/BUILD | 1 + .../app/phases/certs/renewal/manager.go | 3 +- .../app/phases/certs/renewal/readwriter.go | 30 ++++++++++++++++--- .../phases/certs/renewal/readwriter_test.go | 23 ++++++++++---- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/cmd/kubeadm/app/phases/certs/renewal/BUILD b/cmd/kubeadm/app/phases/certs/renewal/BUILD index fe0405c3a16..a9f9e37d908 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/BUILD +++ b/cmd/kubeadm/app/phases/certs/renewal/BUILD @@ -41,6 +41,7 @@ go_test( embed = [":go_default_library"], deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", + "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/util/certs:go_default_library", "//cmd/kubeadm/app/util/kubeconfig:go_default_library", "//cmd/kubeadm/app/util/pkiutil:go_default_library", diff --git a/cmd/kubeadm/app/phases/certs/renewal/manager.go b/cmd/kubeadm/app/phases/certs/renewal/manager.go index 87406360eb9..09807e071af 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/manager.go +++ b/cmd/kubeadm/app/phases/certs/renewal/manager.go @@ -154,7 +154,8 @@ func NewManager(cfg *kubeadmapi.ClusterConfiguration, kubernetesDir string) (*Ma // 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) + kubeConfigReadWriter := newKubeconfigReadWriter(kubernetesDir, kubeConfig.fileName, + rm.cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName) // adds the certificateRenewHandler. // Certificates embedded kubeConfig files in are indexed by fileName, that is a well know constant defined diff --git a/cmd/kubeadm/app/phases/certs/renewal/readwriter.go b/cmd/kubeadm/app/phases/certs/renewal/readwriter.go index cc9b7402776..865c432cc59 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/readwriter.go +++ b/cmd/kubeadm/app/phases/certs/renewal/readwriter.go @@ -28,6 +28,7 @@ import ( clientcmdapi "k8s.io/client-go/tools/clientcmd/api" certutil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" + certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" pkiutil "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" ) @@ -104,14 +105,19 @@ type kubeConfigReadWriter struct { kubeConfigFileName string kubeConfigFilePath string kubeConfig *clientcmdapi.Config + baseName string + certificateDir string + caCert *x509.Certificate } // newKubeconfigReadWriter return a new kubeConfigReadWriter -func newKubeconfigReadWriter(kubernetesDir string, kubeConfigFileName string) *kubeConfigReadWriter { +func newKubeconfigReadWriter(kubernetesDir string, kubeConfigFileName string, certificateDir, baseName string) *kubeConfigReadWriter { return &kubeConfigReadWriter{ kubernetesDir: kubernetesDir, kubeConfigFileName: kubeConfigFileName, kubeConfigFilePath: filepath.Join(kubernetesDir, kubeConfigFileName), + certificateDir: certificateDir, + baseName: baseName, } } @@ -130,6 +136,16 @@ func (rw *kubeConfigReadWriter) Read() (*x509.Certificate, error) { return nil, errors.Wrapf(err, "failed to load kubeConfig file %s", rw.kubeConfigFilePath) } + // The CA cert is required for updating kubeconfig files. + // For local CA renewal, the local CA on disk could have changed, thus a reload is needed. + // For CSR renewal we assume the same CA on disk is mounted for usage with KCM's + // '--cluster-signing-cert-file' flag. + caCert, _, err := certsphase.LoadCertificateAuthority(rw.certificateDir, rw.baseName) + if err != nil { + return nil, err + } + rw.caCert = caCert + // 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) @@ -143,7 +159,7 @@ func (rw *kubeConfigReadWriter) Read() (*x509.Certificate, error) { 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) + return nil, errors.Errorf("kubeConfig file %s does not have an embedded server certificate", rw.kubeConfigFilePath) } // get auth info for current context and ensure a client certificate is embedded in it @@ -154,7 +170,7 @@ func (rw *kubeConfigReadWriter) Read() (*x509.Certificate, error) { 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) + return nil, errors.Errorf("kubeConfig file %s does not have an embedded client certificate", rw.kubeConfigFilePath) } // parse the client certificate, retrive the cert config and then renew it @@ -174,7 +190,7 @@ func (rw *kubeConfigReadWriter) Read() (*x509.Certificate, error) { 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") + return errors.Errorf("failed to Write kubeConfig file with renewed certs. It is necessary to call Read before Write") } // encodes the new key @@ -183,6 +199,12 @@ func (rw *kubeConfigReadWriter) Write(newCert *x509.Certificate, newKey crypto.S return errors.Wrapf(err, "failed to marshal private key to PEM") } + // Update the embedded CA in the kubeconfig file. + // This assumes that the user has kept the current context to the desired one. + clusterName := rw.kubeConfig.Contexts[rw.kubeConfig.CurrentContext].Cluster + cluster := rw.kubeConfig.Clusters[clusterName] + cluster.CertificateAuthorityData = pkiutil.EncodeCertPEM(rw.caCert) + // get auth info for current context and ensure a client certificate is embedded in it authInfoName := rw.kubeConfig.Contexts[rw.kubeConfig.CurrentContext].AuthInfo diff --git a/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go b/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go index cccef9ec3dc..94bc07fc05e 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go +++ b/cmd/kubeadm/app/phases/certs/renewal/readwriter_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/client-go/tools/clientcmd" certutil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" 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" @@ -79,15 +80,27 @@ func TestPKICertificateReadWriter(t *testing.T) { } func TestKubeconfigReadWriter(t *testing.T) { - // creates a tmp folder - dir := testutil.SetupTempDir(t) - defer os.RemoveAll(dir) + // creates tmp folders + dirKubernetes := testutil.SetupTempDir(t) + defer os.RemoveAll(dirKubernetes) + dirPKI := testutil.SetupTempDir(t) + defer os.RemoveAll(dirPKI) + + // write the CA cert and key to the temporary PKI dir + caName := kubeadmconstants.CACertAndKeyBaseName + if err := pkiutil.WriteCertAndKey( + dirPKI, + caName, + testCACert, + testCAKey); err != nil { + t.Fatalf("couldn't write out certificate %s to %s", caName, dirPKI) + } // creates a certificate and then embeds it into a kubeconfig file - cert := writeTestKubeconfig(t, dir, "test", testCACert, testCAKey) + cert := writeTestKubeconfig(t, dirKubernetes, "test", testCACert, testCAKey) // Creates a KubeconfigReadWriter - kubeconfigReadWriter := newKubeconfigReadWriter(dir, "test") + kubeconfigReadWriter := newKubeconfigReadWriter(dirKubernetes, "test", dirPKI, caName) // Reads the certificate embedded in a kubeconfig readCert, err := kubeconfigReadWriter.Read()