mirror of https://github.com/k3s-io/k3s
Merge pull request #77780 from fabriziopandini/refactor-renewal-package
Kubeadm: Refactor renewal packagek3s-v1.15.3
commit
e1770e698e
|
@ -12,6 +12,7 @@ go_library(
|
|||
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/cmd/alpha",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
|
||||
"//cmd/kubeadm/app/apis/kubeadm/scheme:go_default_library",
|
||||
"//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library",
|
||||
"//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library",
|
||||
|
@ -20,7 +21,6 @@ go_library(
|
|||
"//cmd/kubeadm/app/cmd/util:go_default_library",
|
||||
"//cmd/kubeadm/app/constants:go_default_library",
|
||||
"//cmd/kubeadm/app/features:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/certs:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/certs/renewal:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/kubelet:go_default_library",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -3,20 +3,24 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
|||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"certsapi.go",
|
||||
"filerenewal.go",
|
||||
"interface.go",
|
||||
"renewal.go",
|
||||
"apirenewer.go",
|
||||
"filerenewer.go",
|
||||
"manager.go",
|
||||
"readwriter.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
|
||||
"//cmd/kubeadm/app/constants:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/certs:go_default_library",
|
||||
"//cmd/kubeadm/app/util/pkiutil:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/certificate/csr:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/keyutil:go_default_library",
|
||||
|
@ -27,11 +31,14 @@ go_library(
|
|||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"filerenewal_test.go",
|
||||
"renewal_test.go",
|
||||
"apirenewer_test.go",
|
||||
"filerenewer_test.go",
|
||||
"manager_test.go",
|
||||
"readwriter_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//cmd/kubeadm/app/apis/kubeadm: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",
|
||||
|
|
|
@ -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,
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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",
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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...)
|
||||
}
|
|
@ -76,6 +76,7 @@ go_test(
|
|||
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
|
||||
"//cmd/kubeadm/app/constants:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/certs:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/certs/renewal:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/controlplane:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/etcd:go_default_library",
|
||||
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue