diff --git a/cmd/kubeadm/app/cmd/alpha/BUILD b/cmd/kubeadm/app/cmd/alpha/BUILD index 5236e8c767e..fd2bbb4b577 100644 --- a/cmd/kubeadm/app/cmd/alpha/BUILD +++ b/cmd/kubeadm/app/cmd/alpha/BUILD @@ -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", ], ) diff --git a/cmd/kubeadm/app/cmd/alpha/certs.go b/cmd/kubeadm/app/cmd/alpha/certs.go index 57eb92072cc..ae62ffb91ad 100644 --- a/cmd/kubeadm/app/cmd/alpha/certs.go +++ b/cmd/kubeadm/app/cmd/alpha/certs.go @@ -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{ diff --git a/cmd/kubeadm/app/cmd/alpha/certs_test.go b/cmd/kubeadm/app/cmd/alpha/certs_test.go index 67dc460bbec..2897cd0c40b 100644 --- a/cmd/kubeadm/app/cmd/alpha/certs_test.go +++ b/cmd/kubeadm/app/cmd/alpha/certs_test.go @@ -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) + } + }) + } +} diff --git a/cmd/kubeadm/app/cmd/phases/init/certs.go b/cmd/kubeadm/app/cmd/phases/init/certs.go index c7ea16044d3..4338ba5d40e 100644 --- a/cmd/kubeadm/app/cmd/phases/init/certs.go +++ b/cmd/kubeadm/app/cmd/phases/init/certs.go @@ -66,7 +66,9 @@ func NewCertsPhase() workflow.Phase { func localFlags() *pflag.FlagSet { set := pflag.NewFlagSet("csr", pflag.ExitOnError) options.AddCSRFlag(set, &csrOnly) + set.MarkDeprecated(options.CSROnly, "This flag will be removed in a future version. Please use kubeadm alpha certs generate-csr instead.") options.AddCSRDirFlag(set, &csrDir) + set.MarkDeprecated(options.CSRDir, "This flag will be removed in a future version. Please use kubeadm alpha certs generate-csr instead.") return set } diff --git a/cmd/kubeadm/app/phases/certs/certlist.go b/cmd/kubeadm/app/phases/certs/certlist.go index 4bbbd28d2ce..7b1a57c02fb 100644 --- a/cmd/kubeadm/app/phases/certs/certlist.go +++ b/cmd/kubeadm/app/phases/certs/certlist.go @@ -28,6 +28,11 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" ) +const ( + errInvalid = "invalid argument" + errExist = "file already exists" +) + type configMutatorsFunc func(*kubeadmapi.InitConfiguration, *pkiutil.CertConfig) error // KubeadmCert represents a certificate that Kubeadm will create to function properly. @@ -397,3 +402,63 @@ func setCommonNameToNodeName() configMutatorsFunc { return nil } } + +// leafCertificates returns non-CA certificates from the supplied Certificates. +func leafCertificates(c Certificates) (Certificates, error) { + certTree, err := c.AsMap().CertTree() + if err != nil { + return nil, err + } + + var out Certificates + for _, leafCertificates := range certTree { + out = append(out, leafCertificates...) + } + return out, nil +} + +func createKeyAndCSR(kubeadmConfig *kubeadmapi.InitConfiguration, cert *KubeadmCert) error { + if kubeadmConfig == nil { + return errors.Errorf("%s: kubeadmConfig was nil", errInvalid) + } + if cert == nil { + return errors.Errorf("%s: cert was nil", errInvalid) + } + certDir := kubeadmConfig.CertificatesDir + name := cert.BaseName + if pkiutil.CSROrKeyExist(certDir, name) { + return errors.Errorf("%s: key or CSR %s/%s", errExist, certDir, name) + } + cfg, err := cert.GetConfig(kubeadmConfig) + if err != nil { + return err + } + csr, key, err := pkiutil.NewCSRAndKey(cfg) + if err != nil { + return err + } + err = pkiutil.WriteKey(certDir, name, key) + if err != nil { + return err + } + err = pkiutil.WriteCSR(certDir, name, csr) + if err != nil { + return err + } + return nil +} + +// CreateDefaultKeysAndCSRFiles is used in ExternalCA mode to create key files +// and adjacent CSR files. +func CreateDefaultKeysAndCSRFiles(config *kubeadmapi.InitConfiguration) error { + certificates, err := leafCertificates(GetDefaultCertList()) + if err != nil { + return err + } + for _, cert := range certificates { + if err := createKeyAndCSR(config, cert); err != nil { + return err + } + } + return nil +} diff --git a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go index fd165e07357..dbc2aed5b16 100644 --- a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go +++ b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go @@ -26,6 +26,7 @@ import ( "path/filepath" "github.com/pkg/errors" + "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" certutil "k8s.io/client-go/util/cert" @@ -34,9 +35,13 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" - pkiutil "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" - kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + pkiutil "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" +) + +const ( + errInvalid = "invalid argument" + errExist = "file already exists" ) // clientCertAuth struct holds info required to build a client certificate to provide authentication info in a kubeconfig object @@ -103,7 +108,7 @@ func createKubeConfigFiles(outDir string, cfg *kubeadmapi.InitConfiguration, kub return err } - // writes the kubeconfig to disk if it not exists + // writes the kubeconfig to disk if it does not exist if err = createKubeConfigFileIfNotExists(outDir, kubeConfigFileName, config); err != nil { return err } @@ -113,57 +118,21 @@ func createKubeConfigFiles(outDir string, cfg *kubeadmapi.InitConfiguration, kub } // getKubeConfigSpecs returns all KubeConfigSpecs actualized to the context of the current InitConfiguration -// NB. this methods holds the information about how kubeadm creates kubeconfig files. +// NB. this method holds the information about how kubeadm creates kubeconfig files. func getKubeConfigSpecs(cfg *kubeadmapi.InitConfiguration) (map[string]*kubeConfigSpec, error) { - caCert, caKey, err := pkiutil.TryLoadCertAndKeyFromDisk(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName) if err != nil { return nil, errors.Wrap(err, "couldn't create a kubeconfig; the CA files couldn't be loaded") } - - controlPlaneEndpoint, err := kubeadmutil.GetControlPlaneEndpoint(cfg.ControlPlaneEndpoint, &cfg.LocalAPIEndpoint) + configs, err := getKubeConfigSpecsBase(cfg) if err != nil { return nil, err } - - var kubeConfigSpec = map[string]*kubeConfigSpec{ - kubeadmconstants.AdminKubeConfigFileName: { - CACert: caCert, - APIServer: controlPlaneEndpoint, - ClientName: "kubernetes-admin", - ClientCertAuth: &clientCertAuth{ - CAKey: caKey, - Organizations: []string{kubeadmconstants.SystemPrivilegedGroup}, - }, - }, - kubeadmconstants.KubeletKubeConfigFileName: { - CACert: caCert, - APIServer: controlPlaneEndpoint, - ClientName: fmt.Sprintf("%s%s", kubeadmconstants.NodesUserPrefix, cfg.NodeRegistration.Name), - ClientCertAuth: &clientCertAuth{ - CAKey: caKey, - Organizations: []string{kubeadmconstants.NodesGroup}, - }, - }, - kubeadmconstants.ControllerManagerKubeConfigFileName: { - CACert: caCert, - APIServer: controlPlaneEndpoint, - ClientName: kubeadmconstants.ControllerManagerUser, - ClientCertAuth: &clientCertAuth{ - CAKey: caKey, - }, - }, - kubeadmconstants.SchedulerKubeConfigFileName: { - CACert: caCert, - APIServer: controlPlaneEndpoint, - ClientName: kubeadmconstants.SchedulerUser, - ClientCertAuth: &clientCertAuth{ - CAKey: caKey, - }, - }, + for _, spec := range configs { + spec.CACert = caCert + spec.ClientCertAuth.CAKey = caKey } - - return kubeConfigSpec, nil + return configs, nil } // buildKubeConfigFromSpec creates a kubeconfig object for the given kubeConfigSpec @@ -182,13 +151,8 @@ func buildKubeConfigFromSpec(spec *kubeConfigSpec, clustername string) (*clientc } // otherwise, create a client certs - clientCertConfig := pkiutil.CertConfig{ - Config: certutil.Config{ - CommonName: spec.ClientName, - Organization: spec.ClientCertAuth.Organizations, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - }, - } + clientCertConfig := newClientCertConfigFromKubeConfigSpec(spec) + clientCert, clientKey, err := pkiutil.NewCertAndKey(spec.CACert, spec.ClientCertAuth.CAKey, &clientCertConfig) if err != nil { return nil, errors.Wrapf(err, "failure while creating %s client certificate", spec.ClientName) @@ -209,6 +173,16 @@ func buildKubeConfigFromSpec(spec *kubeConfigSpec, clustername string) (*clientc ), nil } +func newClientCertConfigFromKubeConfigSpec(spec *kubeConfigSpec) pkiutil.CertConfig { + return pkiutil.CertConfig{ + Config: certutil.Config{ + CommonName: spec.ClientName, + Organization: spec.ClientCertAuth.Organizations, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }, + } +} + // validateKubeConfig check if the kubeconfig file exist and has the expected CA and server URL func validateKubeConfig(outDir, filename string, config *clientcmdapi.Config) error { kubeConfigFilePath := filepath.Join(outDir, filename) @@ -386,3 +360,115 @@ func ValidateKubeconfigsForExternalCA(outDir string, cfg *kubeadmapi.InitConfigu } return nil } + +func getKubeConfigSpecsBase(cfg *kubeadmapi.InitConfiguration) (map[string]*kubeConfigSpec, error) { + apiServer, err := kubeadmutil.GetControlPlaneEndpoint(cfg.ControlPlaneEndpoint, &cfg.LocalAPIEndpoint) + if err != nil { + return nil, err + } + return map[string]*kubeConfigSpec{ + kubeadmconstants.AdminKubeConfigFileName: { + APIServer: apiServer, + ClientName: "kubernetes-admin", + ClientCertAuth: &clientCertAuth{ + Organizations: []string{kubeadmconstants.SystemPrivilegedGroup}, + }, + }, + kubeadmconstants.KubeletKubeConfigFileName: { + APIServer: apiServer, + ClientName: fmt.Sprintf("%s%s", kubeadmconstants.NodesUserPrefix, cfg.NodeRegistration.Name), + ClientCertAuth: &clientCertAuth{ + Organizations: []string{kubeadmconstants.NodesGroup}, + }, + }, + kubeadmconstants.ControllerManagerKubeConfigFileName: { + APIServer: apiServer, + ClientName: kubeadmconstants.ControllerManagerUser, + ClientCertAuth: &clientCertAuth{}, + }, + kubeadmconstants.SchedulerKubeConfigFileName: { + APIServer: apiServer, + ClientName: kubeadmconstants.SchedulerUser, + ClientCertAuth: &clientCertAuth{}, + }, + }, nil +} + +func createKubeConfigAndCSR(kubeConfigDir string, kubeadmConfig *kubeadmapi.InitConfiguration, name string, spec *kubeConfigSpec) error { + if kubeConfigDir == "" { + return errors.Errorf("%s: kubeConfigDir was empty", errInvalid) + } + if kubeadmConfig == nil { + return errors.Errorf("%s: kubeadmConfig was nil", errInvalid) + } + if name == "" { + return errors.Errorf("%s: name was empty", errInvalid) + } + if spec == nil { + return errors.Errorf("%s: spec was nil", errInvalid) + } + kubeConfigPath := filepath.Join(kubeConfigDir, name) + if _, err := os.Stat(kubeConfigPath); err == nil { + return errors.Errorf("%s: kube config: %s", errExist, kubeConfigPath) + } else if !os.IsNotExist(err) { + return errors.Wrapf(err, "unexpected error while checking if file exists: %s", kubeConfigPath) + } + if pkiutil.CSROrKeyExist(kubeConfigDir, name) { + return errors.Errorf("%s: csr: %s", errExist, kubeConfigPath) + } + + clientCertConfig := newClientCertConfigFromKubeConfigSpec(spec) + + clientKey, err := pkiutil.NewPrivateKey(clientCertConfig.PublicKeyAlgorithm) + if err != nil { + return err + } + clientCSR, err := pkiutil.NewCSR(clientCertConfig, clientKey) + if err != nil { + return err + } + + encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(clientKey) + if err != nil { + return err + } + + var ( + emptyCACert []byte + emptyClientCert []byte + ) + + // create a kubeconfig with the client certs + config := kubeconfigutil.CreateWithCerts( + spec.APIServer, + kubeadmConfig.ClusterName, + spec.ClientName, + emptyCACert, + encodedClientKey, + emptyClientCert, + ) + + if err := kubeconfigutil.WriteToDisk(kubeConfigPath, config); err != nil { + return err + } + // Write CSR to disk + if err := pkiutil.WriteCSR(kubeConfigDir, name, clientCSR); err != nil { + return err + } + return nil +} + +// CreateDefaultKubeConfigsAndCSRFiles is used in ExternalCA mode to create +// kubeconfig files and adjacent CSR files. +func CreateDefaultKubeConfigsAndCSRFiles(kubeConfigDir string, kubeadmConfig *kubeadmapi.InitConfiguration) error { + kubeConfigs, err := getKubeConfigSpecsBase(kubeadmConfig) + if err != nil { + return err + } + for name, spec := range kubeConfigs { + if err := createKubeConfigAndCSR(kubeConfigDir, kubeadmConfig, name, spec); err != nil { + return err + } + } + return nil +} diff --git a/cmd/kubeadm/app/util/pkiutil/pki_helpers.go b/cmd/kubeadm/app/util/pkiutil/pki_helpers.go index a762e522145..f5e78ded7c5 100644 --- a/cmd/kubeadm/app/util/pkiutil/pki_helpers.go +++ b/cmd/kubeadm/app/util/pkiutil/pki_helpers.go @@ -291,16 +291,14 @@ func TryLoadKeyFromDisk(pkiPath, name string) (crypto.Signer, error) { // TryLoadCSRAndKeyFromDisk tries to load the CSR and key from the disk func TryLoadCSRAndKeyFromDisk(pkiPath, name string) (*x509.CertificateRequest, crypto.Signer, error) { - csrPath := pathForCSR(pkiPath, name) - - csr, err := CertificateRequestFromFile(csrPath) + csr, err := TryLoadCSRFromDisk(pkiPath, name) if err != nil { - return nil, nil, errors.Wrapf(err, "couldn't load the certificate request %s", csrPath) + return nil, nil, errors.Wrap(err, "could not load CSR file") } key, err := TryLoadKeyFromDisk(pkiPath, name) if err != nil { - return nil, nil, errors.Wrap(err, "couldn't load key file") + return nil, nil, errors.Wrap(err, "could not load key file") } return csr, key, nil @@ -335,6 +333,18 @@ func TryLoadPrivatePublicKeyFromDisk(pkiPath, name string) (*rsa.PrivateKey, *rs return k, p, nil } +// TryLoadCSRFromDisk tries to load the CSR from the disk +func TryLoadCSRFromDisk(pkiPath, name string) (*x509.CertificateRequest, error) { + csrPath := pathForCSR(pkiPath, name) + + csr, err := CertificateRequestFromFile(csrPath) + if err != nil { + return nil, errors.Wrapf(err, "could not load the CSR %s", csrPath) + } + + return csr, nil +} + // 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)