From a53f478d21928f572d0ee08b8cdbe7531d35f459 Mon Sep 17 00:00:00 2001 From: liz Date: Mon, 27 Aug 2018 16:27:14 -0400 Subject: [PATCH 1/5] Two implmentations of cert renewal --- .../app/phases/certs/renewal/certsapi.go | 151 ++++++++++++++++++ .../app/phases/certs/renewal/filerenewal.go | 51 ++++++ .../phases/certs/renewal/filerenewal_test.go | 61 +++++++ .../app/phases/certs/renewal/interface.go | 29 ++++ .../app/phases/certs/renewal/renewal_test.go | 133 +++++++++++++++ 5 files changed, 425 insertions(+) create mode 100644 cmd/kubeadm/app/phases/certs/renewal/certsapi.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/filerenewal.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/interface.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/renewal_test.go diff --git a/cmd/kubeadm/app/phases/certs/renewal/certsapi.go b/cmd/kubeadm/app/phases/certs/renewal/certsapi.go new file mode 100644 index 00000000000..d7efc1936e5 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/certsapi.go @@ -0,0 +1,151 @@ +/* +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/rand" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "fmt" + "time" + + certsapi "k8s.io/api/certificates/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + certstype "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" + certutil "k8s.io/client-go/util/cert" +) + +const ( + certAPIPrefixName = "kubeadm-cert" +) + +var ( + watchTimeout = 5 * time.Minute +) + +// CertsAPIRenewal creates new certificates using the certs API +type CertsAPIRenewal struct { + client certstype.CertificatesV1beta1Interface +} + +// NewCertsAPIRenawal takes a certificate pair to construct the Interface. +func NewCertsAPIRenawal(client kubernetes.Interface) Interface { + return &CertsAPIRenewal{ + client: client.CertificatesV1beta1(), + } +} + +// Renew takes a certificate using the cert and key. +func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) { + reqTmp := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + DNSNames: cfg.AltNames.DNSNames, + IPAddresses: cfg.AltNames.IPs, + } + + key, err := certutil.NewPrivateKey() + if err != nil { + return nil, nil, fmt.Errorf("Couldn't create new private key: %v", err) + } + + csr, err := x509.CreateCertificateRequest(rand.Reader, reqTmp, key) + if err != nil { + return nil, nil, fmt.Errorf("Couldn't create csr: %v", err) + } + + usages := make([]certsapi.KeyUsage, len(cfg.Usages)) + for i, usage := range cfg.Usages { + certsAPIUsage, ok := usageMap[usage] + if !ok { + return nil, nil, fmt.Errorf("unknown key usage %v", usage) + } + usages[i] = certsAPIUsage + } + + k8sCSR := &certsapi.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: certAPIPrefixName, + }, + Spec: certsapi.CertificateSigningRequestSpec{ + Request: csr, + Usages: usages, + }, + } + + req, err := r.client.CertificateSigningRequests().Create(k8sCSR) + if err != nil { + return nil, nil, fmt.Errorf("couldn't create certificate signing request: %v", err) + } + + watcher, err := r.client.CertificateSigningRequests().Watch(metav1.ListOptions{ + Watch: true, + FieldSelector: fields.Set{"metadata.name": req.Name}.String(), + }) + if err != nil { + return nil, nil, fmt.Errorf("couldn't watch for certificate creation: %v", err) + } + defer watcher.Stop() + + select { + case ev := <-watcher.ResultChan(): + if ev.Type != watch.Modified { + return nil, nil, fmt.Errorf("unexpected event receieved: %q", ev.Type) + } + case <-time.After(watchTimeout): + return nil, nil, errors.New("timeout trying to sign certificate") + } + + req, err = r.client.CertificateSigningRequests().Get(req.Name, metav1.GetOptions{}) + if len(req.Status.Conditions) < 1 { + return nil, nil, errors.New("certificate signing request has no statuses") + } + + // TODO: under what circumstances are there more than one? + if status := req.Status.Conditions[0].Type; status != certsapi.CertificateApproved { + return nil, nil, fmt.Errorf("Unexpected certificate status %v", status) + } + + cert, err := x509.ParseCertificate(req.Status.Certificate) + if err != nil { + return nil, nil, fmt.Errorf("couldn't parse issued certificate: %v", err) + } + + return cert, key, nil +} + +var usageMap = map[x509.ExtKeyUsage]certsapi.KeyUsage{ + x509.ExtKeyUsageAny: certsapi.UsageAny, + x509.ExtKeyUsageServerAuth: certsapi.UsageServerAuth, + x509.ExtKeyUsageClientAuth: certsapi.UsageClientAuth, + x509.ExtKeyUsageCodeSigning: certsapi.UsageCodeSigning, + x509.ExtKeyUsageEmailProtection: certsapi.UsageEmailProtection, + x509.ExtKeyUsageIPSECEndSystem: certsapi.UsageIPsecEndSystem, + x509.ExtKeyUsageIPSECTunnel: certsapi.UsageIPsecTunnel, + x509.ExtKeyUsageIPSECUser: certsapi.UsageIPsecUser, + x509.ExtKeyUsageTimeStamping: certsapi.UsageTimestamping, + x509.ExtKeyUsageOCSPSigning: certsapi.UsageOCSPSigning, + x509.ExtKeyUsageMicrosoftServerGatedCrypto: certsapi.UsageMicrosoftSGC, + x509.ExtKeyUsageNetscapeServerGatedCrypto: certsapi.UsageNetscapSGC, +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go b/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go new file mode 100644 index 00000000000..bdaa95a8d8b --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go @@ -0,0 +1,51 @@ +/* +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/rsa" + "crypto/x509" + "fmt" + + certutil "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" +) + +// FileRenewal renews a certificate using local certs +type FileRenewal struct { + caCert *x509.Certificate + caKey crypto.PrivateKey +} + +// NewFileRenewal takes a certificate pair to construct the Interface. +func NewFileRenewal(caCert *x509.Certificate, caKey crypto.PrivateKey) Interface { + return &FileRenewal{ + caCert: caCert, + caKey: caKey, + } +} + +// Renew takes a certificate using the cert and key +func (r *FileRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) { + caKey, ok := r.caKey.(*rsa.PrivateKey) + if !ok { + return nil, nil, fmt.Errorf("unsupported private key type %t", r.caKey) + } + + return pkiutil.NewCertAndKey(r.caCert, caKey, cfg) +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go b/cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go new file mode 100644 index 00000000000..3c8a9e58f35 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go @@ -0,0 +1,61 @@ +/* +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" + "testing" + + certutil "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" +) + +func TestFileRenew(t *testing.T) { + caCertCfg := &certutil.Config{CommonName: "kubernetes"} + caCert, caKey, err := certs.NewCACertAndKey(caCertCfg) + if err != nil { + t.Fatalf("couldn't create CA: %v", err) + } + + fr := NewFileRenewal(caCert, caKey) + + certCfg := &certutil.Config{ + CommonName: "test-certs", + AltNames: certutil.AltNames{ + DNSNames: []string{"test-domain.space"}, + }, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + cert, _, err := fr.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) + } + +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/interface.go b/cmd/kubeadm/app/phases/certs/renewal/interface.go new file mode 100644 index 00000000000..3ab3d2770df --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/interface.go @@ -0,0 +1,29 @@ +/* +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.PrivateKey, error) +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go new file mode 100644 index 00000000000..631e6edb178 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go @@ -0,0 +1,133 @@ +/* +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/rsa" + "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" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" +) + +func TestRenewImplementations(t *testing.T) { + caCertCfg := &certutil.Config{CommonName: "kubernetes"} + caCert, caKey, err := certs.NewCACertAndKey(caCertCfg) + if err != nil { + t.Fatalf("couldn't create CA: %v", err) + } + + client := &fakecerts.FakeCertificatesV1beta1{ + Fake: &k8stesting.Fake{}, + } + certReq := getCertReq(t, caCert, caKey) + client.AddReactor("get", "certificatesigningrequests", defaultReactionFunc(certReq)) + watcher := watch.NewFakeWithChanSize(1, false) + 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 *rsa.PrivateKey) *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{ + certsapi.CertificateSigningRequestCondition{ + Type: certsapi.CertificateApproved, + }, + }, + Certificate: cert.Raw, + }, + } +} From 7e3340361a15c1fe301a236c731bb55f59703e99 Mon Sep 17 00:00:00 2001 From: liz Date: Tue, 28 Aug 2018 17:49:31 -0400 Subject: [PATCH 2/5] Build artifacts --- cmd/kubeadm/app/cmd/options/BUILD | 1 + cmd/kubeadm/app/cmd/phases/BUILD | 5 +- cmd/kubeadm/app/phases/certs/BUILD | 1 + cmd/kubeadm/app/phases/certs/renewal/BUILD | 59 ++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 cmd/kubeadm/app/phases/certs/renewal/BUILD diff --git a/cmd/kubeadm/app/cmd/options/BUILD b/cmd/kubeadm/app/cmd/options/BUILD index 2751c03fe54..6218d105db8 100644 --- a/cmd/kubeadm/app/cmd/options/BUILD +++ b/cmd/kubeadm/app/cmd/options/BUILD @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ + "certs.go", "generic.go", "token.go", ], diff --git a/cmd/kubeadm/app/cmd/phases/BUILD b/cmd/kubeadm/app/cmd/phases/BUILD index 975954a6af0..636aeef99f0 100644 --- a/cmd/kubeadm/app/cmd/phases/BUILD +++ b/cmd/kubeadm/app/cmd/phases/BUILD @@ -93,7 +93,10 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//cmd/kubeadm/app/cmd/phases/certs:all-srcs", + ], tags = ["automanaged"], visibility = ["//visibility:public"], ) diff --git a/cmd/kubeadm/app/phases/certs/BUILD b/cmd/kubeadm/app/phases/certs/BUILD index 846ac4efad0..22772aaa459 100644 --- a/cmd/kubeadm/app/phases/certs/BUILD +++ b/cmd/kubeadm/app/phases/certs/BUILD @@ -52,6 +52,7 @@ filegroup( srcs = [ ":package-srcs", "//cmd/kubeadm/app/phases/certs/pkiutil:all-srcs", + "//cmd/kubeadm/app/phases/certs/renewal:all-srcs", ], tags = ["automanaged"], ) diff --git a/cmd/kubeadm/app/phases/certs/renewal/BUILD b/cmd/kubeadm/app/phases/certs/renewal/BUILD new file mode 100644 index 00000000000..172d78cc923 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/BUILD @@ -0,0 +1,59 @@ +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", + ], + importpath = "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal", + visibility = ["//visibility:public"], + deps = [ + "//cmd/kubeadm/app/phases/certs/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/apimachinery/pkg/fields:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/watch: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/util/cert:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "filerenewal_test.go", + "renewal_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//cmd/kubeadm/app/phases/certs: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", + "//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/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", + "//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1/fake:go_default_library", + "//staging/src/k8s.io/client-go/testing:go_default_library", + "//staging/src/k8s.io/client-go/util/cert:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) From ab28409da399711105a14b8d0ee8d19dcc189f09 Mon Sep 17 00:00:00 2001 From: liz Date: Tue, 28 Aug 2018 17:49:56 -0400 Subject: [PATCH 3/5] Mechanism for renewing a certificate based on an existing certificate --- cmd/kubeadm/app/cmd/options/certs.go | 24 ++++ cmd/kubeadm/app/cmd/phases/certs.go | 5 +- cmd/kubeadm/app/cmd/phases/certs/BUILD | 42 ++++++ cmd/kubeadm/app/cmd/phases/certs/renew.go | 124 ++++++++++++++++++ .../app/cmd/phases/certs/renewal_test.go | 66 ++++++++++ cmd/kubeadm/app/phases/certs/certs.go | 5 +- .../app/phases/certs/renewal/certsapi.go | 4 +- .../app/phases/certs/renewal/filerenewal.go | 15 +-- .../app/phases/certs/renewal/interface.go | 4 +- .../app/phases/certs/renewal/renewal.go | 55 ++++++++ .../app/phases/certs/renewal/renewal_test.go | 102 ++++++++++++++ 11 files changed, 427 insertions(+), 19 deletions(-) create mode 100644 cmd/kubeadm/app/cmd/options/certs.go create mode 100644 cmd/kubeadm/app/cmd/phases/certs/BUILD create mode 100644 cmd/kubeadm/app/cmd/phases/certs/renew.go create mode 100644 cmd/kubeadm/app/cmd/phases/certs/renewal_test.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/renewal.go diff --git a/cmd/kubeadm/app/cmd/options/certs.go b/cmd/kubeadm/app/cmd/options/certs.go new file mode 100644 index 00000000000..54adbf7fb9f --- /dev/null +++ b/cmd/kubeadm/app/cmd/options/certs.go @@ -0,0 +1,24 @@ +/* +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 options + +import "github.com/spf13/pflag" + +// AddCertificateDirFlag adds the --certs-dir flag to the given flagset +func AddCertificateDirFlag(fs *pflag.FlagSet, certsDir *string) { + fs.StringVar(certsDir, "cert-dir", *certsDir, "The path where to save the certificates") +} diff --git a/cmd/kubeadm/app/cmd/phases/certs.go b/cmd/kubeadm/app/cmd/phases/certs.go index 234862b0459..2fab5f098c1 100644 --- a/cmd/kubeadm/app/cmd/phases/certs.go +++ b/cmd/kubeadm/app/cmd/phases/certs.go @@ -25,6 +25,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" + "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" @@ -184,8 +185,8 @@ func getSANDescription(certSpec *certsphase.KubeadmCert) string { } func addFlags(cmd *cobra.Command, cfgPath *string, cfg *kubeadmapiv1alpha3.InitConfiguration, addAPIFlags bool) { - cmd.Flags().StringVar(cfgPath, "config", *cfgPath, "Path to kubeadm config file. WARNING: Usage of a configuration file is experimental") - cmd.Flags().StringVar(&cfg.CertificatesDir, "cert-dir", cfg.CertificatesDir, "The path where to save the certificates") + options.AddCertificateDirFlag(cmd.Flags(), &cfg.CertificatesDir) + options.AddKubeConfigFlag(cmd.Flags(), cfgPath) if addAPIFlags { cmd.Flags().StringVar(&cfg.Networking.DNSDomain, "service-dns-domain", cfg.Networking.DNSDomain, "Alternative domain for services, to use for the API server serving cert") cmd.Flags().StringVar(&cfg.Networking.ServiceSubnet, "service-cidr", cfg.Networking.ServiceSubnet, "Alternative range of IP address for service VIPs, from which derives the internal API server VIP that will be added to the API Server serving cert") diff --git a/cmd/kubeadm/app/cmd/phases/certs/BUILD b/cmd/kubeadm/app/cmd/phases/certs/BUILD new file mode 100644 index 00000000000..bf17b3eacc8 --- /dev/null +++ b/cmd/kubeadm/app/cmd/phases/certs/BUILD @@ -0,0 +1,42 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["renew.go"], + importpath = "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/certs", + visibility = ["//visibility:public"], + deps = [ + "//cmd/kubeadm/app/apis/kubeadm/scheme:go_default_library", + "//cmd/kubeadm/app/apis/kubeadm/v1alpha3:go_default_library", + "//cmd/kubeadm/app/cmd/options:go_default_library", + "//cmd/kubeadm/app/cmd/util: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/util:go_default_library", + "//cmd/kubeadm/app/util/config:go_default_library", + "//cmd/kubeadm/app/util/kubeconfig:go_default_library", + "//vendor/github.com/spf13/cobra:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["renewal_test.go"], + embed = [":go_default_library"], + deps = ["//vendor/github.com/spf13/cobra:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/kubeadm/app/cmd/phases/certs/renew.go b/cmd/kubeadm/app/cmd/phases/certs/renew.go new file mode 100644 index 00000000000..62891cbb6a0 --- /dev/null +++ b/cmd/kubeadm/app/cmd/phases/certs/renew.go @@ -0,0 +1,124 @@ +/* +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 renew + +import ( + "fmt" + + "github.com/spf13/cobra" + + kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" + kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" + "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" + kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + + "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" +) + +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)...) + + return cmd +} + +type renewConfig struct { + cfgPath string + kubeconfigPath string + cfg kubeadmapiv1alpha3.InitConfiguration + useAPI bool +} + +func getRenewSubCommands(cfg *renewConfig) []*cobra.Command { + // Default values for the cobra help text + kubeadmscheme.Scheme.Default(&cfg.cfg) + + certTree, err := certsphase.GetDefaultCertList().AsMap().CertTree() + kubeadmutil.CheckErr(err) + + cmdList := []*cobra.Command{} + + for caCert, certs := range certTree { + // Don't offer to renew CAs; would cause serious consequences + for _, cert := range certs { + cmdList = append(cmdList, makeCommandForRenew(cert, caCert, cfg)) + } + } + + 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) + cmd.Flags().BoolVar(&cfg.useAPI, "use-api", cfg.useAPI, "Use the kubernetes certificate API to renew certificates") +} + +func generateCertCommand(name, longName string) *cobra.Command { + return &cobra.Command{ + Use: name, + Short: fmt.Sprintf("Generates the %s", longName), + Long: "", // TODO EKF fill out + } +} + +func makeCommandForRenew(cert *certsphase.KubeadmCert, caCert *certsphase.KubeadmCert, cfg *renewConfig) *cobra.Command { + certCmd := generateCertCommand(cert.Name, cert.LongName) + 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) { + if cfg.useAPI { + kubeConfigPath := cmdutil.FindExistingKubeConfig(cfg.kubeconfigPath) + client, err := kubeconfigutil.ClientSetFromFile(kubeConfigPath) + if err != nil { + return nil, err + } + return renewal.NewCertsAPIRenawal(client), nil + } + + caCert, caKey, err := certsphase.LoadCertificateAuthority(cfg.cfg.CertificatesDir, caCertSpec.BaseName) + if err != nil { + return nil, err + } + + return renewal.NewFileRenewal(caCert, caKey), nil +} diff --git a/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go b/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go new file mode 100644 index 00000000000..95a1c3408d1 --- /dev/null +++ b/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go @@ -0,0 +1,66 @@ +/* +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 renew + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestCommandsGenerated(t *testing.T) { + expectedFlags := []string{ + "cert-dir", + "config", + "use-api", + } + + expectedCommands := []string{ + "renew", + + "renew apiserver", + "renew apiserver-kubelet-client", + "renew apiserver-etcd-client", + + "renew front-proxy-client", + + "renew etcd-server", + "renew etcd-peer", + "renew etcd-healthcheck-client", + } + + renewCmd := NewCmdCertsRenewal() + + fakeRoot := &cobra.Command{} + fakeRoot.AddCommand(renewCmd) + + for _, cmdPath := range expectedCommands { + t.Run(cmdPath, func(t *testing.T) { + cmd, rem, _ := fakeRoot.Find(strings.Split(cmdPath, " ")) + if cmd == nil || len(rem) != 0 { + t.Fatalf("couldn't locate command %q (%v)", cmdPath, rem) + } + + for _, flag := range expectedFlags { + if cmd.Flags().Lookup(flag) == nil { + t.Errorf("couldn't find expected flag --%s", flag) + } + } + }) + } +} diff --git a/cmd/kubeadm/app/phases/certs/certs.go b/cmd/kubeadm/app/phases/certs/certs.go index 511cba6179f..60ddf4d6b7b 100644 --- a/cmd/kubeadm/app/phases/certs/certs.go +++ b/cmd/kubeadm/app/phases/certs/certs.go @@ -137,7 +137,7 @@ func CreateCertAndKeyFilesWithCA(certSpec *KubeadmCert, caCertSpec *KubeadmCert, return fmt.Errorf("Expected CAname for %s to be %q, but was %s", certSpec.Name, certSpec.CAName, caCertSpec.Name) } - caCert, caKey, err := loadCertificateAuthority(cfg.CertificatesDir, caCertSpec.BaseName) + caCert, caKey, err := LoadCertificateAuthority(cfg.CertificatesDir, caCertSpec.BaseName) if err != nil { return fmt.Errorf("Couldn't load CA certificate %s: %v", caCertSpec.Name, err) } @@ -158,7 +158,8 @@ func newCertAndKeyFromSpec(certSpec *KubeadmCert, cfg *kubeadmapi.InitConfigurat return cert, key, err } -func loadCertificateAuthority(pkiDir string, baseName string) (*x509.Certificate, *rsa.PrivateKey, error) { +// LoadCertificateAuthority tries to load a CA in the given directory with the given name. +func LoadCertificateAuthority(pkiDir string, baseName string) (*x509.Certificate, *rsa.PrivateKey, error) { // Checks if certificate authority exists in the PKI directory if !pkiutil.CertOrKeyExist(pkiDir, baseName) { return nil, nil, fmt.Errorf("couldn't load %s certificate authority from %s", baseName, pkiDir) diff --git a/cmd/kubeadm/app/phases/certs/renewal/certsapi.go b/cmd/kubeadm/app/phases/certs/renewal/certsapi.go index d7efc1936e5..5298a96402e 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/certsapi.go +++ b/cmd/kubeadm/app/phases/certs/renewal/certsapi.go @@ -17,8 +17,8 @@ limitations under the License. package renewal import ( - "crypto" "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "errors" @@ -55,7 +55,7 @@ func NewCertsAPIRenawal(client kubernetes.Interface) Interface { } // Renew takes a certificate using the cert and key. -func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) { +func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, *rsa.PrivateKey, error) { reqTmp := &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: cfg.CommonName, diff --git a/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go b/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go index bdaa95a8d8b..71090d5cf7d 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go +++ b/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go @@ -17,10 +17,8 @@ limitations under the License. package renewal import ( - "crypto" "crypto/rsa" "crypto/x509" - "fmt" certutil "k8s.io/client-go/util/cert" "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" @@ -29,11 +27,11 @@ import ( // FileRenewal renews a certificate using local certs type FileRenewal struct { caCert *x509.Certificate - caKey crypto.PrivateKey + caKey *rsa.PrivateKey } // NewFileRenewal takes a certificate pair to construct the Interface. -func NewFileRenewal(caCert *x509.Certificate, caKey crypto.PrivateKey) Interface { +func NewFileRenewal(caCert *x509.Certificate, caKey *rsa.PrivateKey) Interface { return &FileRenewal{ caCert: caCert, caKey: caKey, @@ -41,11 +39,6 @@ func NewFileRenewal(caCert *x509.Certificate, caKey crypto.PrivateKey) Interface } // Renew takes a certificate using the cert and key -func (r *FileRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) { - caKey, ok := r.caKey.(*rsa.PrivateKey) - if !ok { - return nil, nil, fmt.Errorf("unsupported private key type %t", r.caKey) - } - - return pkiutil.NewCertAndKey(r.caCert, caKey, cfg) +func (r *FileRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, *rsa.PrivateKey, error) { + return pkiutil.NewCertAndKey(r.caCert, r.caKey, cfg) } diff --git a/cmd/kubeadm/app/phases/certs/renewal/interface.go b/cmd/kubeadm/app/phases/certs/renewal/interface.go index 3ab3d2770df..fd11e68004c 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/interface.go +++ b/cmd/kubeadm/app/phases/certs/renewal/interface.go @@ -17,7 +17,7 @@ limitations under the License. package renewal import ( - "crypto" + "crypto/rsa" "crypto/x509" certutil "k8s.io/client-go/util/cert" @@ -25,5 +25,5 @@ import ( // Interface represents a standard way to renew a certificate. type Interface interface { - Renew(*certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) + Renew(*certutil.Config) (*x509.Certificate, *rsa.PrivateKey, error) } diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal.go b/cmd/kubeadm/app/phases/certs/renewal/renewal.go new file mode 100644 index 00000000000..f5b2c75465a --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/renewal.go @@ -0,0 +1,55 @@ +/* +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" + "fmt" + + certutil "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" +) + +func RenewExistingCert(certsDir, baseName string, impl Interface) error { + cert, err := pkiutil.TryLoadCertFromDisk(certsDir, baseName) + if err != nil { + return fmt.Errorf("failed to load existing certificate %s: %v", baseName, err) + } + + cfg := certToConfig(cert) + newCert, newKey, err := impl.Renew(cfg) + if err != nil { + return fmt.Errorf("failed to renew certificate %s: %v", baseName, err) + } + + if err := pkiutil.WriteCertAndKey(certsDir, baseName, newCert, newKey); err != nil { + return fmt.Errorf("failed to write new certificate %s: %v", baseName, err) + } + 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, + } +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go index 631e6edb178..565c36d8760 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go +++ b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go @@ -19,6 +19,9 @@ package renewal import ( "crypto/rsa" "crypto/x509" + "crypto/x509/pkix" + "net" + "os" "testing" "time" @@ -31,6 +34,8 @@ import ( certutil "k8s.io/client-go/util/cert" "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" + testutil "k8s.io/kubernetes/cmd/kubeadm/test" + certtestutil "k8s.io/kubernetes/cmd/kubeadm/test/certs" ) func TestRenewImplementations(t *testing.T) { @@ -131,3 +136,100 @@ func getCertReq(t *testing.T, caCert *x509.Certificate, caKey *rsa.PrivateKey) * }, } } + +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) { + 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 := certs.NewCACertAndKey(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") + } + + renewer := NewFileRenewal(caCert, caKey) + + if err := RenewExistingCert(dir, "server", renewer); err != nil { + t.Fatalf("couldn't renew certificate: %v", err) + } + + newCert, err := pkiutil.TryLoadCertFromDisk(dir, "server") + if err != nil { + t.Fatalf("couldn't load created certificate: %v", err) + } + + if newCert.SerialNumber.Cmp(cert.SerialNumber) == 0 { + t.Fatal("expected new certificate, but renewed certificate has same serial number") + } + + 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...) +} From 76be5ca581818227062f7d0638cb772bb862bdb9 Mon Sep 17 00:00:00 2001 From: liz Date: Wed, 29 Aug 2018 17:57:02 -0400 Subject: [PATCH 4/5] Actually renew certificates (using on-disk CAs) --- cmd/kubeadm/app/cmd/phases/certs/BUILD | 9 +- cmd/kubeadm/app/cmd/phases/certs/renew.go | 37 ++--- .../app/cmd/phases/certs/renewal_test.go | 136 +++++++++++++++++- .../app/phases/certs/pkiutil/pki_helpers.go | 5 +- .../phases/certs/pkiutil/pki_helpers_test.go | 2 +- .../app/phases/certs/renewal/renewal.go | 9 +- .../src/k8s.io/client-go/util/cert/cert.go | 3 +- 7 files changed, 175 insertions(+), 26 deletions(-) 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 } From d21ed1a1f7675b31bb9f5c471df41d05dbf02ff1 Mon Sep 17 00:00:00 2001 From: liz Date: Thu, 30 Aug 2018 10:34:44 -0400 Subject: [PATCH 5/5] Add `renew all` command --- cmd/kubeadm/app/cmd/phases/certs.go | 2 +- cmd/kubeadm/app/cmd/phases/certs/BUILD | 1 + cmd/kubeadm/app/cmd/phases/certs/renew.go | 43 +++++- .../app/cmd/phases/certs/renewal_test.go | 122 +++++++++++------- cmd/kubeadm/app/phases/certs/renewal/BUILD | 1 + .../app/phases/certs/renewal/certsapi.go | 21 +-- .../app/phases/certs/renewal/renewal.go | 7 +- .../app/phases/certs/renewal/renewal_test.go | 2 +- 8 files changed, 129 insertions(+), 70 deletions(-) diff --git a/cmd/kubeadm/app/cmd/phases/certs.go b/cmd/kubeadm/app/cmd/phases/certs.go index 2fab5f098c1..0838b37482b 100644 --- a/cmd/kubeadm/app/cmd/phases/certs.go +++ b/cmd/kubeadm/app/cmd/phases/certs.go @@ -186,7 +186,7 @@ func getSANDescription(certSpec *certsphase.KubeadmCert) string { func addFlags(cmd *cobra.Command, cfgPath *string, cfg *kubeadmapiv1alpha3.InitConfiguration, addAPIFlags bool) { options.AddCertificateDirFlag(cmd.Flags(), &cfg.CertificatesDir) - options.AddKubeConfigFlag(cmd.Flags(), cfgPath) + options.AddConfigFlag(cmd.Flags(), cfgPath) if addAPIFlags { cmd.Flags().StringVar(&cfg.Networking.DNSDomain, "service-dns-domain", cfg.Networking.DNSDomain, "Alternative domain for services, to use for the API server serving cert") cmd.Flags().StringVar(&cfg.Networking.ServiceSubnet, "service-cidr", cfg.Networking.ServiceSubnet, "Alternative range of IP address for service VIPs, from which derives the internal API server VIP that will be added to the API Server serving cert") diff --git a/cmd/kubeadm/app/cmd/phases/certs/BUILD b/cmd/kubeadm/app/cmd/phases/certs/BUILD index b0ab2077b81..d75fdddeb09 100644 --- a/cmd/kubeadm/app/cmd/phases/certs/BUILD +++ b/cmd/kubeadm/app/cmd/phases/certs/BUILD @@ -16,6 +16,7 @@ go_library( "//cmd/kubeadm/app/util:go_default_library", "//cmd/kubeadm/app/util/config:go_default_library", "//cmd/kubeadm/app/util/kubeconfig:go_default_library", + "//pkg/util/normalizer:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", ], ) diff --git a/cmd/kubeadm/app/cmd/phases/certs/renew.go b/cmd/kubeadm/app/cmd/phases/certs/renew.go index e1ef98a09a3..9f56ef45f10 100644 --- a/cmd/kubeadm/app/cmd/phases/certs/renew.go +++ b/cmd/kubeadm/app/cmd/phases/certs/renew.go @@ -23,6 +23,7 @@ import ( kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" + "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" "k8s.io/kubernetes/cmd/kubeadm/app/constants" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" @@ -30,15 +31,28 @@ import ( kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" - - "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" + "k8s.io/kubernetes/pkg/util/normalizer" ) +var ( + genericLongDesc = normalizer.LongDesc(` + Renews the %[1]s, and saves them into %[2]s.cert and %[2]s.key files. + + Extra attributes such as SANs will be based on the existing certificates, there is no need to resupply them. +`) + allLongDesc = normalizer.LongDesc(` + Renews all known certificates necessary to run the control plan. Renewals are run unconditionally, regardless + of expiration date. Renewals can also be run individually for more control. +`) +) + +// NewCmdCertsRenewal creates a new `cert renew` command. func NewCmdCertsRenewal() *cobra.Command { cmd := &cobra.Command{ Use: "renew", - Short: "Renews all known certificates for kubeadm", - Long: "", // TODO EKF fill out + Short: "Renews certificates for a kubernetes cluster", + Long: cmdutil.MacroCommandLongDescription, + RunE: cmdutil.SubCmdRunE("renew"), } cmd.AddCommand(getRenewSubCommands()...) @@ -64,14 +78,31 @@ func getRenewSubCommands() []*cobra.Command { kubeadmutil.CheckErr(err) cmdList := []*cobra.Command{} + allCmds := []func() error{} for caCert, certs := range certTree { // Don't offer to renew CAs; would cause serious consequences for _, cert := range certs { - cmdList = append(cmdList, makeCommandForRenew(cert, caCert, cfg)) + cmd := makeCommandForRenew(cert, caCert, cfg) + cmdList = append(cmdList, cmd) + allCmds = append(allCmds, cmd.Execute) } } + allCmd := &cobra.Command{ + Use: "all", + Short: "renew all available certificates", + Long: allLongDesc, + Run: func(*cobra.Command, []string) { + for _, cmd := range allCmds { + err := cmd() + kubeadmutil.CheckErr(err) + } + }, + } + addFlags(allCmd, cfg) + + cmdList = append(cmdList, allCmd) return cmdList } @@ -87,7 +118,7 @@ func generateCertCommand(name, longName, baseName, caCertBaseName string, cfg *r return &cobra.Command{ Use: name, Short: fmt.Sprintf("Generates the %s", longName), - Long: "", // TODO EKF fill out + Long: fmt.Sprintf(genericLongDesc, longName, baseName), Run: func(cmd *cobra.Command, args []string) { internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(cfg.cfgPath, &cfg.cfg) kubeadmutil.CheckErr(err) diff --git a/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go b/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go index 217ff880e1b..81bc781b349 100644 --- a/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go +++ b/cmd/kubeadm/app/cmd/phases/certs/renewal_test.go @@ -45,8 +45,7 @@ func TestCommandsGenerated(t *testing.T) { } expectedCommands := []string{ - // TODO(EKF): add `renew all` - // "renew", + "renew all", "renew apiserver", "renew apiserver-kubelet-client", @@ -82,44 +81,61 @@ func TestCommandsGenerated(t *testing.T) { func TestRunRenewCommands(t *testing.T) { tests := []struct { - command string - baseName string - caBaseName string + command string + baseNames []string + caBaseNames []string }{ { - command: "apiserver", - baseName: kubeadmconstants.APIServerCertAndKeyBaseName, - caBaseName: kubeadmconstants.CACertAndKeyBaseName, + command: "all", + baseNames: []string{ + kubeadmconstants.APIServerCertAndKeyBaseName, + kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName, + kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName, + kubeadmconstants.FrontProxyClientCertAndKeyBaseName, + kubeadmconstants.EtcdServerCertAndKeyBaseName, + kubeadmconstants.EtcdPeerCertAndKeyBaseName, + kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName, + }, + caBaseNames: []string{ + kubeadmconstants.CACertAndKeyBaseName, + kubeadmconstants.FrontProxyCACertAndKeyBaseName, + kubeadmconstants.EtcdCACertAndKeyBaseName, + }, }, { - command: "apiserver-kubelet-client", - baseName: kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName, - caBaseName: kubeadmconstants.CACertAndKeyBaseName, + command: "apiserver", + baseNames: []string{kubeadmconstants.APIServerCertAndKeyBaseName}, + caBaseNames: []string{kubeadmconstants.CACertAndKeyBaseName}, }, { - command: "apiserver-etcd-client", - baseName: kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName, - caBaseName: kubeadmconstants.EtcdCACertAndKeyBaseName, + command: "apiserver-kubelet-client", + baseNames: []string{kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName}, + caBaseNames: []string{kubeadmconstants.CACertAndKeyBaseName}, }, { - command: "front-proxy-client", - baseName: kubeadmconstants.FrontProxyClientCertAndKeyBaseName, - caBaseName: kubeadmconstants.FrontProxyCACertAndKeyBaseName, + command: "apiserver-etcd-client", + baseNames: []string{kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName}, + caBaseNames: []string{kubeadmconstants.EtcdCACertAndKeyBaseName}, }, { - command: "etcd-server", - baseName: kubeadmconstants.EtcdServerCertAndKeyBaseName, - caBaseName: kubeadmconstants.EtcdCACertAndKeyBaseName, + command: "front-proxy-client", + baseNames: []string{kubeadmconstants.FrontProxyClientCertAndKeyBaseName}, + caBaseNames: []string{kubeadmconstants.FrontProxyCACertAndKeyBaseName}, }, { - command: "etcd-peer", - baseName: kubeadmconstants.EtcdPeerCertAndKeyBaseName, - caBaseName: kubeadmconstants.EtcdCACertAndKeyBaseName, + command: "etcd-server", + baseNames: []string{kubeadmconstants.EtcdServerCertAndKeyBaseName}, + caBaseNames: []string{kubeadmconstants.EtcdCACertAndKeyBaseName}, }, { - command: "etcd-healthcheck-client", - baseName: kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName, - caBaseName: kubeadmconstants.EtcdCACertAndKeyBaseName, + command: "etcd-peer", + baseNames: []string{kubeadmconstants.EtcdPeerCertAndKeyBaseName}, + caBaseNames: []string{kubeadmconstants.EtcdCACertAndKeyBaseName}, + }, + { + command: "etcd-healthcheck-client", + baseNames: []string{kubeadmconstants.EtcdHealthcheckClientCertAndKeyBaseName}, + caBaseNames: []string{kubeadmconstants.EtcdCACertAndKeyBaseName}, }, } @@ -132,8 +148,10 @@ func TestRunRenewCommands(t *testing.T) { 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) + for _, caBaseName := range test.caBaseNames { + if err := pkiutil.WriteCertAndKey(tmpDir, caBaseName, caCert, caKey); err != nil { + t.Fatalf("couldn't write out CA: %v", err) + } } certTmpl := x509.Certificate{ @@ -163,36 +181,40 @@ func TestRunRenewCommands(t *testing.T) { 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") + for _, baseName := range test.baseNames { + if err := pkiutil.WriteCertAndKey(tmpDir, 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) - } + for _, baseName := range test.baseNames { + newCert, newKey, err := pkiutil.TryLoadCertAndKeyFromDisk(tmpDir, baseName) + if err != nil { + t.Fatalf("couldn't load renewed certificate: %v", err) + } - certstestutil.AssertCertificateIsSignedByCa(t, newCert, caCert) + certstestutil.AssertCertificateIsSignedByCa(t, newCert, caCert) - pool := x509.NewCertPool() - pool.AddCert(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) - } + _, 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") + 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/renewal/BUILD b/cmd/kubeadm/app/phases/certs/renewal/BUILD index 172d78cc923..dfba2b89c4b 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/BUILD +++ b/cmd/kubeadm/app/phases/certs/renewal/BUILD @@ -19,6 +19,7 @@ go_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/util/cert:go_default_library", + "//vendor/github.com/pkg/errors:go_default_library", ], ) diff --git a/cmd/kubeadm/app/phases/certs/renewal/certsapi.go b/cmd/kubeadm/app/phases/certs/renewal/certsapi.go index 5298a96402e..f49ddf69ca6 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/certsapi.go +++ b/cmd/kubeadm/app/phases/certs/renewal/certsapi.go @@ -21,10 +21,11 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" - "errors" "fmt" "time" + "github.com/pkg/errors" + certsapi "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -47,7 +48,7 @@ type CertsAPIRenewal struct { client certstype.CertificatesV1beta1Interface } -// NewCertsAPIRenawal takes a certificate pair to construct the Interface. +// NewCertsAPIRenawal takes a Kubernetes interface and returns a renewal Interface. func NewCertsAPIRenawal(client kubernetes.Interface) Interface { return &CertsAPIRenewal{ client: client.CertificatesV1beta1(), @@ -67,19 +68,19 @@ func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, *rsa.P key, err := certutil.NewPrivateKey() if err != nil { - return nil, nil, fmt.Errorf("Couldn't create new private key: %v", err) + return nil, nil, errors.Wrap(err, "couldn't create new private key") } csr, err := x509.CreateCertificateRequest(rand.Reader, reqTmp, key) if err != nil { - return nil, nil, fmt.Errorf("Couldn't create csr: %v", err) + return nil, nil, errors.Wrap(err, "couldn't create certificate signing request") } usages := make([]certsapi.KeyUsage, len(cfg.Usages)) for i, usage := range cfg.Usages { certsAPIUsage, ok := usageMap[usage] if !ok { - return nil, nil, fmt.Errorf("unknown key usage %v", usage) + return nil, nil, fmt.Errorf("unknown key usage: %v", usage) } usages[i] = certsAPIUsage } @@ -96,7 +97,7 @@ func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, *rsa.P req, err := r.client.CertificateSigningRequests().Create(k8sCSR) if err != nil { - return nil, nil, fmt.Errorf("couldn't create certificate signing request: %v", err) + return nil, nil, errors.Wrap(err, "couldn't create certificate signing request") } watcher, err := r.client.CertificateSigningRequests().Watch(metav1.ListOptions{ @@ -104,14 +105,14 @@ func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, *rsa.P FieldSelector: fields.Set{"metadata.name": req.Name}.String(), }) if err != nil { - return nil, nil, fmt.Errorf("couldn't watch for certificate creation: %v", err) + return nil, nil, errors.Wrap(err, "couldn't watch for certificate creation") } defer watcher.Stop() select { case ev := <-watcher.ResultChan(): if ev.Type != watch.Modified { - return nil, nil, fmt.Errorf("unexpected event receieved: %q", ev.Type) + return nil, nil, fmt.Errorf("unexpected event received: %q", ev.Type) } case <-time.After(watchTimeout): return nil, nil, errors.New("timeout trying to sign certificate") @@ -124,12 +125,12 @@ func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, *rsa.P // TODO: under what circumstances are there more than one? if status := req.Status.Conditions[0].Type; status != certsapi.CertificateApproved { - return nil, nil, fmt.Errorf("Unexpected certificate status %v", status) + return nil, nil, fmt.Errorf("unexpected certificate status: %v", status) } cert, err := x509.ParseCertificate(req.Status.Certificate) if err != nil { - return nil, nil, fmt.Errorf("couldn't parse issued certificate: %v", err) + return nil, nil, errors.Wrap(err, "couldn't parse issued certificate") } return cert, key, nil diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal.go b/cmd/kubeadm/app/phases/certs/renewal/renewal.go index 3b934668845..e6eb7e2a434 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/renewal.go +++ b/cmd/kubeadm/app/phases/certs/renewal/renewal.go @@ -20,10 +20,13 @@ import ( "crypto/x509" "fmt" + "github.com/pkg/errors" certutil "k8s.io/client-go/util/cert" "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/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) @@ -38,11 +41,11 @@ func RenewExistingCert(certsDir, baseName string, impl Interface) error { cfg := certToConfig(certs[0]) newCert, newKey, err := impl.Renew(cfg) if err != nil { - return fmt.Errorf("failed to renew certificate %s: %v", baseName, err) + return errors.Wrapf(err, "failed to renew certificate %s", baseName) } if err := pkiutil.WriteCertAndKey(certsDir, baseName, newCert, newKey); err != nil { - return fmt.Errorf("failed to write new certificate %s: %v", baseName, err) + return errors.Wrapf(err, "failed to write new certificate %s", baseName) } return nil } diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go index 565c36d8760..bb31d10287c 100644 --- a/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go +++ b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go @@ -128,7 +128,7 @@ func getCertReq(t *testing.T, caCert *x509.Certificate, caKey *rsa.PrivateKey) * }, Status: certsapi.CertificateSigningRequestStatus{ Conditions: []certsapi.CertificateSigningRequestCondition{ - certsapi.CertificateSigningRequestCondition{ + { Type: certsapi.CertificateApproved, }, },