kubeadm alpha certs generate-csr

* Creates private keys and CSR files for all the control-plane certificates
* Helps with External CA mode of kubeadm

Signed-off-by: Richard Wall <richard.wall@jetstack.io>
This commit is contained in:
Richard Wall 2020-07-08 11:39:12 +01:00
parent 57712220a1
commit 81554ffdc0
3 changed files with 289 additions and 0 deletions

View File

@ -21,6 +21,7 @@ go_library(
"//cmd/kubeadm/app/cmd/util:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/features:go_default_library",
"//cmd/kubeadm/app/phases/certs:go_default_library",
"//cmd/kubeadm/app/phases/certs/renewal:go_default_library",
"//cmd/kubeadm/app/phases/copycerts:go_default_library",
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
@ -34,6 +35,7 @@ go_library(
"//vendor/github.com/lithammer/dedent:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
],
)
@ -59,6 +61,8 @@ go_test(
],
embed = [":go_default_library"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/phases/certs:go_default_library",
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
@ -68,5 +72,8 @@ go_test(
"//cmd/kubeadm/test/kubeconfig:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
],
)

View File

@ -24,6 +24,7 @@ import (
"github.com/lithammer/dedent"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/util/duration"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
@ -33,8 +34,10 @@ import (
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmconstants "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"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/copycerts"
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
)
@ -68,6 +71,22 @@ var (
You can also use "kubeadm init --upload-certs" without specifying a certificate key and it will
generate and print one for you.
`)
generateCSRLongDesc = cmdutil.LongDesc(`
Generates keys and certificate signing requests (CSRs) for all the certificates required to run the control plane.
This command also generates partial kubeconfig files with private key data in the "users > user > client-key-data" field,
and for each kubeconfig file an accompanying ".csr" file is created.
This command is designed for use in [Kubeadm External CA Mode](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-certs/#external-ca-mode).
It generates CSRs which you can then submit to your external certificate authority for signing.
The PEM encoded signed certificates should then be saved alongside the key files, using ".crt" as the file extension,
or in the case of kubeconfig files, the PEM encoded signed certificate should be base64 encoded
and added to the kubeconfig file in the "users > user > client-certificate-data" field.
`)
generateCSRExample = cmdutil.Examples(`
# The following command will generate keys and CSRs for all control-plane certificates and kubeconfig files:
kubeadm alpha certs generate-csr --kubeconfig-dir /tmp/etc-k8s --cert-dir /tmp/etc-k8s/pki
`)
)
@ -82,9 +101,83 @@ func newCmdCertsUtility(out io.Writer) *cobra.Command {
cmd.AddCommand(newCmdCertsRenewal(out))
cmd.AddCommand(newCmdCertsExpiration(out, constants.KubernetesDir))
cmd.AddCommand(NewCmdCertificateKey())
cmd.AddCommand(newCmdGenCSR())
return cmd
}
// genCSRConfig is the configuration required by the gencsr command
type genCSRConfig struct {
kubeadmConfigPath string
certDir string
kubeConfigDir string
kubeadmConfig *kubeadmapi.InitConfiguration
}
func newGenCSRConfig() *genCSRConfig {
return &genCSRConfig{
kubeConfigDir: kubeadmconstants.KubernetesDir,
}
}
func (o *genCSRConfig) addFlagSet(flagSet *pflag.FlagSet) {
options.AddConfigFlag(flagSet, &o.kubeadmConfigPath)
options.AddCertificateDirFlag(flagSet, &o.certDir)
options.AddKubeConfigDirFlag(flagSet, &o.kubeConfigDir)
}
// load merges command line flag values into kubeadm's config.
// Reads Kubeadm config from a file (if present)
// else use dynamically generated default config.
// This configuration contains the DNS names and IP addresses which
// are encoded in the control-plane CSRs.
func (o *genCSRConfig) load() (err error) {
o.kubeadmConfig, err = configutil.LoadOrDefaultInitConfiguration(
o.kubeadmConfigPath,
&kubeadmapiv1beta2.InitConfiguration{},
&kubeadmapiv1beta2.ClusterConfiguration{},
)
if err != nil {
return err
}
// --cert-dir takes priority over kubeadm config if set.
if o.certDir != "" {
o.kubeadmConfig.CertificatesDir = o.certDir
}
return nil
}
// newCmdGenCSR returns cobra.Command for generating keys and CSRs
func newCmdGenCSR() *cobra.Command {
config := newGenCSRConfig()
cmd := &cobra.Command{
Use: "generate-csr",
Short: "Generate keys and certificate signing requests",
Long: generateCSRLongDesc,
Example: generateCSRExample,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if err := config.load(); err != nil {
return err
}
return runGenCSR(config)
},
}
config.addFlagSet(cmd.Flags())
return cmd
}
// runGenCSR contains the logic of the generate-csr sub-command.
func runGenCSR(config *genCSRConfig) error {
if err := certsphase.CreateDefaultKeysAndCSRFiles(config.kubeadmConfig); err != nil {
return err
}
if err := kubeconfigphase.CreateDefaultKubeConfigsAndCSRFiles(config.kubeConfigDir, config.kubeadmConfig); err != nil {
return err
}
return nil
}
// NewCmdCertificateKey returns cobra.Command for certificate key generate
func NewCmdCertificateKey() *cobra.Command {
return &cobra.Command{

View File

@ -27,6 +27,13 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/client-go/tools/clientcmd"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
@ -285,3 +292,185 @@ func TestRenewUsingCSR(t *testing.T) {
t.Fatalf("couldn't load certificate %q: %v", cert.Name, err)
}
}
func TestRunGenCSR(t *testing.T) {
tmpDir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpDir)
kubeConfigDir := filepath.Join(tmpDir, "kubernetes")
certDir := kubeConfigDir + "/pki"
expectedCertificates := []string{
"apiserver",
"apiserver-etcd-client",
"apiserver-kubelet-client",
"front-proxy-client",
"etcd/healthcheck-client",
"etcd/peer",
"etcd/server",
}
expectedKubeConfigs := []string{
"admin",
"kubelet",
"controller-manager",
"scheduler",
}
config := genCSRConfig{
kubeConfigDir: kubeConfigDir,
kubeadmConfig: &kubeadmapi.InitConfiguration{
LocalAPIEndpoint: kubeadmapi.APIEndpoint{
AdvertiseAddress: "192.0.2.1",
BindPort: 443,
},
ClusterConfiguration: kubeadmapi.ClusterConfiguration{
Networking: kubeadmapi.Networking{
ServiceSubnet: "192.0.2.0/24",
},
CertificatesDir: certDir,
KubernetesVersion: "v1.19.0",
},
},
}
err := runGenCSR(&config)
require.NoError(t, err, "expected runGenCSR to not fail")
t.Log("The command generates key and CSR files in the configured --cert-dir")
for _, name := range expectedCertificates {
_, err = pkiutil.TryLoadKeyFromDisk(certDir, name)
assert.NoErrorf(t, err, "failed to load key file: %s", name)
_, err = pkiutil.TryLoadCSRFromDisk(certDir, name)
assert.NoError(t, err, "failed to load CSR file: %s", name)
}
t.Log("The command generates kubeconfig files in the configured --kubeconfig-dir")
for _, name := range expectedKubeConfigs {
_, err = clientcmd.LoadFromFile(kubeConfigDir + "/" + name + ".conf")
assert.NoErrorf(t, err, "failed to load kubeconfig file: %s", name)
_, err = pkiutil.TryLoadCSRFromDisk(kubeConfigDir, name+".conf")
assert.NoError(t, err, "failed to load kubeconfig CSR file: %s", name)
}
}
func TestGenCSRConfig(t *testing.T) {
type assertion func(*testing.T, *genCSRConfig)
hasCertDir := func(expected string) assertion {
return func(t *testing.T, config *genCSRConfig) {
assert.Equal(t, expected, config.kubeadmConfig.CertificatesDir)
}
}
hasKubeConfigDir := func(expected string) assertion {
return func(t *testing.T, config *genCSRConfig) {
assert.Equal(t, expected, config.kubeConfigDir)
}
}
hasAdvertiseAddress := func(expected string) assertion {
return func(t *testing.T, config *genCSRConfig) {
assert.Equal(t, expected, config.kubeadmConfig.LocalAPIEndpoint.AdvertiseAddress)
}
}
// A minimal kubeadm config with just enough values to avoid triggering
// auto-detection of config values at runtime.
const kubeadmConfig = `
apiVersion: kubeadm.k8s.io/v1beta2
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 192.0.2.1
nodeRegistration:
criSocket: /path/to/dockershim.sock
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
certificatesDir: /custom/config/certificates-dir
kubernetesVersion: v1.19.0
`
tmpDir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpDir)
customConfigPath := tmpDir + "/kubeadm.conf"
f, err := os.Create(customConfigPath)
require.NoError(t, err)
_, err = f.Write([]byte(kubeadmConfig))
require.NoError(t, err)
tests := []struct {
name string
flags []string
assertions []assertion
expectErr bool
}{
{
name: "default",
assertions: []assertion{
hasCertDir(kubeadmapiv1beta2.DefaultCertificatesDir),
hasKubeConfigDir(kubeadmconstants.KubernetesDir),
},
},
{
name: "--cert-dir overrides default",
flags: []string{"--cert-dir", "/foo/bar/pki"},
assertions: []assertion{
hasCertDir("/foo/bar/pki"),
},
},
{
name: "--config is loaded",
flags: []string{"--config", customConfigPath},
assertions: []assertion{
hasCertDir("/custom/config/certificates-dir"),
hasAdvertiseAddress("192.0.2.1"),
},
},
{
name: "--config not found",
flags: []string{"--config", "/does/not/exist"},
expectErr: true,
},
{
name: "--cert-dir overrides --config certificatesDir",
flags: []string{
"--config", customConfigPath,
"--cert-dir", "/foo/bar/pki",
},
assertions: []assertion{
hasCertDir("/foo/bar/pki"),
hasAdvertiseAddress("192.0.2.1"),
},
},
{
name: "--kubeconfig-dir overrides default",
flags: []string{
"--kubeconfig-dir", "/foo/bar/kubernetes",
},
assertions: []assertion{
hasKubeConfigDir("/foo/bar/kubernetes"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
flagset := pflag.NewFlagSet("flags-for-gencsr", pflag.ContinueOnError)
config := newGenCSRConfig()
config.addFlagSet(flagset)
require.NoError(t, flagset.Parse(test.flags))
err := config.load()
if test.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
for _, assertFunc := range test.assertions {
assertFunc(t, config)
}
})
}
}