diff --git a/cmd/kubeadm/app/cmd/phases/certs/BUILD b/cmd/kubeadm/app/cmd/phases/certs/BUILD index bf17b3eacc8..b0ab2077b81 100644 --- a/cmd/kubeadm/app/cmd/phases/certs/BUILD +++ b/cmd/kubeadm/app/cmd/phases/certs/BUILD @@ -24,7 +24,14 @@ go_test( name = "go_default_test", srcs = ["renewal_test.go"], embed = [":go_default_library"], - deps = ["//vendor/github.com/spf13/cobra:go_default_library"], + deps = [ + "//cmd/kubeadm/app/constants:go_default_library", + "//cmd/kubeadm/app/phases/certs/pkiutil:go_default_library", + "//cmd/kubeadm/test:go_default_library", + "//cmd/kubeadm/test/certs:go_default_library", + "//cmd/kubeadm/test/cmd:go_default_library", + "//vendor/github.com/spf13/cobra:go_default_library", + ], ) filegroup( diff --git a/cmd/kubeadm/app/cmd/phases/certs/renew.go b/cmd/kubeadm/app/cmd/phases/certs/renew.go index 62891cbb6a0..e1ef98a09a3 100644 --- a/cmd/kubeadm/app/cmd/phases/certs/renew.go +++ b/cmd/kubeadm/app/cmd/phases/certs/renew.go @@ -35,18 +35,13 @@ import ( ) func NewCmdCertsRenewal() *cobra.Command { - cfg := &renewConfig{ - kubeconfigPath: constants.GetAdminKubeConfigPath(), - } - cmd := &cobra.Command{ Use: "renew", Short: "Renews all known certificates for kubeadm", Long: "", // TODO EKF fill out } - addFlags(cmd, cfg) - cmd.AddCommand(getRenewSubCommands(cfg)...) + cmd.AddCommand(getRenewSubCommands()...) return cmd } @@ -58,7 +53,10 @@ type renewConfig struct { useAPI bool } -func getRenewSubCommands(cfg *renewConfig) []*cobra.Command { +func getRenewSubCommands() []*cobra.Command { + cfg := &renewConfig{ + kubeconfigPath: constants.GetAdminKubeConfigPath(), + } // Default values for the cobra help text kubeadmscheme.Scheme.Default(&cfg.cfg) @@ -84,28 +82,31 @@ func addFlags(cmd *cobra.Command, cfg *renewConfig) { cmd.Flags().BoolVar(&cfg.useAPI, "use-api", cfg.useAPI, "Use the kubernetes certificate API to renew certificates") } -func generateCertCommand(name, longName string) *cobra.Command { +// generateCertCommand takes mostly strings instead of structs to avoid using structs in a for loop +func generateCertCommand(name, longName, baseName, caCertBaseName string, cfg *renewConfig) *cobra.Command { return &cobra.Command{ Use: name, Short: fmt.Sprintf("Generates the %s", longName), Long: "", // TODO EKF fill out + Run: func(cmd *cobra.Command, args []string) { + internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(cfg.cfgPath, &cfg.cfg) + kubeadmutil.CheckErr(err) + renewer, err := getRenewer(cfg, caCertBaseName) + kubeadmutil.CheckErr(err) + + err = renewal.RenewExistingCert(internalcfg.CertificatesDir, baseName, renewer) + kubeadmutil.CheckErr(err) + }, } } func makeCommandForRenew(cert *certsphase.KubeadmCert, caCert *certsphase.KubeadmCert, cfg *renewConfig) *cobra.Command { - certCmd := generateCertCommand(cert.Name, cert.LongName) + certCmd := generateCertCommand(cert.Name, cert.LongName, cert.BaseName, caCert.BaseName, cfg) addFlags(certCmd, cfg) - - certCmd.Run = func(cmd *cobra.Command, args []string) { - internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(cfg.cfgPath, &cfg.cfg) - kubeadmutil.CheckErr(err) - - renewer, err := getRenewer(cfg, caCert) - } return certCmd } -func getRenewer(cfg *renewConfig, caCertSpec *certsphase.KubeadmCert) (renewal.Interface, error) { +func getRenewer(cfg *renewConfig, caCertBaseName string) (renewal.Interface, error) { if cfg.useAPI { kubeConfigPath := cmdutil.FindExistingKubeConfig(cfg.kubeconfigPath) client, err := kubeconfigutil.ClientSetFromFile(kubeConfigPath) @@ -115,7 +116,7 @@ func getRenewer(cfg *renewConfig, caCertSpec *certsphase.KubeadmCert) (renewal.I return renewal.NewCertsAPIRenawal(client), nil } - caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.cfg.CertificatesDir, caCertSpec.BaseName) + caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.cfg.CertificatesDir, caCertBaseName) if err != nil { return nil, err } diff --git a/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go b/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go index 95a1c3408d1..217ff880e1b 100644 --- a/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go +++ b/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go @@ -17,10 +17,24 @@ limitations under the License. package renew import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + "os" "strings" "testing" + "time" "github.com/spf13/cobra" + + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" + testutil "k8s.io/kubernetes/cmd/kubeadm/test" + certstestutil "k8s.io/kubernetes/cmd/kubeadm/test/certs" + cmdtestutil "k8s.io/kubernetes/cmd/kubeadm/test/cmd" ) func TestCommandsGenerated(t *testing.T) { @@ -31,7 +45,8 @@ func TestCommandsGenerated(t *testing.T) { } expectedCommands := []string{ - "renew", + // TODO(EKF): add `renew all` + // "renew", "renew apiserver", "renew apiserver-kubelet-client", @@ -64,3 +79,122 @@ func TestCommandsGenerated(t *testing.T) { }) } } + +func TestRunRenewCommands(t *testing.T) { + tests := []struct { + command string + baseName string + caBaseName string + }{ + { + command: "apiserver", + baseName: kubeadmconstants.APIServerCertAndKeyBaseName, + caBaseName: kubeadmconstants.CACertAndKeyBaseName, + }, + { + command: "apiserver-kubelet-client", + baseName: kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName, + caBaseName: kubeadmconstants.CACertAndKeyBaseName, + }, + { + command: "apiserver-etcd-client", + baseName: kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName, + caBaseName: kubeadmconstants.EtcdCACertAndKeyBaseName, + }, + { + command: "front-proxy-client", + baseName: kubeadmconstants.FrontProxyClientCertAndKeyBaseName, + caBaseName: kubeadmconstants.FrontProxyCACertAndKeyBaseName, + }, + { + command: "etcd-server", + baseName: kubeadmconstants.EtcdServerCertAndKeyBaseName, + caBaseName: kubeadmconstants.EtcdCACertAndKeyBaseName, + }, + { + command: "etcd-peer", + baseName: kubeadmconstants.EtcdPeerCertAndKeyBaseName, + caBaseName: kubeadmconstants.EtcdCACertAndKeyBaseName, + }, + { + command: "etcd-healthcheck-client", + baseName: kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName, + caBaseName: kubeadmconstants.EtcdCACertAndKeyBaseName, + }, + } + + renewCmds := getRenewSubCommands() + + for _, test := range tests { + t.Run(test.command, func(t *testing.T) { + tmpDir := testutil.SetupTempDir(t) + defer os.RemoveAll(tmpDir) + + caCert, caKey := certstestutil.SetupCertificateAuthorithy(t) + + if err := pkiutil.WriteCertAndKey(tmpDir, test.caBaseName, caCert, caKey); err != nil { + t.Fatalf("couldn't write out CA: %v", err) + } + + certTmpl := x509.Certificate{ + Subject: pkix.Name{ + CommonName: "test-cert", + Organization: []string{"sig-cluster-lifecycle"}, + }, + DNSNames: []string{"test-domain.space"}, + SerialNumber: new(big.Int).SetInt64(0), + NotBefore: time.Now().Add(-time.Hour * 24 * 365), + NotAfter: time.Now().Add(-time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("couldn't generate private key: %v", err) + } + + certDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, key.Public(), caKey) + if err != nil { + t.Fatalf("couldn't generate private key: %v", err) + } + cert, err := x509.ParseCertificate(certDERBytes) + if err != nil { + t.Fatalf("couldn't generate private key: %v", err) + } + + if err := pkiutil.WriteCertAndKey(tmpDir, test.baseName, cert, key); err != nil { + t.Fatalf("couldn't write out initial certificate") + } + + cmdtestutil.RunSubCommand(t, renewCmds, test.command, fmt.Sprintf("--cert-dir=%s", tmpDir)) + + newCert, newKey, err := pkiutil.TryLoadCertAndKeyFromDisk(tmpDir, test.baseName) + if err != nil { + t.Fatalf("couldn't load renewed certificate: %v", err) + } + + certstestutil.AssertCertificateIsSignedByCa(t, newCert, caCert) + + pool := x509.NewCertPool() + pool.AddCert(caCert) + + _, err = newCert.Verify(x509.VerifyOptions{ + DNSName: "test-domain.space", + Roots: pool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + if err != nil { + t.Errorf("couldn't verify renewed cert: %v", err) + } + + pubKey, ok := newCert.PublicKey.(*rsa.PublicKey) + if !ok { + t.Errorf("unknown public key type %T", newCert.PublicKey) + } else if pubKey.N.Cmp(newKey.N) != 0 { + t.Error("private key does not match public key") + } + + }) + } +} diff --git a/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers.go b/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers.go index e9d9f301b80..29c668b2b3c 100644 --- a/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers.go +++ b/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers.go @@ -130,7 +130,7 @@ func WritePublicKey(pkiPath, name string, key *rsa.PublicKey) error { // CertOrKeyExist returns a boolean whether the cert or the key exists func CertOrKeyExist(pkiPath, name string) bool { - certificatePath, privateKeyPath := pathsForCertAndKey(pkiPath, name) + certificatePath, privateKeyPath := PathsForCertAndKey(pkiPath, name) _, certErr := os.Stat(certificatePath) _, keyErr := os.Stat(privateKeyPath) @@ -234,7 +234,8 @@ func TryLoadPrivatePublicKeyFromDisk(pkiPath, name string) (*rsa.PrivateKey, *rs return k, p, nil } -func pathsForCertAndKey(pkiPath, name string) (string, string) { +// PathsForCertAndKey returns the paths for the certificate and key given the path and basename. +func PathsForCertAndKey(pkiPath, name string) (string, string) { return pathForCert(pkiPath, name), pathForKey(pkiPath, name) } diff --git a/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers_test.go b/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers_test.go index 6e3464b9fb5..ad75b5f36f0 100644 --- a/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers_test.go +++ b/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers_test.go @@ -405,7 +405,7 @@ func TestTryLoadKeyFromDisk(t *testing.T) { } func TestPathsForCertAndKey(t *testing.T) { - crtPath, keyPath := pathsForCertAndKey("/foo", "bar") + crtPath, keyPath := PathsForCertAndKey("/foo", "bar") if crtPath != "/foo/bar.crt" { t.Errorf("unexpected certificate path: %s", crtPath) } diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal.go b/cmd/kubeadm/app/phases/certs/renewal/renewal.go index f5b2c75465a..3b934668845 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/renewal.go +++ b/cmd/kubeadm/app/phases/certs/renewal/renewal.go @@ -25,12 +25,17 @@ import ( ) func RenewExistingCert(certsDir, baseName string, impl Interface) error { - cert, err := pkiutil.TryLoadCertFromDisk(certsDir, baseName) + certificatePath, _ := pkiutil.PathsForCertAndKey(certsDir, baseName) + certs, err := certutil.CertsFromFile(certificatePath) if err != nil { return fmt.Errorf("failed to load existing certificate %s: %v", baseName, err) } - cfg := certToConfig(cert) + if len(certs) != 1 { + return fmt.Errorf("wanted exactly one certificate, got %d", len(certs)) + } + + cfg := certToConfig(certs[0]) newCert, newKey, err := impl.Renew(cfg) if err != nil { return fmt.Errorf("failed to renew certificate %s: %v", baseName, err) diff --git a/staging/src/k8s.io/client-go/util/cert/cert.go b/staging/src/k8s.io/client-go/util/cert/cert.go index 0d6794bb5de..fe2158b238d 100644 --- a/staging/src/k8s.io/client-go/util/cert/cert.go +++ b/staging/src/k8s.io/client-go/util/cert/cert.go @@ -20,6 +20,7 @@ import ( "bytes" "crypto/ecdsa" "crypto/elliptic" + "crypto/rand" cryptorand "crypto/rand" "crypto/rsa" "crypto/x509" @@ -87,7 +88,7 @@ func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, er // NewSignedCert creates a signed certificate using the given CA certificate and key func NewSignedCert(cfg Config, key *rsa.PrivateKey, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, error) { - serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64)) + serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) if err != nil { return nil, err }