mirror of https://github.com/k3s-io/k3s
refactor certs renewal
parent
b03367bd88
commit
6db533dd5b
|
@ -19,14 +19,16 @@ package alpha
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||||
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
|
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
|
||||||
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
|
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
|
||||||
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
|
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
|
||||||
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
|
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
|
||||||
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
||||||
kubeadmconstants "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"
|
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal"
|
||||||
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
||||||
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
|
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
|
||||||
|
@ -36,14 +38,16 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
genericCertRenewLongDesc = normalizer.LongDesc(`
|
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.
|
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.
|
||||||
genericCertRenewEmbeddedLongDesc = normalizer.LongDesc(`
|
|
||||||
Renew the certificate embedded in the kubeconfig file %s.
|
|
||||||
|
|
||||||
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(`
|
allLongDesc = normalizer.LongDesc(`
|
||||||
|
@ -78,17 +82,17 @@ func newCmdCertsRenewal() *cobra.Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type renewConfig struct {
|
type renewFlags struct {
|
||||||
cfgPath string
|
cfgPath string
|
||||||
kubeconfigPath string
|
kubeconfigPath string
|
||||||
cfg kubeadmapiv1beta2.InitConfiguration
|
cfg kubeadmapiv1beta2.InitConfiguration
|
||||||
useAPI bool
|
useAPI bool
|
||||||
useCSR bool
|
csrOnly bool
|
||||||
csrPath string
|
csrPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRenewSubCommands(kdir string) []*cobra.Command {
|
func getRenewSubCommands(kdir string) []*cobra.Command {
|
||||||
cfg := &renewConfig{
|
flags := &renewFlags{
|
||||||
cfg: kubeadmapiv1beta2.InitConfiguration{
|
cfg: kubeadmapiv1beta2.InitConfiguration{
|
||||||
ClusterConfiguration: kubeadmapiv1beta2.ClusterConfiguration{
|
ClusterConfiguration: kubeadmapiv1beta2.ClusterConfiguration{
|
||||||
// Setting kubernetes version to a default value in order to allow a not necessary internet lookup
|
// 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
|
// 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)
|
kubeadmutil.CheckErr(err)
|
||||||
|
|
||||||
cmdList := []*cobra.Command{}
|
cmdList := []*cobra.Command{}
|
||||||
funcList := []func(){}
|
funcList := []func(){}
|
||||||
|
|
||||||
for caCert, certs := range certTree {
|
for _, handler := range rm.Certificates() {
|
||||||
// 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 {
|
|
||||||
// get the cobra.Command skeleton for this command
|
// 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
|
// get the implementation of renewing this certificate
|
||||||
renewalFunc := func(kdir, k string) func() {
|
renewalFunc := func(handler *renewal.CertificateRenewHandler) func() {
|
||||||
return func() { renewEmbeddedCert(kdir, k, cfg) }
|
return func() { renewCert(flags, kdir, handler) }
|
||||||
}(kdir, k)
|
}(handler)
|
||||||
// install the implementation into the command
|
// install the implementation into the command
|
||||||
cmd.Run = func(*cobra.Command, []string) { renewalFunc() }
|
cmd.Run = func(*cobra.Command, []string) { renewalFunc() }
|
||||||
cmdList = append(cmdList, cmd)
|
cmdList = append(cmdList, cmd)
|
||||||
|
@ -153,134 +140,60 @@ func getRenewSubCommands(kdir string) []*cobra.Command {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
addFlags(allCmd, cfg)
|
addFlags(allCmd, flags)
|
||||||
|
|
||||||
cmdList = append(cmdList, allCmd)
|
cmdList = append(cmdList, allCmd)
|
||||||
return cmdList
|
return cmdList
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFlags(cmd *cobra.Command, cfg *renewConfig) {
|
func addFlags(cmd *cobra.Command, flags *renewFlags) {
|
||||||
options.AddConfigFlag(cmd.Flags(), &cfg.cfgPath)
|
options.AddConfigFlag(cmd.Flags(), &flags.cfgPath)
|
||||||
options.AddCertificateDirFlag(cmd.Flags(), &cfg.cfg.CertificatesDir)
|
options.AddCertificateDirFlag(cmd.Flags(), &flags.cfg.CertificatesDir)
|
||||||
options.AddKubeConfigFlag(cmd.Flags(), &cfg.kubeconfigPath)
|
options.AddKubeConfigFlag(cmd.Flags(), &flags.kubeconfigPath)
|
||||||
options.AddCSRFlag(cmd.Flags(), &cfg.useCSR)
|
options.AddCSRFlag(cmd.Flags(), &flags.csrOnly)
|
||||||
options.AddCSRDirFlag(cmd.Flags(), &cfg.csrPath)
|
options.AddCSRDirFlag(cmd.Flags(), &flags.csrPath)
|
||||||
cmd.Flags().BoolVar(&cfg.useAPI, "use-api", cfg.useAPI, "Use the Kubernetes certificate API to renew certificates")
|
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) {
|
func renewCert(flags *renewFlags, kdir string, handler *renewal.CertificateRenewHandler) {
|
||||||
internalcfg, err := configutil.LoadOrDefaultInitConfiguration(cfg.cfgPath, &cfg.cfg)
|
internalcfg, err := configutil.LoadOrDefaultInitConfiguration(flags.cfgPath, &flags.cfg)
|
||||||
kubeadmutil.CheckErr(err)
|
kubeadmutil.CheckErr(err)
|
||||||
|
|
||||||
// if the renewal operation is set to generate only CSR request
|
// Get a renewal manager for the given cluster configuration
|
||||||
if cfg.useCSR {
|
rm, err := renewal.NewManager(&internalcfg.ClusterConfiguration, kdir)
|
||||||
// trigger CSR generation in the csrPath, or if this one is missing, in the CertificateDir
|
kubeadmutil.CheckErr(err)
|
||||||
path := cfg.csrPath
|
|
||||||
if path == "" {
|
// if the renewal operation is set to generate CSR request only
|
||||||
path = cfg.cfg.CertificatesDir
|
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)
|
kubeadmutil.CheckErr(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, the renewal operation has to actually renew a certificate
|
// otherwise, the renewal operation has to actually renew a certificate
|
||||||
|
|
||||||
var externalCA bool
|
// renew the certificate using the requested renew method
|
||||||
switch caCert.BaseName {
|
if flags.useAPI {
|
||||||
case kubeadmconstants.CACertAndKeyBaseName:
|
// renew using K8s certificate API
|
||||||
// Check if an external CA is provided by the user (when the CA Cert is present but the CA Key is not)
|
kubeConfigPath := cmdutil.GetKubeConfigPath(flags.kubeconfigPath)
|
||||||
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)
|
|
||||||
client, err := kubeconfigutil.ClientSetFromFile(kubeConfigPath)
|
client, err := kubeconfigutil.ClientSetFromFile(kubeConfigPath)
|
||||||
if err != nil {
|
kubeadmutil.CheckErr(err)
|
||||||
return nil, 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
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("%s renewed\n", handler.LongName)
|
||||||
caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.cfg.CertificatesDir, caCertBaseName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return renewal.NewFileRenewal(caCert, caKey), nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,10 @@ func TestCommandsGenerated(t *testing.T) {
|
||||||
"renew etcd-server",
|
"renew etcd-server",
|
||||||
"renew etcd-peer",
|
"renew etcd-peer",
|
||||||
"renew etcd-healthcheck-client",
|
"renew etcd-healthcheck-client",
|
||||||
|
|
||||||
|
"renew admin.conf",
|
||||||
|
"renew scheduler.conf",
|
||||||
|
"renew controller-manager.conf",
|
||||||
}
|
}
|
||||||
|
|
||||||
renewCmd := newCmdCertsRenewal()
|
renewCmd := newCmdCertsRenewal()
|
||||||
|
@ -79,19 +83,63 @@ func TestCommandsGenerated(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunRenewCommands(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 {
|
tests := []struct {
|
||||||
command string
|
command string
|
||||||
CAs []*certsphase.KubeadmCert
|
|
||||||
Certs []*certsphase.KubeadmCert
|
Certs []*certsphase.KubeadmCert
|
||||||
KubeconfigFiles []string
|
KubeconfigFiles []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
command: "all",
|
command: "all",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertRootCA,
|
|
||||||
&certsphase.KubeadmCertFrontProxyCA,
|
|
||||||
&certsphase.KubeadmCertEtcdCA,
|
|
||||||
},
|
|
||||||
Certs: []*certsphase.KubeadmCert{
|
Certs: []*certsphase.KubeadmCert{
|
||||||
&certsphase.KubeadmCertAPIServer,
|
&certsphase.KubeadmCertAPIServer,
|
||||||
&certsphase.KubeadmCertKubeletClient,
|
&certsphase.KubeadmCertKubeletClient,
|
||||||
|
@ -109,90 +157,60 @@ func TestRunRenewCommands(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "apiserver",
|
command: "apiserver",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertRootCA,
|
|
||||||
},
|
|
||||||
Certs: []*certsphase.KubeadmCert{
|
Certs: []*certsphase.KubeadmCert{
|
||||||
&certsphase.KubeadmCertAPIServer,
|
&certsphase.KubeadmCertAPIServer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "apiserver-kubelet-client",
|
command: "apiserver-kubelet-client",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertRootCA,
|
|
||||||
},
|
|
||||||
Certs: []*certsphase.KubeadmCert{
|
Certs: []*certsphase.KubeadmCert{
|
||||||
&certsphase.KubeadmCertKubeletClient,
|
&certsphase.KubeadmCertKubeletClient,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "apiserver-etcd-client",
|
command: "apiserver-etcd-client",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertEtcdCA,
|
|
||||||
},
|
|
||||||
Certs: []*certsphase.KubeadmCert{
|
Certs: []*certsphase.KubeadmCert{
|
||||||
&certsphase.KubeadmCertEtcdAPIClient,
|
&certsphase.KubeadmCertEtcdAPIClient,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "front-proxy-client",
|
command: "front-proxy-client",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertFrontProxyCA,
|
|
||||||
},
|
|
||||||
Certs: []*certsphase.KubeadmCert{
|
Certs: []*certsphase.KubeadmCert{
|
||||||
&certsphase.KubeadmCertFrontProxyClient,
|
&certsphase.KubeadmCertFrontProxyClient,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "etcd-server",
|
command: "etcd-server",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertEtcdCA,
|
|
||||||
},
|
|
||||||
Certs: []*certsphase.KubeadmCert{
|
Certs: []*certsphase.KubeadmCert{
|
||||||
&certsphase.KubeadmCertEtcdServer,
|
&certsphase.KubeadmCertEtcdServer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "etcd-peer",
|
command: "etcd-peer",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertEtcdCA,
|
|
||||||
},
|
|
||||||
Certs: []*certsphase.KubeadmCert{
|
Certs: []*certsphase.KubeadmCert{
|
||||||
&certsphase.KubeadmCertEtcdPeer,
|
&certsphase.KubeadmCertEtcdPeer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "etcd-healthcheck-client",
|
command: "etcd-healthcheck-client",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertEtcdCA,
|
|
||||||
},
|
|
||||||
Certs: []*certsphase.KubeadmCert{
|
Certs: []*certsphase.KubeadmCert{
|
||||||
&certsphase.KubeadmCertEtcdHealthcheck,
|
&certsphase.KubeadmCertEtcdHealthcheck,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "admin.conf",
|
command: "admin.conf",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertRootCA,
|
|
||||||
},
|
|
||||||
KubeconfigFiles: []string{
|
KubeconfigFiles: []string{
|
||||||
kubeadmconstants.AdminKubeConfigFileName,
|
kubeadmconstants.AdminKubeConfigFileName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "scheduler.conf",
|
command: "scheduler.conf",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertRootCA,
|
|
||||||
},
|
|
||||||
KubeconfigFiles: []string{
|
KubeconfigFiles: []string{
|
||||||
kubeadmconstants.SchedulerKubeConfigFileName,
|
kubeadmconstants.SchedulerKubeConfigFileName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "controller-manager.conf",
|
command: "controller-manager.conf",
|
||||||
CAs: []*certsphase.KubeadmCert{
|
|
||||||
&certsphase.KubeadmCertRootCA,
|
|
||||||
},
|
|
||||||
KubeconfigFiles: []string{
|
KubeconfigFiles: []string{
|
||||||
kubeadmconstants.ControllerManagerKubeConfigFileName,
|
kubeadmconstants.ControllerManagerKubeConfigFileName,
|
||||||
},
|
},
|
||||||
|
@ -201,74 +219,43 @@ func TestRunRenewCommands(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.command, func(t *testing.T) {
|
t.Run(test.command, func(t *testing.T) {
|
||||||
tmpDir := testutil.SetupTempDir(t)
|
// Get file ModTime before renew
|
||||||
defer os.RemoveAll(tmpDir)
|
ModTime := map[string]time.Time{}
|
||||||
|
|
||||||
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{}
|
|
||||||
for _, cert := range test.Certs {
|
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)))
|
file, err := os.Stat(filepath.Join(tmpDir, fmt.Sprintf("%s.crt", cert.BaseName)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("couldn't get certificate %s: %v", cert.Name, err)
|
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 {
|
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))
|
file, err := os.Stat(filepath.Join(tmpDir, kubeConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("couldn't get kubeconfig %s: %v", kubeConfig, err)
|
t.Fatalf("couldn't get kubeconfig %s: %v", kubeConfig, err)
|
||||||
}
|
}
|
||||||
createTime[kubeConfig] = file.ModTime()
|
ModTime[kubeConfig] = file.ModTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// exec renew
|
// exec renew
|
||||||
renewCmds := getRenewSubCommands(tmpDir)
|
renewCmds := getRenewSubCommands(tmpDir)
|
||||||
cmdtestutil.RunSubCommand(t, renewCmds, test.command, fmt.Sprintf("--cert-dir=%s", 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 {
|
for _, cert := range test.Certs {
|
||||||
file, err := os.Stat(filepath.Join(tmpDir, fmt.Sprintf("%s.crt", cert.BaseName)))
|
file, err := os.Stat(filepath.Join(tmpDir, fmt.Sprintf("%s.crt", cert.BaseName)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("couldn't get certificate %s: %v", cert.Name, err)
|
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)
|
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 {
|
for _, kubeConfig := range test.KubeconfigFiles {
|
||||||
file, err := os.Stat(filepath.Join(tmpDir, kubeConfig))
|
file, err := os.Stat(filepath.Join(tmpDir, kubeConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("couldn't get kubeconfig %s: %v", kubeConfig, err)
|
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)
|
t.Errorf("kubeconfig %s was not renewed as expected", kubeConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,10 +268,22 @@ func TestRenewUsingCSR(t *testing.T) {
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
cert := &certs.KubeadmCertEtcdServer
|
cert := &certs.KubeadmCertEtcdServer
|
||||||
|
|
||||||
renewCmds := getRenewSubCommands(tmpDir)
|
cfg := testutil.GetDefaultInternalConfig(t)
|
||||||
cmdtestutil.RunSubCommand(t, renewCmds, cert.Name, "--csr-only", "--csr-dir="+tmpDir)
|
cfg.CertificatesDir = tmpDir
|
||||||
|
|
||||||
if _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(tmpDir, cert.BaseName); err != nil {
|
caCert, caKey, err := certsphase.KubeadmCertEtcdCA.CreateAsCA(cfg)
|
||||||
t.Fatalf("couldn't load certificate %q: %v", cert.BaseName, err)
|
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 is the definition of the cert used by the API server to access the kubelet.
|
||||||
KubeadmCertKubeletClient = KubeadmCert{
|
KubeadmCertKubeletClient = KubeadmCert{
|
||||||
Name: "apiserver-kubelet-client",
|
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,
|
BaseName: kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName,
|
||||||
CAName: "ca",
|
CAName: "ca",
|
||||||
config: certutil.Config{
|
config: certutil.Config{
|
||||||
|
@ -284,7 +284,7 @@ var (
|
||||||
KubeadmCertFrontProxyClient = KubeadmCert{
|
KubeadmCertFrontProxyClient = KubeadmCert{
|
||||||
Name: "front-proxy-client",
|
Name: "front-proxy-client",
|
||||||
BaseName: kubeadmconstants.FrontProxyClientCertAndKeyBaseName,
|
BaseName: kubeadmconstants.FrontProxyClientCertAndKeyBaseName,
|
||||||
LongName: "client for the front proxy",
|
LongName: "certificate for the front proxy client",
|
||||||
CAName: "front-proxy-ca",
|
CAName: "front-proxy-ca",
|
||||||
config: certutil.Config{
|
config: certutil.Config{
|
||||||
CommonName: kubeadmconstants.FrontProxyClientCertCommonName,
|
CommonName: kubeadmconstants.FrontProxyClientCertCommonName,
|
||||||
|
@ -322,7 +322,7 @@ var (
|
||||||
// KubeadmCertEtcdPeer is the definition of the cert used by etcd peers to access each other.
|
// KubeadmCertEtcdPeer is the definition of the cert used by etcd peers to access each other.
|
||||||
KubeadmCertEtcdPeer = KubeadmCert{
|
KubeadmCertEtcdPeer = KubeadmCert{
|
||||||
Name: "etcd-peer",
|
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,
|
BaseName: kubeadmconstants.EtcdPeerCertAndKeyBaseName,
|
||||||
CAName: "etcd-ca",
|
CAName: "etcd-ca",
|
||||||
config: certutil.Config{
|
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 is the definition of the cert used by Kubernetes to check the health of the etcd server.
|
||||||
KubeadmCertEtcdHealthcheck = KubeadmCert{
|
KubeadmCertEtcdHealthcheck = KubeadmCert{
|
||||||
Name: "etcd-healthcheck-client",
|
Name: "etcd-healthcheck-client",
|
||||||
LongName: "client certificate for liveness probes to healtcheck etcd",
|
LongName: "certificate for liveness probes to healtcheck etcd",
|
||||||
BaseName: kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName,
|
BaseName: kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName,
|
||||||
CAName: "etcd-ca",
|
CAName: "etcd-ca",
|
||||||
config: certutil.Config{
|
config: certutil.Config{
|
||||||
|
@ -348,7 +348,7 @@ var (
|
||||||
// KubeadmCertEtcdAPIClient is the definition of the cert used by the API server to access etcd.
|
// KubeadmCertEtcdAPIClient is the definition of the cert used by the API server to access etcd.
|
||||||
KubeadmCertEtcdAPIClient = KubeadmCert{
|
KubeadmCertEtcdAPIClient = KubeadmCert{
|
||||||
Name: "apiserver-etcd-client",
|
Name: "apiserver-etcd-client",
|
||||||
LongName: "client apiserver uses to access etcd",
|
LongName: "certificate the apiserver uses to access etcd",
|
||||||
BaseName: kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName,
|
BaseName: kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName,
|
||||||
CAName: "etcd-ca",
|
CAName: "etcd-ca",
|
||||||
config: certutil.Config{
|
config: certutil.Config{
|
||||||
|
|
|
@ -27,7 +27,7 @@ import (
|
||||||
|
|
||||||
certsapi "k8s.io/api/certificates/v1beta1"
|
certsapi "k8s.io/api/certificates/v1beta1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
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"
|
certstype "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
|
||||||
certutil "k8s.io/client-go/util/cert"
|
certutil "k8s.io/client-go/util/cert"
|
||||||
csrutil "k8s.io/client-go/util/certificate/csr"
|
csrutil "k8s.io/client-go/util/certificate/csr"
|
||||||
|
@ -38,20 +38,20 @@ const certAPIPrefixName = "kubeadm-cert"
|
||||||
|
|
||||||
var watchTimeout = 5 * time.Minute
|
var watchTimeout = 5 * time.Minute
|
||||||
|
|
||||||
// CertsAPIRenewal creates new certificates using the certs API
|
// APIRenewer define a certificate renewer implementation that uses the K8s certificate API
|
||||||
type CertsAPIRenewal struct {
|
type APIRenewer struct {
|
||||||
client certstype.CertificatesV1beta1Interface
|
client certstype.CertificatesV1beta1Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCertsAPIRenawal takes a Kubernetes interface and returns a renewal Interface.
|
// NewAPIRenewer a new certificate renewer implementation that uses the K8s certificate API
|
||||||
func NewCertsAPIRenawal(client kubernetes.Interface) Interface {
|
func NewAPIRenewer(client clientset.Interface) *APIRenewer {
|
||||||
return &CertsAPIRenewal{
|
return &APIRenewer{
|
||||||
client: client.CertificatesV1beta1(),
|
client: client.CertificatesV1beta1(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renew takes a certificate using the cert and key.
|
// Renew a certificate using the K8s certificate API
|
||||||
func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) {
|
func (r *APIRenewer) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) {
|
||||||
reqTmp := &x509.CertificateRequest{
|
reqTmp := &x509.CertificateRequest{
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
CommonName: cfg.CommonName,
|
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"
|
"k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileRenewal renews a certificate using local certs
|
// FileRenewer define a certificate renewer implementation that uses given CA cert and key for generating new certficiates
|
||||||
type FileRenewal struct {
|
type FileRenewer struct {
|
||||||
caCert *x509.Certificate
|
caCert *x509.Certificate
|
||||||
caKey crypto.Signer
|
caKey crypto.Signer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileRenewal takes a certificate pair to construct the Interface.
|
// NewFileRenewer returns a new certificate renewer that uses given CA cert and key for generating new certficiates
|
||||||
func NewFileRenewal(caCert *x509.Certificate, caKey crypto.Signer) Interface {
|
func NewFileRenewer(caCert *x509.Certificate, caKey crypto.Signer) *FileRenewer {
|
||||||
return &FileRenewal{
|
return &FileRenewer{
|
||||||
caCert: caCert,
|
caCert: caCert,
|
||||||
caKey: caKey,
|
caKey: caKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renew takes a certificate using the cert and key
|
// Renew a certificate using a given CA cert and key
|
||||||
func (r *FileRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) {
|
func (r *FileRenewer) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) {
|
||||||
return pkiutil.NewCertAndKey(r.caCert, r.caKey, cfg)
|
return pkiutil.NewCertAndKey(r.caCert, r.caKey, cfg)
|
||||||
}
|
}
|
|
@ -21,18 +21,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
certutil "k8s.io/client-go/util/cert"
|
certutil "k8s.io/client-go/util/cert"
|
||||||
"k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFileRenew(t *testing.T) {
|
func TestFileRenewer(t *testing.T) {
|
||||||
caCertCfg := &certutil.Config{CommonName: "kubernetes"}
|
// creates a File renewer using a test Certificate authority
|
||||||
caCert, caKey, err := pkiutil.NewCertificateAuthority(caCertCfg)
|
fr := NewFileRenewer(testCACert, testCAKey)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("couldn't create CA: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fr := NewFileRenewal(caCert, caKey)
|
|
||||||
|
|
||||||
|
// renews a certificate
|
||||||
certCfg := &certutil.Config{
|
certCfg := &certutil.Config{
|
||||||
CommonName: "test-certs",
|
CommonName: "test-certs",
|
||||||
AltNames: certutil.AltNames{
|
AltNames: certutil.AltNames{
|
||||||
|
@ -46,8 +41,9 @@ func TestFileRenew(t *testing.T) {
|
||||||
t.Fatalf("unexpected error renewing cert: %v", err)
|
t.Fatalf("unexpected error renewing cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verify the renewed certificate
|
||||||
pool := x509.NewCertPool()
|
pool := x509.NewCertPool()
|
||||||
pool.AddCert(caCert)
|
pool.AddCert(testCACert)
|
||||||
|
|
||||||
_, err = cert.Verify(x509.VerifyOptions{
|
_, err = cert.Verify(x509.VerifyOptions{
|
||||||
DNSName: "test-domain.space",
|
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...)
|
|
||||||
}
|
|
|
@ -178,7 +178,7 @@ func (spm *KubeStaticPodPathManager) CleanupDirs() error {
|
||||||
return utilerrors.NewAggregate(errlist)
|
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
|
// Special treatment is required for etcd case, when rollbackOldManifests should roll back etcd
|
||||||
// manifests only for the case when component is Etcd
|
// manifests only for the case when component is Etcd
|
||||||
recoverEtcd := false
|
recoverEtcd := false
|
||||||
|
@ -211,9 +211,9 @@ func upgradeComponent(component string, renewCerts bool, waiter apiclient.Waiter
|
||||||
}
|
}
|
||||||
|
|
||||||
// if certificate renewal should be performed
|
// if certificate renewal should be performed
|
||||||
if renewCerts {
|
if certsRenewMgr != nil {
|
||||||
// renew all the certificates used by the current component
|
// 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)
|
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.
|
// 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
|
// Add etcd static pod spec only if external etcd is not configured
|
||||||
if cfg.Etcd.External != nil {
|
if cfg.Etcd.External != nil {
|
||||||
return false, errors.New("external etcd detected, won't try to change any etcd state")
|
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
|
retryInterval := 15 * time.Second
|
||||||
|
|
||||||
// Perform etcd upgrade using common to all control plane components function
|
// 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)
|
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
|
// 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
|
// 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
|
// etcd upgrade is done prior to other control plane components
|
||||||
if !isExternalEtcd && etcdUpgrade {
|
if !isExternalEtcd && etcdUpgrade {
|
||||||
// set the TLS upgrade flag for all components
|
// set the TLS upgrade flag for all components
|
||||||
fmt.Printf("[upgrade/etcd] Upgrading to TLS for %s\n", constants.Etcd)
|
fmt.Printf("[upgrade/etcd] Upgrading to TLS for %s\n", constants.Etcd)
|
||||||
|
|
||||||
// Perform etcd upgrade using common to all control plane components function
|
// 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 err != nil {
|
||||||
if fatal {
|
if fatal {
|
||||||
return err
|
return err
|
||||||
|
@ -456,17 +464,22 @@ func StaticPodControlPlane(client clientset.Interface, waiter apiclient.Waiter,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, component := range constants.ControlPlaneComponents {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if renewCerts {
|
if renewCerts {
|
||||||
// renew the certificate embedded in the admin.conf file
|
// 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 {
|
if err != nil {
|
||||||
return rollbackOldManifests(recoverManifests, errors.Wrapf(err, "failed to upgrade the %s certificates", constants.AdminKubeConfigFileName), pathMgr, false)
|
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)
|
// 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
|
// renewCertsByComponent takes charge of renewing certificates used by a specific component before
|
||||||
// the static pod of the component is upgraded
|
// the static pod of the component is upgraded
|
||||||
func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, kubernetesDir, component string) error {
|
func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, component string, certsRenewMgr *renewal.Manager) error {
|
||||||
// if the cluster is using a local etcd
|
var certificates []string
|
||||||
if cfg.Etcd.Local != nil {
|
|
||||||
if component == constants.Etcd || component == constants.KubeAPIServer {
|
// if etcd, only in case of local etcd, renew server, peer and health check certificate
|
||||||
// try to load the etcd CA
|
if component == constants.Etcd {
|
||||||
caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertEtcdCA.BaseName)
|
if cfg.Etcd.Local != nil {
|
||||||
if err != nil {
|
certificates = []string{
|
||||||
return errors.Wrapf(err, "failed to upgrade the %s CA certificate and key", constants.Etcd)
|
certsphase.KubeadmCertEtcdServer.Name,
|
||||||
}
|
certsphase.KubeadmCertEtcdPeer.Name,
|
||||||
// create a renewer for certificates signed by etcd CA
|
certsphase.KubeadmCertEtcdHealthcheck.Name,
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
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)
|
certificates = []string{
|
||||||
// if not, then CA is managed by kubeadm, so it is possible to renew all the certificates signed by ca
|
certsphase.KubeadmCertAPIServer.Name,
|
||||||
// and used the apis server (the apiserver certificate and the apiserver-kubelet-client certificate)
|
certsphase.KubeadmCertKubeletClient.Name,
|
||||||
externalCA, _ := certsphase.UsingExternalCA(&cfg.ClusterConfiguration)
|
certsphase.KubeadmCertFrontProxyClient.Name,
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if cfg.Etcd.Local != nil {
|
||||||
// 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)
|
certificates = append(certificates, certsphase.KubeadmCertEtcdAPIClient.Name)
|
||||||
// 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 controller-manager, renew the certificate embedded in the controller-manager kubeConfig file
|
||||||
if component == constants.KubeControllerManager {
|
if component == constants.KubeControllerManager {
|
||||||
// renew the certificate embedded in the controller-manager.conf file
|
certificates = []string{
|
||||||
err := renewEmbeddedCertsByName(cfg, kubernetesDir, constants.ControllerManagerKubeConfigFileName)
|
constants.ControllerManagerKubeConfigFileName,
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.ControllerManagerKubeConfigFileName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if scheduler, renew the certificate embedded in the scheduler kubeConfig file
|
||||||
if component == constants.KubeScheduler {
|
if component == constants.KubeScheduler {
|
||||||
// renew the certificate embedded in the scheduler.conf file
|
certificates = []string{
|
||||||
err := renewEmbeddedCertsByName(cfg, kubernetesDir, constants.SchedulerKubeConfigFileName)
|
constants.SchedulerKubeConfigFileName,
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.SchedulerKubeConfigFileName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func renewEmbeddedCertsByName(cfg *kubeadmapi.InitConfiguration, kubernetesDir, kubeConfigFile string) error {
|
// renew the selected components
|
||||||
// Checks if an external CA is provided by the user (when the CA Cert is present but the CA Key is not)
|
for _, cert := range certificates {
|
||||||
// if not, then CA is managed by kubeadm, so it is possible to renew all the certificates signed by ca
|
fmt.Printf("[upgrade/staticpods] Renewing %s certificate\n", cert)
|
||||||
// and used by the apis server (the apiserver certificate and the apiserver-kubelet-client certificate)
|
renewed, err := certsRenewMgr.RenewUsingLocalCA(cert)
|
||||||
externalCA, _ := certsphase.UsingExternalCA(&cfg.ClusterConfiguration)
|
|
||||||
if !externalCA {
|
|
||||||
// try to load ca
|
|
||||||
caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertRootCA.BaseName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to upgrade the %s certificates", kubeConfigFile)
|
return err
|
||||||
}
|
}
|
||||||
// create a renewer for certificates signed by CA
|
if !renewed {
|
||||||
renewer := renewal.NewFileRenewal(caCert, caKey)
|
// if not error, but not renewed because of external CA detected, inform the user
|
||||||
// renew the certificate embedded in the controller-manager.conf file
|
fmt.Printf("[upgrade/staticpods] External CA detected, %s certificate can't be renewed\n", cert)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ import (
|
||||||
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
||||||
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
||||||
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
|
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"
|
controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane"
|
||||||
etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd"
|
etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd"
|
||||||
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
|
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
|
||||||
|
@ -823,7 +824,12 @@ func TestRenewCertsByComponent(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renew everything
|
// 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 test.shouldErrorOnRenew {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected renewal error, got nothing")
|
t.Fatal("expected renewal error, got nothing")
|
||||||
|
|
Loading…
Reference in New Issue