Merge pull request #67910 from liztio/cert-renewal

Automatic merge from submit-queue (batch tested with PRs 64283, 67910, 67803, 68100). If you want to cherry-pick this change to another branch, please follow the instructions here: https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md.

Kubeadm Cert Renewal

**What this PR does / why we need it**:

adds explicit support for renewal of certificates via command

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes kubernetes/kubeadm#206

**Special notes for your reviewer**:
The targeted documentation is at kubernetes/website#9712

**Release note**:

```release-note
Adds the commands `kubeadm alpha phases renew <cert-name>`
```
This commit is contained in:
Kubernetes Submit Queue 2018-08-31 16:46:37 -07:00 committed by GitHub
commit 17dde46bae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1114 additions and 9 deletions

View File

@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"certs.go",
"generic.go", "generic.go",
"token.go", "token.go",
], ],

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

@ -93,7 +93,10 @@ filegroup(
filegroup( filegroup(
name = "all-srcs", name = "all-srcs",
srcs = [":package-srcs"], srcs = [
":package-srcs",
"//cmd/kubeadm/app/cmd/phases/certs:all-srcs",
],
tags = ["automanaged"], tags = ["automanaged"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )

View File

@ -25,6 +25,7 @@ import (
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" 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" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
@ -184,8 +185,8 @@ func getSANDescription(certSpec *certsphase.KubeadmCert) string {
} }
func addFlags(cmd *cobra.Command, cfgPath *string, cfg *kubeadmapiv1alpha3.InitConfiguration, addAPIFlags bool) { 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") options.AddCertificateDirFlag(cmd.Flags(), &cfg.CertificatesDir)
cmd.Flags().StringVar(&cfg.CertificatesDir, "cert-dir", cfg.CertificatesDir, "The path where to save the certificates") options.AddConfigFlag(cmd.Flags(), cfgPath)
if addAPIFlags { 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.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") 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,50 @@
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",
"//pkg/util/normalizer: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 = [
"//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(
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,156 @@
/*
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"
"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"
"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/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 certificates for a kubernetes cluster",
Long: cmdutil.MacroCommandLongDescription,
RunE: cmdutil.SubCmdRunE("renew"),
}
cmd.AddCommand(getRenewSubCommands()...)
return cmd
}
type renewConfig struct {
cfgPath string
kubeconfigPath string
cfg kubeadmapiv1alpha3.InitConfiguration
useAPI bool
}
func getRenewSubCommands() []*cobra.Command {
cfg := &renewConfig{
kubeconfigPath: constants.GetAdminKubeConfigPath(),
}
// Default values for the cobra help text
kubeadmscheme.Scheme.Default(&cfg.cfg)
certTree, err := certsphase.GetDefaultCertList().AsMap().CertTree()
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 {
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
}
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")
}
// 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: fmt.Sprintf(genericLongDesc, longName, baseName),
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, cert.BaseName, caCert.BaseName, cfg)
addFlags(certCmd, cfg)
return certCmd
}
func getRenewer(cfg *renewConfig, caCertBaseName string) (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, caCertBaseName)
if err != nil {
return nil, err
}
return renewal.NewFileRenewal(caCert, caKey), nil
}

View File

@ -0,0 +1,222 @@
/*
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 (
"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) {
expectedFlags := []string{
"cert-dir",
"config",
"use-api",
}
expectedCommands := []string{
"renew all",
"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)
}
}
})
}
}
func TestRunRenewCommands(t *testing.T) {
tests := []struct {
command string
baseNames []string
caBaseNames []string
}{
{
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",
baseNames: []string{kubeadmconstants.APIServerCertAndKeyBaseName},
caBaseNames: []string{kubeadmconstants.CACertAndKeyBaseName},
},
{
command: "apiserver-kubelet-client",
baseNames: []string{kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName},
caBaseNames: []string{kubeadmconstants.CACertAndKeyBaseName},
},
{
command: "apiserver-etcd-client",
baseNames: []string{kubeadmconstants.APIServerEtcdClientCertAndKeyBaseName},
caBaseNames: []string{kubeadmconstants.EtcdCACertAndKeyBaseName},
},
{
command: "front-proxy-client",
baseNames: []string{kubeadmconstants.FrontProxyClientCertAndKeyBaseName},
caBaseNames: []string{kubeadmconstants.FrontProxyCACertAndKeyBaseName},
},
{
command: "etcd-server",
baseNames: []string{kubeadmconstants.EtcdServerCertAndKeyBaseName},
caBaseNames: []string{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},
},
}
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)
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{
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)
}
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))
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)
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")
}
}
})
}
}

View File

@ -52,6 +52,7 @@ filegroup(
srcs = [ srcs = [
":package-srcs", ":package-srcs",
"//cmd/kubeadm/app/phases/certs/pkiutil:all-srcs", "//cmd/kubeadm/app/phases/certs/pkiutil:all-srcs",
"//cmd/kubeadm/app/phases/certs/renewal:all-srcs",
], ],
tags = ["automanaged"], tags = ["automanaged"],
) )

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) 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 { if err != nil {
return fmt.Errorf("Couldn't load CA certificate %s: %v", caCertSpec.Name, err) 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 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 // Checks if certificate authority exists in the PKI directory
if !pkiutil.CertOrKeyExist(pkiDir, baseName) { if !pkiutil.CertOrKeyExist(pkiDir, baseName) {
return nil, nil, fmt.Errorf("couldn't load %s certificate authority from %s", baseName, pkiDir) return nil, nil, fmt.Errorf("couldn't load %s certificate authority from %s", baseName, pkiDir)

View File

@ -130,7 +130,7 @@ func WritePublicKey(pkiPath, name string, key *rsa.PublicKey) error {
// CertOrKeyExist returns a boolean whether the cert or the key exists // CertOrKeyExist returns a boolean whether the cert or the key exists
func CertOrKeyExist(pkiPath, name string) bool { func CertOrKeyExist(pkiPath, name string) bool {
certificatePath, privateKeyPath := pathsForCertAndKey(pkiPath, name) certificatePath, privateKeyPath := PathsForCertAndKey(pkiPath, name)
_, certErr := os.Stat(certificatePath) _, certErr := os.Stat(certificatePath)
_, keyErr := os.Stat(privateKeyPath) _, keyErr := os.Stat(privateKeyPath)
@ -234,7 +234,8 @@ func TryLoadPrivatePublicKeyFromDisk(pkiPath, name string) (*rsa.PrivateKey, *rs
return k, p, nil 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) return pathForCert(pkiPath, name), pathForKey(pkiPath, name)
} }

View File

@ -405,7 +405,7 @@ func TestTryLoadKeyFromDisk(t *testing.T) {
} }
func TestPathsForCertAndKey(t *testing.T) { func TestPathsForCertAndKey(t *testing.T) {
crtPath, keyPath := pathsForCertAndKey("/foo", "bar") crtPath, keyPath := PathsForCertAndKey("/foo", "bar")
if crtPath != "/foo/bar.crt" { if crtPath != "/foo/bar.crt" {
t.Errorf("unexpected certificate path: %s", crtPath) t.Errorf("unexpected certificate path: %s", crtPath)
} }

View File

@ -0,0 +1,60 @@
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",
"//vendor/github.com/pkg/errors: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"],
)

View File

@ -0,0 +1,152 @@
/*
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/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"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"
"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 Kubernetes interface and returns a renewal 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, *rsa.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, errors.Wrap(err, "couldn't create new private key")
}
csr, err := x509.CreateCertificateRequest(rand.Reader, reqTmp, key)
if err != nil {
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)
}
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, errors.Wrap(err, "couldn't create certificate signing request")
}
watcher, err := r.client.CertificateSigningRequests().Watch(metav1.ListOptions{
Watch: true,
FieldSelector: fields.Set{"metadata.name": req.Name}.String(),
})
if err != nil {
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 received: %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, errors.Wrap(err, "couldn't parse issued certificate")
}
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,
}

View File

@ -0,0 +1,44 @@
/*
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"
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 *rsa.PrivateKey
}
// NewFileRenewal takes a certificate pair to construct the Interface.
func NewFileRenewal(caCert *x509.Certificate, caKey *rsa.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, *rsa.PrivateKey, error) {
return pkiutil.NewCertAndKey(r.caCert, r.caKey, cfg)
}

View File

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

View File

@ -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/rsa"
"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, *rsa.PrivateKey, error)
}

View File

@ -0,0 +1,63 @@
/*
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"
"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)
if err != nil {
return fmt.Errorf("failed to load existing certificate %s: %v", baseName, err)
}
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 errors.Wrapf(err, "failed to renew certificate %s", baseName)
}
if err := pkiutil.WriteCertAndKey(certsDir, baseName, newCert, newKey); err != nil {
return errors.Wrapf(err, "failed to write new certificate %s", baseName)
}
return nil
}
func certToConfig(cert *x509.Certificate) *certutil.Config {
return &certutil.Config{
CommonName: cert.Subject.CommonName,
Organization: cert.Subject.Organization,
AltNames: certutil.AltNames{
IPs: cert.IPAddresses,
DNSNames: cert.DNSNames,
},
Usages: cert.ExtKeyUsage,
}
}

View File

@ -0,0 +1,235 @@
/*
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"
"crypto/x509/pkix"
"net"
"os"
"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"
testutil "k8s.io/kubernetes/cmd/kubeadm/test"
certtestutil "k8s.io/kubernetes/cmd/kubeadm/test/certs"
)
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{
{
Type: certsapi.CertificateApproved,
},
},
Certificate: cert.Raw,
},
}
}
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...)
}

View File

@ -20,6 +20,7 @@ import (
"bytes" "bytes"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand"
cryptorand "crypto/rand" cryptorand "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "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 // 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) { 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 { if err != nil {
return nil, err return nil, err
} }