Merge pull request #77780 from fabriziopandini/refactor-renewal-package

Kubeadm: Refactor renewal package
k3s-v1.15.3
Kubernetes Prow Robot 2019-05-16 02:55:46 -07:00 committed by GitHub
commit e1770e698e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1275 additions and 894 deletions

View File

@ -12,6 +12,7 @@ go_library(
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/cmd/alpha",
visibility = ["//visibility:public"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/scheme:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library",
@ -20,7 +21,6 @@ go_library(
"//cmd/kubeadm/app/cmd/util:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/features:go_default_library",
"//cmd/kubeadm/app/phases/certs:go_default_library",
"//cmd/kubeadm/app/phases/certs/renewal:go_default_library",
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
"//cmd/kubeadm/app/phases/kubelet:go_default_library",

View File

@ -19,14 +19,16 @@ package alpha
import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
@ -36,14 +38,16 @@ import (
var (
genericCertRenewLongDesc = normalizer.LongDesc(`
Renew the %[1]s, and save them into %[2]s.cert and %[2]s.key files.
Renew the %s.
Extra attributes such as SANs will be based on the existing certificates, there is no need to resupply them.
`)
genericCertRenewEmbeddedLongDesc = normalizer.LongDesc(`
Renew the certificate embedded in the kubeconfig file %s.
Renewals run unconditionally, regardless of certificate expiration date; extra attributes such as SANs will
be based on the existing file/certificates, there is no need to resupply them.
Kubeconfig attributes and certificate extra attributes such as SANs will be based on the existing kubeconfig/certificates, there is no need to resupply them.
Renewal by default tries to use the certificate authority in the local PKI managed by kubeadm; as alternative
it is possible to use K8s certificate API for certificate renewal, or as a last option, to generate a CSR request.
After renewal, in order to make changes effective, is is required to restart control-plane components and
eventually re-distribute the renewed certificate in case the file is used elsewhere.
`)
allLongDesc = normalizer.LongDesc(`
@ -78,17 +82,17 @@ func newCmdCertsRenewal() *cobra.Command {
return cmd
}
type renewConfig struct {
type renewFlags struct {
cfgPath string
kubeconfigPath string
cfg kubeadmapiv1beta2.InitConfiguration
useAPI bool
useCSR bool
csrOnly bool
csrPath string
}
func getRenewSubCommands(kdir string) []*cobra.Command {
cfg := &renewConfig{
flags := &renewFlags{
cfg: kubeadmapiv1beta2.InitConfiguration{
ClusterConfiguration: kubeadmapiv1beta2.ClusterConfiguration{
// Setting kubernetes version to a default value in order to allow a not necessary internet lookup
@ -97,45 +101,28 @@ func getRenewSubCommands(kdir string) []*cobra.Command {
},
}
// Default values for the cobra help text
kubeadmscheme.Scheme.Default(&cfg.cfg)
kubeadmscheme.Scheme.Default(&flags.cfg)
certTree, err := certsphase.GetDefaultCertList().AsMap().CertTree()
// Get a renewal manager for a generic Cluster configuration, that is used only for getting
// the list of certificates for building subcommands
rm, err := renewal.NewManager(&kubeadmapi.ClusterConfiguration{}, "")
kubeadmutil.CheckErr(err)
cmdList := []*cobra.Command{}
funcList := []func(){}
for caCert, certs := range certTree {
// Don't offer to renew CAs; would cause serious consequences
for _, cert := range certs {
// get the cobra.Command skeleton for this command
cmd := generateCertRenewalCommand(cert, cfg)
// get the implementation of renewing this certificate
renewalFunc := func(cert *certsphase.KubeadmCert, caCert *certsphase.KubeadmCert) func() {
return func() { renewCert(cert, caCert, cfg) }
}(cert, caCert)
// install the implementation into the command
cmd.Run = func(*cobra.Command, []string) { renewalFunc() }
cmdList = append(cmdList, cmd)
// Collect renewal functions for `renew all`
funcList = append(funcList, renewalFunc)
}
}
kubeconfigs := []string{
kubeadmconstants.AdminKubeConfigFileName,
kubeadmconstants.ControllerManagerKubeConfigFileName,
kubeadmconstants.SchedulerKubeConfigFileName,
//NB. we are escluding KubeletKubeConfig from renewal because management of this certificate is delegated to kubelet
}
for _, k := range kubeconfigs {
for _, handler := range rm.Certificates() {
// get the cobra.Command skeleton for this command
cmd := generateEmbeddedCertRenewalCommand(k, cfg)
cmd := &cobra.Command{
Use: handler.Name,
Short: fmt.Sprintf("Renew the %s", handler.LongName),
Long: fmt.Sprintf(genericCertRenewLongDesc, handler.LongName),
}
addFlags(cmd, flags)
// get the implementation of renewing this certificate
renewalFunc := func(kdir, k string) func() {
return func() { renewEmbeddedCert(kdir, k, cfg) }
}(kdir, k)
renewalFunc := func(handler *renewal.CertificateRenewHandler) func() {
return func() { renewCert(flags, kdir, handler) }
}(handler)
// install the implementation into the command
cmd.Run = func(*cobra.Command, []string) { renewalFunc() }
cmdList = append(cmdList, cmd)
@ -153,134 +140,60 @@ func getRenewSubCommands(kdir string) []*cobra.Command {
}
},
}
addFlags(allCmd, cfg)
addFlags(allCmd, flags)
cmdList = append(cmdList, allCmd)
return cmdList
}
func addFlags(cmd *cobra.Command, cfg *renewConfig) {
options.AddConfigFlag(cmd.Flags(), &cfg.cfgPath)
options.AddCertificateDirFlag(cmd.Flags(), &cfg.cfg.CertificatesDir)
options.AddKubeConfigFlag(cmd.Flags(), &cfg.kubeconfigPath)
options.AddCSRFlag(cmd.Flags(), &cfg.useCSR)
options.AddCSRDirFlag(cmd.Flags(), &cfg.csrPath)
cmd.Flags().BoolVar(&cfg.useAPI, "use-api", cfg.useAPI, "Use the Kubernetes certificate API to renew certificates")
func addFlags(cmd *cobra.Command, flags *renewFlags) {
options.AddConfigFlag(cmd.Flags(), &flags.cfgPath)
options.AddCertificateDirFlag(cmd.Flags(), &flags.cfg.CertificatesDir)
options.AddKubeConfigFlag(cmd.Flags(), &flags.kubeconfigPath)
options.AddCSRFlag(cmd.Flags(), &flags.csrOnly)
options.AddCSRDirFlag(cmd.Flags(), &flags.csrPath)
cmd.Flags().BoolVar(&flags.useAPI, "use-api", flags.useAPI, "Use the Kubernetes certificate API to renew certificates")
}
func renewCert(cert *certsphase.KubeadmCert, caCert *certsphase.KubeadmCert, cfg *renewConfig) {
internalcfg, err := configutil.LoadOrDefaultInitConfiguration(cfg.cfgPath, &cfg.cfg)
func renewCert(flags *renewFlags, kdir string, handler *renewal.CertificateRenewHandler) {
internalcfg, err := configutil.LoadOrDefaultInitConfiguration(flags.cfgPath, &flags.cfg)
kubeadmutil.CheckErr(err)
// if the renewal operation is set to generate only CSR request
if cfg.useCSR {
// trigger CSR generation in the csrPath, or if this one is missing, in the CertificateDir
path := cfg.csrPath
if path == "" {
path = cfg.cfg.CertificatesDir
// Get a renewal manager for the given cluster configuration
rm, err := renewal.NewManager(&internalcfg.ClusterConfiguration, kdir)
kubeadmutil.CheckErr(err)
// if the renewal operation is set to generate CSR request only
if flags.csrOnly {
// checks a path for storing CSR request is given
if flags.csrPath == "" {
kubeadmutil.CheckErr(errors.New("please provide a path where CSR request should be stored"))
}
err := certsphase.CreateCSR(cert, internalcfg, path)
err := rm.CreateRenewCSR(handler.Name, flags.csrPath)
kubeadmutil.CheckErr(err)
return
}
// otherwise, the renewal operation has to actually renew a certificate
var externalCA bool
switch caCert.BaseName {
case kubeadmconstants.CACertAndKeyBaseName:
// Check if an external CA is provided by the user (when the CA Cert is present but the CA Key is not)
externalCA, _ = certsphase.UsingExternalCA(&internalcfg.ClusterConfiguration)
case kubeadmconstants.FrontProxyCACertAndKeyBaseName:
// Check if an external Front-Proxy CA is provided by the user (when the Front-Proxy CA Cert is present but the Front-Proxy CA Key is not)
externalCA, _ = certsphase.UsingExternalFrontProxyCA(&internalcfg.ClusterConfiguration)
default:
externalCA = false
}
if !externalCA {
renewer, err := getRenewer(cfg, caCert.BaseName)
kubeadmutil.CheckErr(err)
err = renewal.RenewExistingCert(internalcfg.CertificatesDir, cert.BaseName, renewer)
kubeadmutil.CheckErr(err)
fmt.Printf("Certificate %s renewed\n", cert.Name)
return
}
fmt.Printf("Detected external %s, certificate %s can't be renewed\n", cert.CAName, cert.Name)
}
func renewEmbeddedCert(kdir, k string, cfg *renewConfig) {
internalcfg, err := configutil.LoadOrDefaultInitConfiguration(cfg.cfgPath, &cfg.cfg)
kubeadmutil.CheckErr(err)
// if the renewal operation is set to generate only CSR request
if cfg.useCSR {
// trigger CSR generation in the csrPath, or if this one is missing, in the CertificateDir
path := cfg.csrPath
if path == "" {
path = cfg.cfg.CertificatesDir
}
err := certsphase.CreateCSR(nil, internalcfg, path)
kubeadmutil.CheckErr(err)
return
}
// otherwise, the renewal operation has to actually renew a certificate
// Check if an external CA is provided by the user (when the CA Cert is present but the CA Key is not)
externalCA, _ := certsphase.UsingExternalCA(&internalcfg.ClusterConfiguration)
if !externalCA {
renewer, err := getRenewer(cfg, certsphase.KubeadmCertRootCA.BaseName)
kubeadmutil.CheckErr(err)
err = renewal.RenewEmbeddedClientCert(kdir, k, renewer)
kubeadmutil.CheckErr(err)
fmt.Printf("Certificate embedded in %s renewed\n", k)
return
}
fmt.Printf("Detected external CA, certificate embedded in %s can't be renewed\n", k)
}
func generateCertRenewalCommand(cert *certsphase.KubeadmCert, cfg *renewConfig) *cobra.Command {
cmd := &cobra.Command{
Use: cert.Name,
Short: fmt.Sprintf("Renew the %s", cert.LongName),
Long: fmt.Sprintf(genericCertRenewLongDesc, cert.LongName, cert.BaseName),
}
addFlags(cmd, cfg)
return cmd
}
func generateEmbeddedCertRenewalCommand(k string, cfg *renewConfig) *cobra.Command {
cmd := &cobra.Command{
Use: k,
Short: fmt.Sprintf("Renew the certificate embedded in %s", k),
Long: fmt.Sprintf(genericCertRenewEmbeddedLongDesc, k),
}
addFlags(cmd, cfg)
return cmd
}
func getRenewer(cfg *renewConfig, caCertBaseName string) (renewal.Interface, error) {
if cfg.useAPI {
kubeConfigPath := cmdutil.GetKubeConfigPath(cfg.kubeconfigPath)
// renew the certificate using the requested renew method
if flags.useAPI {
// renew using K8s certificate API
kubeConfigPath := cmdutil.GetKubeConfigPath(flags.kubeconfigPath)
client, err := kubeconfigutil.ClientSetFromFile(kubeConfigPath)
if err != nil {
return nil, err
kubeadmutil.CheckErr(err)
err = rm.RenewUsingCSRAPI(handler.Name, client)
kubeadmutil.CheckErr(err)
} else {
// renew using local certificate authorities.
// this operation can't complete in case the certificate key is not provided (external CA)
renewed, err := rm.RenewUsingLocalCA(handler.Name)
kubeadmutil.CheckErr(err)
if !renewed {
fmt.Printf("Detected external %s, %s can't be renewed\n", handler.CABaseName, handler.LongName)
return
}
return renewal.NewCertsAPIRenawal(client), nil
}
caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.cfg.CertificatesDir, caCertBaseName)
if err != nil {
return nil, err
}
return renewal.NewFileRenewal(caCert, caKey), nil
fmt.Printf("%s renewed\n", handler.LongName)
}

View File

@ -55,6 +55,10 @@ func TestCommandsGenerated(t *testing.T) {
"renew etcd-server",
"renew etcd-peer",
"renew etcd-healthcheck-client",
"renew admin.conf",
"renew scheduler.conf",
"renew controller-manager.conf",
}
renewCmd := newCmdCertsRenewal()
@ -79,19 +83,63 @@ func TestCommandsGenerated(t *testing.T) {
}
func TestRunRenewCommands(t *testing.T) {
tmpDir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpDir)
cfg := testutil.GetDefaultInternalConfig(t)
cfg.CertificatesDir = tmpDir
// Generate all the CA
CACerts := map[string]*x509.Certificate{}
CAKeys := map[string]crypto.Signer{}
for _, ca := range []*certsphase.KubeadmCert{
&certsphase.KubeadmCertRootCA,
&certsphase.KubeadmCertFrontProxyCA,
&certsphase.KubeadmCertEtcdCA,
} {
caCert, caKey, err := ca.CreateAsCA(cfg)
if err != nil {
t.Fatalf("couldn't write out CA %s: %v", ca.Name, err)
}
CACerts[ca.Name] = caCert
CAKeys[ca.Name] = caKey
}
// Generate all the signed certificates
for _, cert := range []*certsphase.KubeadmCert{
&certsphase.KubeadmCertAPIServer,
&certsphase.KubeadmCertKubeletClient,
&certsphase.KubeadmCertFrontProxyClient,
&certsphase.KubeadmCertEtcdAPIClient,
&certsphase.KubeadmCertEtcdServer,
&certsphase.KubeadmCertEtcdPeer,
&certsphase.KubeadmCertEtcdHealthcheck,
} {
caCert := CACerts[cert.CAName]
caKey := CAKeys[cert.CAName]
if err := cert.CreateFromCA(cfg, caCert, caKey); err != nil {
t.Fatalf("couldn't write certificate %s: %v", cert.Name, err)
}
}
// Generate all the kubeconfig files with embedded certs
for _, kubeConfig := range []string{
kubeadmconstants.AdminKubeConfigFileName,
kubeadmconstants.SchedulerKubeConfigFileName,
kubeadmconstants.ControllerManagerKubeConfigFileName,
} {
if err := kubeconfigphase.CreateKubeConfigFile(kubeConfig, tmpDir, cfg); err != nil {
t.Fatalf("couldn't create kubeconfig %q: %v", kubeConfig, err)
}
}
tests := []struct {
command string
CAs []*certsphase.KubeadmCert
Certs []*certsphase.KubeadmCert
KubeconfigFiles []string
}{
{
command: "all",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertRootCA,
&certsphase.KubeadmCertFrontProxyCA,
&certsphase.KubeadmCertEtcdCA,
},
Certs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertAPIServer,
&certsphase.KubeadmCertKubeletClient,
@ -109,90 +157,60 @@ func TestRunRenewCommands(t *testing.T) {
},
{
command: "apiserver",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertRootCA,
},
Certs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertAPIServer,
},
},
{
command: "apiserver-kubelet-client",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertRootCA,
},
Certs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertKubeletClient,
},
},
{
command: "apiserver-etcd-client",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdCA,
},
Certs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdAPIClient,
},
},
{
command: "front-proxy-client",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertFrontProxyCA,
},
Certs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertFrontProxyClient,
},
},
{
command: "etcd-server",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdCA,
},
Certs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdServer,
},
},
{
command: "etcd-peer",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdCA,
},
Certs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdPeer,
},
},
{
command: "etcd-healthcheck-client",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdCA,
},
Certs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdHealthcheck,
},
},
{
command: "admin.conf",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertRootCA,
},
KubeconfigFiles: []string{
kubeadmconstants.AdminKubeConfigFileName,
},
},
{
command: "scheduler.conf",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertRootCA,
},
KubeconfigFiles: []string{
kubeadmconstants.SchedulerKubeConfigFileName,
},
},
{
command: "controller-manager.conf",
CAs: []*certsphase.KubeadmCert{
&certsphase.KubeadmCertRootCA,
},
KubeconfigFiles: []string{
kubeadmconstants.ControllerManagerKubeConfigFileName,
},
@ -201,74 +219,43 @@ func TestRunRenewCommands(t *testing.T) {
for _, test := range tests {
t.Run(test.command, func(t *testing.T) {
tmpDir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpDir)
cfg := testutil.GetDefaultInternalConfig(t)
cfg.CertificatesDir = tmpDir
// Generate all the CA
CACerts := map[string]*x509.Certificate{}
CAKeys := map[string]crypto.Signer{}
for _, ca := range test.CAs {
caCert, caKey, err := ca.CreateAsCA(cfg)
if err != nil {
t.Fatalf("couldn't write out CA %s: %v", ca.Name, err)
}
CACerts[ca.Name] = caCert
CAKeys[ca.Name] = caKey
}
// Generate all the signed certificates (and store creation time)
createTime := map[string]time.Time{}
// Get file ModTime before renew
ModTime := map[string]time.Time{}
for _, cert := range test.Certs {
caCert := CACerts[cert.CAName]
caKey := CAKeys[cert.CAName]
if err := cert.CreateFromCA(cfg, caCert, caKey); err != nil {
t.Fatalf("couldn't write certificate %s: %v", cert.Name, err)
}
file, err := os.Stat(filepath.Join(tmpDir, fmt.Sprintf("%s.crt", cert.BaseName)))
if err != nil {
t.Fatalf("couldn't get certificate %s: %v", cert.Name, err)
}
createTime[cert.Name] = file.ModTime()
ModTime[cert.Name] = file.ModTime()
}
// Generate all the kubeconfig files with embedded certs(and store creation time)
for _, kubeConfig := range test.KubeconfigFiles {
if err := kubeconfigphase.CreateKubeConfigFile(kubeConfig, tmpDir, cfg); err != nil {
t.Fatalf("couldn't create kubeconfig %q: %v", kubeConfig, err)
}
file, err := os.Stat(filepath.Join(tmpDir, kubeConfig))
if err != nil {
t.Fatalf("couldn't get kubeconfig %s: %v", kubeConfig, err)
}
createTime[kubeConfig] = file.ModTime()
ModTime[kubeConfig] = file.ModTime()
}
// exec renew
renewCmds := getRenewSubCommands(tmpDir)
cmdtestutil.RunSubCommand(t, renewCmds, test.command, fmt.Sprintf("--cert-dir=%s", tmpDir))
// read renewed certificates and check the file is modified
// check the file is modified
for _, cert := range test.Certs {
file, err := os.Stat(filepath.Join(tmpDir, fmt.Sprintf("%s.crt", cert.BaseName)))
if err != nil {
t.Fatalf("couldn't get certificate %s: %v", cert.Name, err)
}
if createTime[cert.Name] == file.ModTime() {
if ModTime[cert.Name] == file.ModTime() {
t.Errorf("certificate %s was not renewed as expected", cert.Name)
}
}
// ead renewed kubeconfig files and check the file is modified
for _, kubeConfig := range test.KubeconfigFiles {
file, err := os.Stat(filepath.Join(tmpDir, kubeConfig))
if err != nil {
t.Fatalf("couldn't get kubeconfig %s: %v", kubeConfig, err)
}
if createTime[kubeConfig] == file.ModTime() {
if ModTime[kubeConfig] == file.ModTime() {
t.Errorf("kubeconfig %s was not renewed as expected", kubeConfig)
}
}
@ -281,10 +268,22 @@ func TestRenewUsingCSR(t *testing.T) {
defer os.RemoveAll(tmpDir)
cert := &certs.KubeadmCertEtcdServer
renewCmds := getRenewSubCommands(tmpDir)
cmdtestutil.RunSubCommand(t, renewCmds, cert.Name, "--csr-only", "--csr-dir="+tmpDir)
cfg := testutil.GetDefaultInternalConfig(t)
cfg.CertificatesDir = tmpDir
if _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(tmpDir, cert.BaseName); err != nil {
t.Fatalf("couldn't load certificate %q: %v", cert.BaseName, err)
caCert, caKey, err := certsphase.KubeadmCertEtcdCA.CreateAsCA(cfg)
if err != nil {
t.Fatalf("couldn't write out CA %s: %v", certsphase.KubeadmCertEtcdCA.Name, err)
}
if err := cert.CreateFromCA(cfg, caCert, caKey); err != nil {
t.Fatalf("couldn't write certificate %s: %v", cert.Name, err)
}
renewCmds := getRenewSubCommands(tmpDir)
cmdtestutil.RunSubCommand(t, renewCmds, cert.Name, "--csr-only", "--csr-dir="+tmpDir, fmt.Sprintf("--cert-dir=%s", tmpDir))
if _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(tmpDir, cert.Name); err != nil {
t.Fatalf("couldn't load certificate %q: %v", cert.Name, err)
}
}

View File

@ -260,7 +260,7 @@ var (
// KubeadmCertKubeletClient is the definition of the cert used by the API server to access the kubelet.
KubeadmCertKubeletClient = KubeadmCert{
Name: "apiserver-kubelet-client",
LongName: "Client certificate for the API server to connect to kubelet",
LongName: "certificate for the API server to connect to kubelet",
BaseName: kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName,
CAName: "ca",
config: certutil.Config{
@ -284,7 +284,7 @@ var (
KubeadmCertFrontProxyClient = KubeadmCert{
Name: "front-proxy-client",
BaseName: kubeadmconstants.FrontProxyClientCertAndKeyBaseName,
LongName: "client for the front proxy",
LongName: "certificate for the front proxy client",
CAName: "front-proxy-ca",
config: certutil.Config{
CommonName: kubeadmconstants.FrontProxyClientCertCommonName,
@ -322,7 +322,7 @@ var (
// KubeadmCertEtcdPeer is the definition of the cert used by etcd peers to access each other.
KubeadmCertEtcdPeer = KubeadmCert{
Name: "etcd-peer",
LongName: "credentials for etcd nodes to communicate with each other",
LongName: "certificate for etcd nodes to communicate with each other",
BaseName: kubeadmconstants.EtcdPeerCertAndKeyBaseName,
CAName: "etcd-ca",
config: certutil.Config{
@ -336,7 +336,7 @@ var (
// KubeadmCertEtcdHealthcheck is the definition of the cert used by Kubernetes to check the health of the etcd server.
KubeadmCertEtcdHealthcheck = KubeadmCert{
Name: "etcd-healthcheck-client",
LongName: "client certificate for liveness probes to healtcheck etcd",
LongName: "certificate for liveness probes to healtcheck etcd",
BaseName: kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName,
CAName: "etcd-ca",
config: certutil.Config{
@ -348,7 +348,7 @@ var (
// KubeadmCertEtcdAPIClient is the definition of the cert used by the API server to access etcd.
KubeadmCertEtcdAPIClient = KubeadmCert{
Name: "apiserver-etcd-client",
LongName: "client apiserver uses to access etcd",
LongName: "certificate the apiserver uses to access etcd",
BaseName: kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName,
CAName: "etcd-ca",
config: certutil.Config{

View File

@ -3,20 +3,24 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"certsapi.go",
"filerenewal.go",
"interface.go",
"renewal.go",
"apirenewer.go",
"filerenewer.go",
"manager.go",
"readwriter.go",
],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal",
visibility = ["//visibility:public"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/phases/certs:go_default_library",
"//cmd/kubeadm/app/util/pkiutil:go_default_library",
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library",
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
"//staging/src/k8s.io/client-go/util/certificate/csr:go_default_library",
"//staging/src/k8s.io/client-go/util/keyutil:go_default_library",
@ -27,11 +31,14 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"filerenewal_test.go",
"renewal_test.go",
"apirenewer_test.go",
"filerenewer_test.go",
"manager_test.go",
"readwriter_test.go",
],
embed = [":go_default_library"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/util/certs:go_default_library",
"//cmd/kubeadm/app/util/kubeconfig:go_default_library",
"//cmd/kubeadm/app/util/pkiutil:go_default_library",

View File

@ -27,7 +27,7 @@ import (
certsapi "k8s.io/api/certificates/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
clientset "k8s.io/client-go/kubernetes"
certstype "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
certutil "k8s.io/client-go/util/cert"
csrutil "k8s.io/client-go/util/certificate/csr"
@ -38,20 +38,20 @@ const certAPIPrefixName = "kubeadm-cert"
var watchTimeout = 5 * time.Minute
// CertsAPIRenewal creates new certificates using the certs API
type CertsAPIRenewal struct {
// APIRenewer define a certificate renewer implementation that uses the K8s certificate API
type APIRenewer struct {
client certstype.CertificatesV1beta1Interface
}
// NewCertsAPIRenawal takes a Kubernetes interface and returns a renewal Interface.
func NewCertsAPIRenawal(client kubernetes.Interface) Interface {
return &CertsAPIRenewal{
// NewAPIRenewer a new certificate renewer implementation that uses the K8s certificate API
func NewAPIRenewer(client clientset.Interface) *APIRenewer {
return &APIRenewer{
client: client.CertificatesV1beta1(),
}
}
// Renew takes a certificate using the cert and key.
func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) {
// Renew a certificate using the K8s certificate API
func (r *APIRenewer) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) {
reqTmp := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: cfg.CommonName,

View File

@ -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),
},
}
}

View File

@ -24,21 +24,21 @@ import (
"k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil"
)
// FileRenewal renews a certificate using local certs
type FileRenewal struct {
// FileRenewer define a certificate renewer implementation that uses given CA cert and key for generating new certficiates
type FileRenewer struct {
caCert *x509.Certificate
caKey crypto.Signer
}
// NewFileRenewal takes a certificate pair to construct the Interface.
func NewFileRenewal(caCert *x509.Certificate, caKey crypto.Signer) Interface {
return &FileRenewal{
// NewFileRenewer returns a new certificate renewer that uses given CA cert and key for generating new certficiates
func NewFileRenewer(caCert *x509.Certificate, caKey crypto.Signer) *FileRenewer {
return &FileRenewer{
caCert: caCert,
caKey: caKey,
}
}
// Renew takes a certificate using the cert and key
func (r *FileRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) {
// Renew a certificate using a given CA cert and key
func (r *FileRenewer) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.Signer, error) {
return pkiutil.NewCertAndKey(r.caCert, r.caKey, cfg)
}

View File

@ -21,18 +21,13 @@ import (
"testing"
certutil "k8s.io/client-go/util/cert"
"k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil"
)
func TestFileRenew(t *testing.T) {
caCertCfg := &certutil.Config{CommonName: "kubernetes"}
caCert, caKey, err := pkiutil.NewCertificateAuthority(caCertCfg)
if err != nil {
t.Fatalf("couldn't create CA: %v", err)
}
fr := NewFileRenewal(caCert, caKey)
func TestFileRenewer(t *testing.T) {
// creates a File renewer using a test Certificate authority
fr := NewFileRenewer(testCACert, testCAKey)
// renews a certificate
certCfg := &certutil.Config{
CommonName: "test-certs",
AltNames: certutil.AltNames{
@ -46,8 +41,9 @@ func TestFileRenew(t *testing.T) {
t.Fatalf("unexpected error renewing cert: %v", err)
}
// verify the renewed certificate
pool := x509.NewCertPool()
pool.AddCert(caCert)
pool.AddCert(testCACert)
_, err = cert.Verify(x509.VerifyOptions{
DNSName: "test-domain.space",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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...)
}

View File

@ -76,6 +76,7 @@ go_test(
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/phases/certs:go_default_library",
"//cmd/kubeadm/app/phases/certs/renewal:go_default_library",
"//cmd/kubeadm/app/phases/controlplane:go_default_library",
"//cmd/kubeadm/app/phases/etcd:go_default_library",
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",

View File

@ -178,7 +178,7 @@ func (spm *KubeStaticPodPathManager) CleanupDirs() error {
return utilerrors.NewAggregate(errlist)
}
func upgradeComponent(component string, renewCerts bool, waiter apiclient.Waiter, pathMgr StaticPodPathManager, cfg *kubeadmapi.InitConfiguration, beforePodHash string, recoverManifests map[string]string) error {
func upgradeComponent(component string, certsRenewMgr *renewal.Manager, waiter apiclient.Waiter, pathMgr StaticPodPathManager, cfg *kubeadmapi.InitConfiguration, beforePodHash string, recoverManifests map[string]string) error {
// Special treatment is required for etcd case, when rollbackOldManifests should roll back etcd
// manifests only for the case when component is Etcd
recoverEtcd := false
@ -211,9 +211,9 @@ func upgradeComponent(component string, renewCerts bool, waiter apiclient.Waiter
}
// if certificate renewal should be performed
if renewCerts {
if certsRenewMgr != nil {
// renew all the certificates used by the current component
if err := renewCertsByComponent(cfg, pathMgr.KubernetesDir(), component); err != nil {
if err := renewCertsByComponent(cfg, component, certsRenewMgr); err != nil {
return rollbackOldManifests(recoverManifests, errors.Wrapf(err, "failed to renew certificates for component %q", component), pathMgr, recoverEtcd)
}
}
@ -256,7 +256,7 @@ func upgradeComponent(component string, renewCerts bool, waiter apiclient.Waiter
}
// performEtcdStaticPodUpgrade performs upgrade of etcd, it returns bool which indicates fatal error or not and the actual error.
func performEtcdStaticPodUpgrade(renewCerts bool, client clientset.Interface, waiter apiclient.Waiter, pathMgr StaticPodPathManager, cfg *kubeadmapi.InitConfiguration, recoverManifests map[string]string, oldEtcdClient, newEtcdClient etcdutil.ClusterInterrogator) (bool, error) {
func performEtcdStaticPodUpgrade(certsRenewMgr *renewal.Manager, client clientset.Interface, waiter apiclient.Waiter, pathMgr StaticPodPathManager, cfg *kubeadmapi.InitConfiguration, recoverManifests map[string]string, oldEtcdClient, newEtcdClient etcdutil.ClusterInterrogator) (bool, error) {
// Add etcd static pod spec only if external etcd is not configured
if cfg.Etcd.External != nil {
return false, errors.New("external etcd detected, won't try to change any etcd state")
@ -320,7 +320,7 @@ func performEtcdStaticPodUpgrade(renewCerts bool, client clientset.Interface, wa
retryInterval := 15 * time.Second
// Perform etcd upgrade using common to all control plane components function
if err := upgradeComponent(constants.Etcd, renewCerts, waiter, pathMgr, cfg, beforeEtcdPodHash, recoverManifests); err != nil {
if err := upgradeComponent(constants.Etcd, certsRenewMgr, waiter, pathMgr, cfg, beforeEtcdPodHash, recoverManifests); err != nil {
fmt.Printf("[upgrade/etcd] Failed to upgrade etcd: %v\n", err)
// Since upgrade component failed, the old etcd manifest has either been restored or was never touched
// Now we need to check the health of etcd cluster if it is up with old manifest
@ -433,13 +433,21 @@ func StaticPodControlPlane(client clientset.Interface, waiter apiclient.Waiter,
}
}
var certsRenewMgr *renewal.Manager
if renewCerts {
certsRenewMgr, err = renewal.NewManager(&cfg.ClusterConfiguration, pathMgr.KubernetesDir())
if err != nil {
return errors.Wrap(err, "failed to create the certificate renewal manager")
}
}
// etcd upgrade is done prior to other control plane components
if !isExternalEtcd && etcdUpgrade {
// set the TLS upgrade flag for all components
fmt.Printf("[upgrade/etcd] Upgrading to TLS for %s\n", constants.Etcd)
// Perform etcd upgrade using common to all control plane components function
fatal, err := performEtcdStaticPodUpgrade(renewCerts, client, waiter, pathMgr, cfg, recoverManifests, oldEtcdClient, newEtcdClient)
fatal, err := performEtcdStaticPodUpgrade(certsRenewMgr, client, waiter, pathMgr, cfg, recoverManifests, oldEtcdClient, newEtcdClient)
if err != nil {
if fatal {
return err
@ -456,17 +464,22 @@ func StaticPodControlPlane(client clientset.Interface, waiter apiclient.Waiter,
}
for _, component := range constants.ControlPlaneComponents {
if err = upgradeComponent(component, renewCerts, waiter, pathMgr, cfg, beforePodHashMap[component], recoverManifests); err != nil {
if err = upgradeComponent(component, certsRenewMgr, waiter, pathMgr, cfg, beforePodHashMap[component], recoverManifests); err != nil {
return err
}
}
if renewCerts {
// renew the certificate embedded in the admin.conf file
err := renewEmbeddedCertsByName(cfg, pathMgr.KubernetesDir(), constants.AdminKubeConfigFileName)
renewed, err := certsRenewMgr.RenewUsingLocalCA(constants.AdminKubeConfigFileName)
if err != nil {
return rollbackOldManifests(recoverManifests, errors.Wrapf(err, "failed to upgrade the %s certificates", constants.AdminKubeConfigFileName), pathMgr, false)
}
if !renewed {
// if not error, but not renewed because of external CA detected, inform the user
fmt.Printf("[upgrade/staticpods] External CA detected, %s certificate can't be renewed\n", constants.AdminKubeConfigFileName)
}
}
// Remove the temporary directories used on a best-effort (don't fail if the calls error out)
@ -514,121 +527,57 @@ func rollbackEtcdData(cfg *kubeadmapi.InitConfiguration, pathMgr StaticPodPathMa
// renewCertsByComponent takes charge of renewing certificates used by a specific component before
// the static pod of the component is upgraded
func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, kubernetesDir, component string) error {
// if the cluster is using a local etcd
if cfg.Etcd.Local != nil {
if component == constants.Etcd || component == constants.KubeAPIServer {
// try to load the etcd CA
caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertEtcdCA.BaseName)
if err != nil {
return errors.Wrapf(err, "failed to upgrade the %s CA certificate and key", constants.Etcd)
}
// create a renewer for certificates signed by etcd CA
renewer := renewal.NewFileRenewal(caCert, caKey)
// then, if upgrading the etcd component, renew all the certificates signed by etcd CA and used
// by etcd itself (the etcd-server, the etcd-peer and the etcd-healthcheck-client certificate)
if component == constants.Etcd {
for _, cert := range []*certsphase.KubeadmCert{
&certsphase.KubeadmCertEtcdServer,
&certsphase.KubeadmCertEtcdPeer,
&certsphase.KubeadmCertEtcdHealthcheck,
} {
fmt.Printf("[upgrade/staticpods] Renewing %q certificate\n", cert.BaseName)
if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil {
return errors.Wrapf(err, "failed to renew %s certificates", cert.Name)
}
}
}
// if upgrading the apiserver component, renew the certificate signed by etcd CA and used
// by the apiserver (the apiserver-etcd-client certificate)
if component == constants.KubeAPIServer {
cert := certsphase.KubeadmCertEtcdAPIClient
fmt.Printf("[upgrade/staticpods] Renewing %q certificate\n", cert.BaseName)
if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil {
return errors.Wrapf(err, "failed to renew %s certificate and key", cert.Name)
}
func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, component string, certsRenewMgr *renewal.Manager) error {
var certificates []string
// if etcd, only in case of local etcd, renew server, peer and health check certificate
if component == constants.Etcd {
if cfg.Etcd.Local != nil {
certificates = []string{
certsphase.KubeadmCertEtcdServer.Name,
certsphase.KubeadmCertEtcdPeer.Name,
certsphase.KubeadmCertEtcdHealthcheck.Name,
}
}
}
// if apiserver, renew apiserver serving certificate, kubelet and front-proxy client certificate.
//if local etcd, renew also the etcd client certificate
if component == constants.KubeAPIServer {
// Checks if an external CA is provided by the user (when the CA Cert is present but the CA Key is not)
// if not, then CA is managed by kubeadm, so it is possible to renew all the certificates signed by ca
// and used the apis server (the apiserver certificate and the apiserver-kubelet-client certificate)
externalCA, _ := certsphase.UsingExternalCA(&cfg.ClusterConfiguration)
if !externalCA {
// try to load ca
caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertRootCA.BaseName)
if err != nil {
return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.KubeAPIServer)
}
// create a renewer for certificates signed by CA
renewer := renewal.NewFileRenewal(caCert, caKey)
// renew the certificates
for _, cert := range []*certsphase.KubeadmCert{
&certsphase.KubeadmCertAPIServer,
&certsphase.KubeadmCertKubeletClient,
} {
fmt.Printf("[upgrade/staticpods] Renewing %q certificate\n", cert.BaseName)
if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil {
return errors.Wrapf(err, "failed to renew %s certificate and key", cert.Name)
}
}
certificates = []string{
certsphase.KubeadmCertAPIServer.Name,
certsphase.KubeadmCertKubeletClient.Name,
certsphase.KubeadmCertFrontProxyClient.Name,
}
// Checks if an external Front-Proxy CA is provided by the user (when the Front-Proxy CA Cert is present but the Front-Proxy CA Key is not)
// if not, then Front-Proxy CA is managed by kubeadm, so it is possible to renew all the certificates signed by ca
// and used the apis server (the front-proxy-client certificate)
externalFrontProxyCA, _ := certsphase.UsingExternalFrontProxyCA(&cfg.ClusterConfiguration)
if !externalFrontProxyCA {
// try to load front-proxy-ca
caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertFrontProxyCA.BaseName)
if err != nil {
return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.KubeAPIServer)
}
// create a renewer for certificates signed by Front-Proxy CA
renewer := renewal.NewFileRenewal(caCert, caKey)
// renew the certificates
cert := certsphase.KubeadmCertFrontProxyClient
fmt.Printf("[upgrade/staticpods] Renewing %q certificate\n", cert.BaseName)
if err := renewal.RenewExistingCert(cfg.CertificatesDir, cert.BaseName, renewer); err != nil {
return errors.Wrapf(err, "failed to renew %s certificate and key", cert.Name)
}
if cfg.Etcd.Local != nil {
certificates = append(certificates, certsphase.KubeadmCertEtcdAPIClient.Name)
}
}
// if controller-manager, renew the certificate embedded in the controller-manager kubeConfig file
if component == constants.KubeControllerManager {
// renew the certificate embedded in the controller-manager.conf file
err := renewEmbeddedCertsByName(cfg, kubernetesDir, constants.ControllerManagerKubeConfigFileName)
if err != nil {
return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.ControllerManagerKubeConfigFileName)
certificates = []string{
constants.ControllerManagerKubeConfigFileName,
}
}
// if scheduler, renew the certificate embedded in the scheduler kubeConfig file
if component == constants.KubeScheduler {
// renew the certificate embedded in the scheduler.conf file
err := renewEmbeddedCertsByName(cfg, kubernetesDir, constants.SchedulerKubeConfigFileName)
if err != nil {
return errors.Wrapf(err, "failed to upgrade the %s certificates", constants.SchedulerKubeConfigFileName)
certificates = []string{
constants.SchedulerKubeConfigFileName,
}
}
return nil
}
func renewEmbeddedCertsByName(cfg *kubeadmapi.InitConfiguration, kubernetesDir, kubeConfigFile string) error {
// Checks if an external CA is provided by the user (when the CA Cert is present but the CA Key is not)
// if not, then CA is managed by kubeadm, so it is possible to renew all the certificates signed by ca
// and used by the apis server (the apiserver certificate and the apiserver-kubelet-client certificate)
externalCA, _ := certsphase.UsingExternalCA(&cfg.ClusterConfiguration)
if !externalCA {
// try to load ca
caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.CertificatesDir, certsphase.KubeadmCertRootCA.BaseName)
// renew the selected components
for _, cert := range certificates {
fmt.Printf("[upgrade/staticpods] Renewing %s certificate\n", cert)
renewed, err := certsRenewMgr.RenewUsingLocalCA(cert)
if err != nil {
return errors.Wrapf(err, "failed to upgrade the %s certificates", kubeConfigFile)
return err
}
// create a renewer for certificates signed by CA
renewer := renewal.NewFileRenewal(caCert, caKey)
// renew the certificate embedded in the controller-manager.conf file
fmt.Printf("[upgrade/staticpods] Renewing certificate embedded in %q \n", kubeConfigFile)
if err := renewal.RenewEmbeddedClientCert(kubernetesDir, kubeConfigFile, renewer); err != nil {
return errors.Wrapf(err, "failed to renew certificate embedded in %s", kubeConfigFile)
if !renewed {
// if not error, but not renewed because of external CA detected, inform the user
fmt.Printf("[upgrade/staticpods] External CA detected, %s certificate can't be renewed\n", cert)
}
}

View File

@ -38,6 +38,7 @@ import (
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal"
controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane"
etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd"
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
@ -823,7 +824,12 @@ func TestRenewCertsByComponent(t *testing.T) {
}
// Renew everything
err := renewCertsByComponent(cfg, tmpDir, test.component)
rm, err := renewal.NewManager(&cfg.ClusterConfiguration, tmpDir)
if err != nil {
t.Fatalf("Failed to create the certificate renewal manager: %v", err)
}
err = renewCertsByComponent(cfg, test.component, rm)
if test.shouldErrorOnRenew {
if err == nil {
t.Fatal("expected renewal error, got nothing")