Mechanism for renewing a certificate based on an existing certificate

This commit is contained in:
liz 2018-08-28 17:49:56 -04:00
parent 7e3340361a
commit ab28409da3
No known key found for this signature in database
GPG Key ID: 42D1F3A8C4A02586
11 changed files with 427 additions and 19 deletions

View File

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

View File

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

View File

@ -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"],
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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