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.
This commit is contained in:
Lubomir I. Ivanov 2020-02-12 05:03:20 +02:00
parent f9250c4f95
commit 0ba5891519
4 changed files with 47 additions and 10 deletions

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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()