mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
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:
commit
17dde46bae
@ -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",
|
||||||
],
|
],
|
||||||
|
24
cmd/kubeadm/app/cmd/options/certs.go
Normal file
24
cmd/kubeadm/app/cmd/options/certs.go
Normal 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")
|
||||||
|
}
|
@ -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"],
|
||||||
)
|
)
|
||||||
|
@ -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")
|
||||||
|
50
cmd/kubeadm/app/cmd/phases/certs/BUILD
Normal file
50
cmd/kubeadm/app/cmd/phases/certs/BUILD
Normal 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"],
|
||||||
|
)
|
156
cmd/kubeadm/app/cmd/phases/certs/renew.go
Normal file
156
cmd/kubeadm/app/cmd/phases/certs/renew.go
Normal 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
|
||||||
|
}
|
222
cmd/kubeadm/app/cmd/phases/certs/renewal_test.go
Normal file
222
cmd/kubeadm/app/cmd/phases/certs/renewal_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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"],
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
60
cmd/kubeadm/app/phases/certs/renewal/BUILD
Normal file
60
cmd/kubeadm/app/phases/certs/renewal/BUILD
Normal 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"],
|
||||||
|
)
|
152
cmd/kubeadm/app/phases/certs/renewal/certsapi.go
Normal file
152
cmd/kubeadm/app/phases/certs/renewal/certsapi.go
Normal 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,
|
||||||
|
}
|
44
cmd/kubeadm/app/phases/certs/renewal/filerenewal.go
Normal file
44
cmd/kubeadm/app/phases/certs/renewal/filerenewal.go
Normal 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)
|
||||||
|
}
|
61
cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go
Normal file
61
cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
29
cmd/kubeadm/app/phases/certs/renewal/interface.go
Normal file
29
cmd/kubeadm/app/phases/certs/renewal/interface.go
Normal 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)
|
||||||
|
}
|
63
cmd/kubeadm/app/phases/certs/renewal/renewal.go
Normal file
63
cmd/kubeadm/app/phases/certs/renewal/renewal.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
235
cmd/kubeadm/app/phases/certs/renewal/renewal_test.go
Normal file
235
cmd/kubeadm/app/phases/certs/renewal/renewal_test.go
Normal 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...)
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user