diff --git a/cmd/kubeadm/app/apis/kubeadm/env.go b/cmd/kubeadm/app/apis/kubeadm/env.go index 127ef9c0299..5d89f645ecc 100644 --- a/cmd/kubeadm/app/apis/kubeadm/env.go +++ b/cmd/kubeadm/app/apis/kubeadm/env.go @@ -32,7 +32,6 @@ func SetEnvParams() *EnvParams { envParams := map[string]string{ "kubernetes_dir": "/etc/kubernetes", - "host_pki_path": "/etc/kubernetes/pki", "host_etcd_path": "/var/lib/etcd", "hyperkube_image": "", "repo_prefix": "gcr.io/google_containers", @@ -48,7 +47,6 @@ func SetEnvParams() *EnvParams { return &EnvParams{ KubernetesDir: path.Clean(envParams["kubernetes_dir"]), - HostPKIPath: path.Clean(envParams["host_pki_path"]), HostEtcdPath: path.Clean(envParams["host_etcd_path"]), HyperkubeImage: envParams["hyperkube_image"], RepositoryPrefix: envParams["repo_prefix"], diff --git a/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go b/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go index d10125ea94b..7a02ecbdcf3 100644 --- a/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go +++ b/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go @@ -34,6 +34,8 @@ func KubeadmFuzzerFuncs(t apitesting.TestingCommon) []interface{} { obj.Networking.DNSDomain = "foo" obj.AuthorizationMode = "foo" obj.Discovery.Token = &kubeadm.TokenDiscovery{} + obj.CertificatesDir = "foo" + obj.APIServerCertSANs = []string{} }, func(obj *kubeadm.NodeConfiguration, c fuzz.Continue) { c.FuzzNoCustom(obj) diff --git a/cmd/kubeadm/app/apis/kubeadm/types.go b/cmd/kubeadm/app/apis/kubeadm/types.go index cd6987c757d..333761048c3 100644 --- a/cmd/kubeadm/app/apis/kubeadm/types.go +++ b/cmd/kubeadm/app/apis/kubeadm/types.go @@ -22,7 +22,6 @@ import ( type EnvParams struct { KubernetesDir string - HostPKIPath string HostEtcdPath string HyperkubeImage string RepositoryPrefix string @@ -40,6 +39,7 @@ type MasterConfiguration struct { KubernetesVersion string CloudProvider string AuthorizationMode string + // SelfHosted enables an alpha deployment type where the apiserver, scheduler, and // controller manager are managed by Kubernetes itself. This option is likely to // become the default in the future. @@ -48,12 +48,18 @@ type MasterConfiguration struct { APIServerExtraArgs map[string]string ControllerManagerExtraArgs map[string]string SchedulerExtraArgs map[string]string + + // APIServerCertSANs sets extra Subject Alternative Names for the API Server signing cert + APIServerCertSANs []string + // CertificatesDir specifies where to store or look for all required certificates + CertificatesDir string } type API struct { + // AdvertiseAddress sets the address for the API server to advertise. AdvertiseAddress string - ExternalDNSNames []string - BindPort int32 + // BindPort sets the secure port for the API Server to bind to + BindPort int32 } type Discovery struct { diff --git a/cmd/kubeadm/app/apis/kubeadm/v1alpha1/defaults.go b/cmd/kubeadm/app/apis/kubeadm/v1alpha1/defaults.go index 2f067ff3aaf..e0266ec965b 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1alpha1/defaults.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1alpha1/defaults.go @@ -32,6 +32,7 @@ const ( DefaultDiscoveryBindPort = 9898 DefaultAuthorizationMode = "RBAC" DefaultCACertPath = "/etc/kubernetes/pki/ca.crt" + DefaultCertificatesDir = "/etc/kubernetes/pki" ) func addDefaultingFuncs(scheme *runtime.Scheme) error { @@ -66,6 +67,10 @@ func SetDefaults_MasterConfiguration(obj *MasterConfiguration) { if obj.AuthorizationMode == "" { obj.AuthorizationMode = DefaultAuthorizationMode } + + if obj.CertificatesDir == "" { + obj.CertificatesDir = DefaultCertificatesDir + } } func SetDefaults_NodeConfiguration(obj *NodeConfiguration) { diff --git a/cmd/kubeadm/app/apis/kubeadm/v1alpha1/types.go b/cmd/kubeadm/app/apis/kubeadm/v1alpha1/types.go index 7e4cc0f41bc..0c24d624c94 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1alpha1/types.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1alpha1/types.go @@ -30,6 +30,7 @@ type MasterConfiguration struct { KubernetesVersion string `json:"kubernetesVersion"` CloudProvider string `json:"cloudProvider"` AuthorizationMode string `json:"authorizationMode"` + // SelfHosted enables an alpha deployment type where the apiserver, scheduler, and // controller manager are managed by Kubernetes itself. This option is likely to // become the default in the future. @@ -38,13 +39,18 @@ type MasterConfiguration struct { APIServerExtraArgs map[string]string `json:"apiServerExtraArgs"` ControllerManagerExtraArgs map[string]string `json:"controllerManagerExtraArgs"` SchedulerExtraArgs map[string]string `json:"schedulerExtraArgs"` + + // APIServerCertSANs sets extra Subject Alternative Names for the API Server signing cert + APIServerCertSANs []string `json:"apiServerCertSANs"` + // CertificatesDir specifies where to store or look for all required certificates + CertificatesDir string `json:"certificatesDir"` } type API struct { - // The address for the API server to advertise. - AdvertiseAddress string `json:"advertiseAddress"` - ExternalDNSNames []string `json:"externalDNSNames"` - BindPort int32 `json:"bindPort"` + // AdvertiseAddress sets the address for the API server to advertise. + AdvertiseAddress string `json:"advertiseAddress"` + // BindPort sets the secure port for the API Server to bind to + BindPort int32 `json:"bindPort"` } type Discovery struct { diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/BUILD b/cmd/kubeadm/app/apis/kubeadm/validation/BUILD index 0c3f1130072..54d6e8184c1 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/BUILD +++ b/cmd/kubeadm/app/apis/kubeadm/validation/BUILD @@ -27,8 +27,10 @@ go_library( "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/util/token:go_default_library", + "//pkg/api/validation:go_default_library", "//pkg/kubeapiserver/authorizer/modes:go_default_library", "//pkg/registry/core/service/ipallocator:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/util/validation", "//vendor:k8s.io/apimachinery/pkg/util/validation/field", ], ) diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go index 8505a4de2b5..ad4d691cb5d 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go @@ -21,13 +21,15 @@ import ( "net" "net/url" "os" - "path" + "path/filepath" "strings" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" "k8s.io/kubernetes/cmd/kubeadm/app/constants" tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token" + apivalidation "k8s.io/kubernetes/pkg/api/validation" authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" "k8s.io/kubernetes/pkg/registry/core/service/ipallocator" ) @@ -49,9 +51,11 @@ var cloudproviders = []string{ func ValidateMasterConfiguration(c *kubeadm.MasterConfiguration) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, ValidateServiceSubnet(c.Networking.ServiceSubnet, field.NewPath("service subnet"))...) allErrs = append(allErrs, ValidateCloudProvider(c.CloudProvider, field.NewPath("cloudprovider"))...) allErrs = append(allErrs, ValidateAuthorizationMode(c.AuthorizationMode, field.NewPath("authorization-mode"))...) + allErrs = append(allErrs, ValidateNetworking(&c.Networking, field.NewPath("networking"))...) + allErrs = append(allErrs, ValidateAPIServerCertSANs(c.APIServerCertSANs, field.NewPath("cert-altnames"))...) + allErrs = append(allErrs, ValidateAbsolutePath(c.CertificatesDir, field.NewPath("certificates-dir"))...) return allErrs } @@ -59,7 +63,7 @@ func ValidateNodeConfiguration(c *kubeadm.NodeConfiguration) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, ValidateDiscovery(c, field.NewPath("discovery"))...) - if !path.IsAbs(c.CACertPath) || !strings.HasSuffix(c.CACertPath, ".crt") { + if !filepath.IsAbs(c.CACertPath) || !strings.HasSuffix(c.CACertPath, ".crt") { allErrs = append(allErrs, field.Invalid(field.NewPath("ca-cert-path"), c.CACertPath, "the ca certificate path must be an absolute path")) } return allErrs @@ -142,35 +146,75 @@ func ValidateToken(t string, fldPath *field.Path) field.ErrorList { id, secret, err := tokenutil.ParseToken(t) if err != nil { - allErrs = append(allErrs, field.Invalid(fldPath, nil, err.Error())) + allErrs = append(allErrs, field.Invalid(fldPath, t, err.Error())) } if len(id) == 0 || len(secret) == 0 { - allErrs = append(allErrs, field.Invalid(fldPath, nil, "token must be of form '[a-z0-9]{6}.[a-z0-9]{16}'")) + allErrs = append(allErrs, field.Invalid(fldPath, t, "token must be of form '[a-z0-9]{6}.[a-z0-9]{16}'")) } return allErrs } -func ValidateServiceSubnet(subnet string, fldPath *field.Path) field.ErrorList { +func ValidateAPIServerCertSANs(altnames []string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + for _, altname := range altnames { + if len(validation.IsDNS1123Subdomain(altname)) != 0 && net.ParseIP(altname) == nil { + allErrs = append(allErrs, field.Invalid(fldPath, altname, "altname is not a valid dns label or ip address")) + } + } + return allErrs +} + +func ValidateIPFromString(ipaddr string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if net.ParseIP(ipaddr) == nil { + allErrs = append(allErrs, field.Invalid(fldPath, ipaddr, "ip address is not valid")) + } + return allErrs +} + +func ValidateIPNetFromString(subnet string, minAddrs int64, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} _, svcSubnet, err := net.ParseCIDR(subnet) if err != nil { - return field.ErrorList{field.Invalid(fldPath, nil, "couldn't parse the service subnet")} + allErrs = append(allErrs, field.Invalid(fldPath, subnet, "couldn't parse subnet")) + return allErrs } numAddresses := ipallocator.RangeSize(svcSubnet) - if numAddresses < constants.MinimumAddressesInServiceSubnet { - return field.ErrorList{field.Invalid(fldPath, nil, "service subnet is too small")} + if numAddresses < minAddrs { + allErrs = append(allErrs, field.Invalid(fldPath, subnet, "subnet is too small")) } - return field.ErrorList{} + return allErrs +} + +func ValidateNetworking(c *kubeadm.Networking, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateDNS1123Subdomain(c.DNSDomain, field.NewPath("dns-domain"))...) + allErrs = append(allErrs, ValidateIPNetFromString(c.ServiceSubnet, constants.MinimumAddressesInServiceSubnet, field.NewPath("service-subnet"))...) + if len(c.PodSubnet) != 0 { + allErrs = append(allErrs, ValidateIPNetFromString(c.PodSubnet, constants.MinimumAddressesInServiceSubnet, field.NewPath("pod-subnet"))...) + } + return allErrs +} + +func ValidateAbsolutePath(path string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if !filepath.IsAbs(path) { + allErrs = append(allErrs, field.Invalid(fldPath, path, "path is not absolute")) + } + return allErrs } func ValidateCloudProvider(provider string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} if len(provider) == 0 { - return field.ErrorList{} + return allErrs } for _, supported := range cloudproviders { if provider == supported { - return field.ErrorList{} + return allErrs } } - return field.ErrorList{field.Invalid(fldPath, nil, "cloudprovider not supported")} + allErrs = append(allErrs, field.Invalid(fldPath, provider, "cloudprovider not supported")) + return allErrs } diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go index 916028cc293..4f4c5cb464b 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go @@ -69,31 +69,6 @@ func TestValidateAuthorizationMode(t *testing.T) { } } -func TestValidateServiceSubnet(t *testing.T) { - var tests = []struct { - s string - f *field.Path - expected bool - }{ - {"", nil, false}, - {"this is not a cidr", nil, false}, // not a CIDR - {"10.0.0.1", nil, false}, // not a CIDR - {"10.96.0.1/29", nil, false}, // CIDR too small, only 8 addresses and we require at least 10 - {"10.96.0.1/28", nil, true}, // a /28 subnet is ok because it can contain 16 addresses - {"10.96.0.1/12", nil, true}, // the default subnet should obviously pass as well - } - for _, rt := range tests { - actual := ValidateServiceSubnet(rt.s, rt.f) - if (len(actual) == 0) != rt.expected { - t.Errorf( - "failed ValidateServiceSubnet:\n\texpected: %t\n\t actual: %t", - rt.expected, - (len(actual) == 0), - ) - } - } -} - func TestValidateCloudProvider(t *testing.T) { var tests = []struct { s string @@ -118,6 +93,78 @@ func TestValidateCloudProvider(t *testing.T) { } } +func TestValidateAPIServerCertSANs(t *testing.T) { + var tests = []struct { + sans []string + expected bool + }{ + {[]string{}, true}, // ok if not provided + {[]string{"1,2,,3"}, false}, // not a DNS label or IP + {[]string{"my-hostname", "???&?.garbage"}, false}, // not valid + {[]string{"my-hostname", "my.subdomain", "1.2.3.4"}, true}, // supported + {[]string{"my-hostname2", "my.other.subdomain", "10.0.0.10"}, true}, // supported + } + for _, rt := range tests { + actual := ValidateAPIServerCertSANs(rt.sans, nil) + if (len(actual) == 0) != rt.expected { + t.Errorf( + "failed ValidateAPIServerCertSANs:\n\texpected: %t\n\t actual: %t", + rt.expected, + (len(actual) == 0), + ) + } + } +} + +func TestValidateIPFromString(t *testing.T) { + var tests = []struct { + ip string + expected bool + }{ + {"", false}, // not valid + {"1234", false}, // not valid + {"1.2", false}, // not valid + {"1.2.3.4/16", false}, // not valid + {"1.2.3.4", true}, // valid + {"16.0.1.1", true}, // valid + } + for _, rt := range tests { + actual := ValidateIPFromString(rt.ip, nil) + if (len(actual) == 0) != rt.expected { + t.Errorf( + "failed ValidateIPFromString:\n\texpected: %t\n\t actual: %t", + rt.expected, + (len(actual) == 0), + ) + } + } +} + +func TestValidateIPNetFromString(t *testing.T) { + var tests = []struct { + subnet string + minaddrs int64 + expected bool + }{ + {"", 0, false}, // not valid + {"1234", 0, false}, // not valid + {"abc", 0, false}, // not valid + {"1.2.3.4", 0, false}, // ip not valid + {"10.0.0.16/29", 10, false}, // valid, but too small. At least 10 addrs needed + {"10.0.0.16/12", 10, true}, // valid + } + for _, rt := range tests { + actual := ValidateIPNetFromString(rt.subnet, rt.minaddrs, nil) + if (len(actual) == 0) != rt.expected { + t.Errorf( + "failed ValidateIPNetFromString:\n\texpected: %t\n\t actual: %t", + rt.expected, + (len(actual) == 0), + ) + } + } +} + func TestValidateMasterConfiguration(t *testing.T) { var tests = []struct { s *kubeadm.MasterConfiguration @@ -131,7 +178,9 @@ func TestValidateMasterConfiguration(t *testing.T) { AuthorizationMode: "RBAC", Networking: kubeadm.Networking{ ServiceSubnet: "10.96.0.1/12", + DNSDomain: "cluster.local", }, + CertificatesDir: "/some/cert/dir", }, true}, {&kubeadm.MasterConfiguration{ Discovery: kubeadm.Discovery{ @@ -140,7 +189,9 @@ func TestValidateMasterConfiguration(t *testing.T) { AuthorizationMode: "RBAC", Networking: kubeadm.Networking{ ServiceSubnet: "10.96.0.1/12", + DNSDomain: "cluster.local", }, + CertificatesDir: "/some/other/cert/dir", }, true}, {&kubeadm.MasterConfiguration{ Discovery: kubeadm.Discovery{ @@ -153,7 +204,9 @@ func TestValidateMasterConfiguration(t *testing.T) { AuthorizationMode: "RBAC", Networking: kubeadm.Networking{ ServiceSubnet: "10.96.0.1/12", + DNSDomain: "cluster.local", }, + CertificatesDir: "/yet/another/cert/dir", }, true}, } for _, rt := range tests { diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index 22544c23a64..65edb68ec5f 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -88,10 +88,6 @@ func NewCmdInit(out io.Writer) *cobra.Command { &cfg.API.BindPort, "apiserver-bind-port", cfg.API.BindPort, "Port for the API Server to bind to", ) - cmd.PersistentFlags().StringSliceVar( - &cfg.API.ExternalDNSNames, "api-external-dns-names", cfg.API.ExternalDNSNames, - "The DNS names to advertise, in case you have configured them yourself", - ) cmd.PersistentFlags().StringVar( &cfg.Networking.ServiceSubnet, "service-cidr", cfg.Networking.ServiceSubnet, "Use alternative range of IP address for service VIPs", @@ -108,6 +104,14 @@ func NewCmdInit(out io.Writer) *cobra.Command { &cfg.KubernetesVersion, "kubernetes-version", cfg.KubernetesVersion, `Choose a specific Kubernetes version for the control plane`, ) + cmd.PersistentFlags().StringVar( + &cfg.CertificatesDir, "cert-dir", cfg.CertificatesDir, + `The path where to save and store the certificates`, + ) + cmd.PersistentFlags().StringSliceVar( + &cfg.APIServerCertSANs, "apiserver-cert-extra-sans", cfg.APIServerCertSANs, + `Optional extra altnames to use for the API Server serving cert. Can be both IP addresses and dns names.`, + ) cmd.PersistentFlags().StringVar(&cfgPath, "config", cfgPath, "Path to kubeadm config file") @@ -179,7 +183,7 @@ func (i *Init) Validate() error { func (i *Init) Run(out io.Writer) error { // PHASE 1: Generate certificates - err := certphase.CreatePKIAssets(i.cfg, kubeadmapi.GlobalEnvParams.HostPKIPath) + err := certphase.CreatePKIAssets(i.cfg) if err != nil { return err } @@ -190,7 +194,7 @@ func (i *Init) Run(out io.Writer) error { // so we'll pick the first one, there is much of chance to have an empty // slice by the time this gets called masterEndpoint := fmt.Sprintf("https://%s:%d", i.cfg.API.AdvertiseAddress, i.cfg.API.BindPort) - err = kubeconfigphase.CreateInitKubeConfigFiles(masterEndpoint, kubeadmapi.GlobalEnvParams.HostPKIPath, kubeadmapi.GlobalEnvParams.KubernetesDir) + err = kubeconfigphase.CreateInitKubeConfigFiles(masterEndpoint, i.cfg.CertificatesDir, kubeadmapi.GlobalEnvParams.KubernetesDir) if err != nil { return err } @@ -198,7 +202,7 @@ func (i *Init) Run(out io.Writer) error { // TODO: It's not great to have an exception for token here, but necessary because the apiserver doesn't handle this properly in the API yet // but relies on files on disk for now, which is daunting. if i.cfg.Discovery.Token != nil { - if err := tokenphase.CreateTokenAuthFile(tokenutil.BearerToken(i.cfg.Discovery.Token)); err != nil { + if err := tokenphase.CreateTokenAuthFile(i.cfg.CertificatesDir, tokenutil.BearerToken(i.cfg.Discovery.Token)); err != nil { return err } } diff --git a/cmd/kubeadm/app/cmd/phases/BUILD b/cmd/kubeadm/app/cmd/phases/BUILD index 21bdfd8e5d5..be440630264 100644 --- a/cmd/kubeadm/app/cmd/phases/BUILD +++ b/cmd/kubeadm/app/cmd/phases/BUILD @@ -10,15 +10,22 @@ load( go_library( name = "go_default_library", srcs = [ + "certs.go", "kubeconfig.go", "phase.go", ], tags = ["automanaged"], deps = [ - "//cmd/kubeadm/app/constants:go_default_library", + "//cmd/kubeadm/app/apis/kubeadm:go_default_library", + "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", + "//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library", + "//cmd/kubeadm/app/phases/certs:go_default_library", "//cmd/kubeadm/app/phases/kubeconfig:go_default_library", "//cmd/kubeadm/app/util:go_default_library", "//vendor:github.com/spf13/cobra", + "//vendor:k8s.io/apimachinery/pkg/util/net", + "//vendor:k8s.io/apimachinery/pkg/util/validation/field", + "//vendor:k8s.io/client-go/pkg/api", ], ) diff --git a/cmd/kubeadm/app/cmd/phases/certs.go b/cmd/kubeadm/app/cmd/phases/certs.go new file mode 100644 index 00000000000..b2b44d30034 --- /dev/null +++ b/cmd/kubeadm/app/cmd/phases/certs.go @@ -0,0 +1,99 @@ +/* +Copyright 2017 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 phases + +import ( + "fmt" + "net" + + "github.com/spf13/cobra" + + netutil "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/client-go/pkg/api" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation" + certphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" +) + +func NewCmdCerts() *cobra.Command { + cmd := &cobra.Command{ + Use: "certs", + Aliases: []string{"certificates"}, + Short: "Generate certificates for a Kubernetes cluster.", + RunE: subCmdRunE("certs"), + } + + cmd.AddCommand(NewCmdSelfSign()) + return cmd +} + +func NewCmdSelfSign() *cobra.Command { + // TODO: Move this into a dedicated Certificates Phase API object + cfg := &kubeadmapiext.MasterConfiguration{} + // Default values for the cobra help text + api.Scheme.Default(cfg) + + cmd := &cobra.Command{ + Use: "selfsign", + Short: "Generate the CA, APIServer signing/client cert, the ServiceAccount public/private keys and a CA and client cert for the front proxy", + Run: func(cmd *cobra.Command, args []string) { + + // Run the defaulting once again to take passed flags into account + api.Scheme.Default(cfg) + internalcfg := &kubeadmapi.MasterConfiguration{} + api.Scheme.Convert(cfg, internalcfg, nil) + + err := RunSelfSign(internalcfg) + kubeadmutil.CheckErr(err) + }, + } + cmd.Flags().StringVar(&cfg.Networking.DNSDomain, "dns-domain", cfg.Networking.DNSDomain, "The DNS Domain for the Kubernetes cluster.") + cmd.Flags().StringVar(&cfg.CertificatesDir, "cert-dir", cfg.CertificatesDir, "The path where to save and store the certificates") + cmd.Flags().StringVar(&cfg.Networking.ServiceSubnet, "service-cidr", cfg.Networking.ServiceSubnet, "The subnet for the Services in the cluster.") + cmd.Flags().StringSliceVar(&cfg.APIServerCertSANs, "cert-altnames", []string{}, "Optional extra altnames to use for the API Server serving cert. Can be both IP addresses and dns names.") + return cmd +} + +// RunSelfSign generates certificate assets in the specified directory +func RunSelfSign(config *kubeadmapi.MasterConfiguration) error { + if err := validateArgs(config); err != nil { + return fmt.Errorf("The argument validation failed: %v", err) + } + + // If it's possible to detect the default IP, add it to the SANs as well. Otherwise, just go with the provided ones + ip, err := netutil.ChooseBindAddress(net.ParseIP(config.API.AdvertiseAddress)) + if err == nil { + config.API.AdvertiseAddress = ip.String() + } + + err = certphase.CreatePKIAssets(config) + if err != nil { + return err + } + return nil +} + +func validateArgs(config *kubeadmapi.MasterConfiguration) error { + allErrs := field.ErrorList{} + allErrs = append(allErrs, validation.ValidateNetworking(&config.Networking, field.NewPath("networking"))...) + allErrs = append(allErrs, validation.ValidateAbsolutePath(config.CertificatesDir, field.NewPath("cert-dir"))...) + allErrs = append(allErrs, validation.ValidateAPIServerCertSANs(config.APIServerCertSANs, field.NewPath("cert-altnames"))...) + return allErrs.ToAggregate() +} diff --git a/cmd/kubeadm/app/cmd/phases/kubeconfig.go b/cmd/kubeadm/app/cmd/phases/kubeconfig.go index 47583e3fbed..998e4bfdc7b 100644 --- a/cmd/kubeadm/app/cmd/phases/kubeconfig.go +++ b/cmd/kubeadm/app/cmd/phases/kubeconfig.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" - kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" ) @@ -74,7 +74,7 @@ func NewCmdClientCerts(out io.Writer) *cobra.Command { } func addCommonFlags(cmd *cobra.Command, config *kubeconfigphase.BuildConfigProperties) { - cmd.Flags().StringVar(&config.CertDir, "cert-dir", kubeadmconstants.DefaultCertDir, "The path to the directory where the certificates are.") + cmd.Flags().StringVar(&config.CertDir, "cert-dir", kubeadmapiext.DefaultCertificatesDir, "The path to the directory where the certificates are.") cmd.Flags().StringVar(&config.ClientName, "client-name", "", "The name of the client for which the KubeConfig file will be generated.") cmd.Flags().StringVar(&config.APIServer, "server", "", "The location of the api server.") } diff --git a/cmd/kubeadm/app/cmd/phases/phase.go b/cmd/kubeadm/app/cmd/phases/phase.go index 2ef206a1a95..269ff8c5c2d 100644 --- a/cmd/kubeadm/app/cmd/phases/phase.go +++ b/cmd/kubeadm/app/cmd/phases/phase.go @@ -30,6 +30,7 @@ func NewCmdPhase(out io.Writer) *cobra.Command { RunE: subCmdRunE("phase"), } cmd.AddCommand(NewCmdKubeConfig(out)) + cmd.AddCommand(NewCmdCerts()) return cmd } diff --git a/cmd/kubeadm/app/cmd/reset.go b/cmd/kubeadm/app/cmd/reset.go index 3721af2f18b..27ca00e32cb 100644 --- a/cmd/kubeadm/app/cmd/reset.go +++ b/cmd/kubeadm/app/cmd/reset.go @@ -27,6 +27,7 @@ import ( "github.com/spf13/cobra" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/preflight" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" @@ -36,11 +37,12 @@ import ( // NewCmdReset returns the "kubeadm reset" command func NewCmdReset(out io.Writer) *cobra.Command { var skipPreFlight, removeNode bool + var certsDir string cmd := &cobra.Command{ Use: "reset", Short: "Run this to revert any changes made to this host by 'kubeadm init' or 'kubeadm join'.", Run: func(cmd *cobra.Command, args []string) { - r, err := NewReset(skipPreFlight, removeNode) + r, err := NewReset(skipPreFlight, removeNode, certsDir) kubeadmutil.CheckErr(err) kubeadmutil.CheckErr(r.Run(out)) }, @@ -56,14 +58,20 @@ func NewCmdReset(out io.Writer) *cobra.Command { "Remove this node from the pool of nodes in this cluster", ) + cmd.PersistentFlags().StringVar( + &certsDir, "cert-dir", kubeadmapiext.DefaultCertificatesDir, + "The path to the directory where the certificates are stored. If specified, clean this directory.", + ) + return cmd } type Reset struct { removeNode bool + certsDir string } -func NewReset(skipPreFlight, removeNode bool) (*Reset, error) { +func NewReset(skipPreFlight, removeNode bool, certsDir string) (*Reset, error) { if !skipPreFlight { fmt.Println("[preflight] Running pre-flight checks") @@ -76,6 +84,7 @@ func NewReset(skipPreFlight, removeNode bool) (*Reset, error) { return &Reset{ removeNode: removeNode, + certsDir: certsDir, }, nil } @@ -137,7 +146,7 @@ func (r *Reset) Run(out io.Writer) error { } // Remove contents from the config and pki directories - resetConfigDir(kubeadmapi.GlobalEnvParams.KubernetesDir, kubeadmapi.GlobalEnvParams.HostPKIPath) + resetConfigDir(kubeadmapi.GlobalEnvParams.KubernetesDir, r.certsDir) return nil } diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go index 6c6321e4219..56236f113c5 100644 --- a/cmd/kubeadm/app/constants/constants.go +++ b/cmd/kubeadm/app/constants/constants.go @@ -56,8 +56,6 @@ const ( ControllerManagerKubeConfigFileName = "controller-manager.conf" SchedulerKubeConfigFileName = "scheduler.conf" - DefaultCertDir = "/etc/kubernetes/pki" - // Important: a "v"-prefix shouldn't exist here; semver doesn't allow that MinimumControlPlaneVersion = "1.6.0-alpha.2" diff --git a/cmd/kubeadm/app/master/discovery.go b/cmd/kubeadm/app/master/discovery.go index 64886c3b3ee..5e932f7c4df 100644 --- a/cmd/kubeadm/app/master/discovery.go +++ b/cmd/kubeadm/app/master/discovery.go @@ -60,7 +60,7 @@ func encodeKubeDiscoverySecretData(dcfg *kubeadmapi.TokenDiscovery, apicfg kubea } func CreateDiscoveryDeploymentAndSecret(cfg *kubeadmapi.MasterConfiguration, client *clientset.Clientset) error { - caCertificatePath := path.Join(kubeadmapi.GlobalEnvParams.HostPKIPath, kubeadmconstants.CACertName) + caCertificatePath := path.Join(cfg.CertificatesDir, kubeadmconstants.CACertName) caCerts, err := certutil.CertsFromFile(caCertificatePath) if err != nil { return fmt.Errorf("couldn't load the CA certificate file %s: %v", caCertificatePath, err) diff --git a/cmd/kubeadm/app/master/manifests.go b/cmd/kubeadm/app/master/manifests.go index 1adf89d2a9c..5ef8464949c 100644 --- a/cmd/kubeadm/app/master/manifests.go +++ b/cmd/kubeadm/app/master/manifests.go @@ -292,10 +292,6 @@ func getComponentBaseCommand(component string) []string { return []string{"kube-" + component} } -func getCertFilePath(certName string) string { - return path.Join(kubeadmapi.GlobalEnvParams.HostPKIPath, certName) -} - func getAPIServerCommand(cfg *kubeadmapi.MasterConfiguration, selfHosted bool) []string { var command []string @@ -308,13 +304,13 @@ func getAPIServerCommand(cfg *kubeadmapi.MasterConfiguration, selfHosted bool) [ "insecure-port": "0", "admission-control": kubeadmconstants.DefaultAdmissionControl, "service-cluster-ip-range": cfg.Networking.ServiceSubnet, - "service-account-key-file": getCertFilePath(kubeadmconstants.ServiceAccountPublicKeyName), - "client-ca-file": getCertFilePath(kubeadmconstants.CACertName), - "tls-cert-file": getCertFilePath(kubeadmconstants.APIServerCertName), - "tls-private-key-file": getCertFilePath(kubeadmconstants.APIServerKeyName), - "kubelet-client-certificate": getCertFilePath(kubeadmconstants.APIServerKubeletClientCertName), - "kubelet-client-key": getCertFilePath(kubeadmconstants.APIServerKubeletClientKeyName), - "token-auth-file": path.Join(kubeadmapi.GlobalEnvParams.HostPKIPath, kubeadmconstants.CSVTokenFileName), + "service-account-key-file": path.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPublicKeyName), + "client-ca-file": path.Join(cfg.CertificatesDir, kubeadmconstants.CACertName), + "tls-cert-file": path.Join(cfg.CertificatesDir, kubeadmconstants.APIServerCertName), + "tls-private-key-file": path.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKeyName), + "kubelet-client-certificate": path.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKubeletClientCertName), + "kubelet-client-key": path.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKubeletClientKeyName), + "token-auth-file": path.Join(cfg.CertificatesDir, kubeadmconstants.CSVTokenFileName), "secure-port": fmt.Sprintf("%d", cfg.API.BindPort), "allow-privileged": "true", "storage-backend": "etcd3", @@ -324,7 +320,7 @@ func getAPIServerCommand(cfg *kubeadmapi.MasterConfiguration, selfHosted bool) [ "requestheader-username-headers": "X-Remote-User", "requestheader-group-headers": "X-Remote-Group", "requestheader-extra-headers-prefix": "X-Remote-Extra-", - "requestheader-client-ca-file": getCertFilePath(kubeadmconstants.FrontProxyCACertName), + "requestheader-client-ca-file": path.Join(cfg.CertificatesDir, kubeadmconstants.FrontProxyCACertName), "requestheader-allowed-names": "front-proxy-client", } @@ -379,10 +375,10 @@ func getControllerManagerCommand(cfg *kubeadmapi.MasterConfiguration, selfHosted "address": "127.0.0.1", "leader-elect": "true", "kubeconfig": path.Join(kubeadmapi.GlobalEnvParams.KubernetesDir, kubeadmconstants.ControllerManagerKubeConfigFileName), - "root-ca-file": getCertFilePath(kubeadmconstants.CACertName), - "service-account-private-key-file": getCertFilePath(kubeadmconstants.ServiceAccountPrivateKeyName), - "cluster-signing-cert-file": getCertFilePath(kubeadmconstants.CACertName), - "cluster-signing-key-file": getCertFilePath(kubeadmconstants.CAKeyName), + "root-ca-file": path.Join(cfg.CertificatesDir, kubeadmconstants.CACertName), + "service-account-private-key-file": path.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPrivateKeyName), + "cluster-signing-cert-file": path.Join(cfg.CertificatesDir, kubeadmconstants.CACertName), + "cluster-signing-key-file": path.Join(cfg.CertificatesDir, kubeadmconstants.CAKeyName), "insecure-experimental-approve-all-kubelet-csrs-for-group": kubeadmconstants.CSVTokenBootstrapGroup, "use-service-account-credentials": "true", } diff --git a/cmd/kubeadm/app/master/manifests_test.go b/cmd/kubeadm/app/master/manifests_test.go index 47ed0ec3a7e..33fc3bdc341 100644 --- a/cmd/kubeadm/app/master/manifests_test.go +++ b/cmd/kubeadm/app/master/manifests_test.go @@ -30,6 +30,8 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" ) +const testCertsDir = "/var/lib/certs" + func TestWriteStaticPodManifests(t *testing.T) { tmpdir, err := ioutil.TempDir("", "") if err != nil { @@ -380,21 +382,22 @@ func TestGetAPIServerCommand(t *testing.T) { }{ { cfg: &kubeadmapi.MasterConfiguration{ - API: kubeadm.API{BindPort: 123, AdvertiseAddress: "1.2.3.4"}, - Networking: kubeadm.Networking{ServiceSubnet: "bar"}, + API: kubeadm.API{BindPort: 123, AdvertiseAddress: "1.2.3.4"}, + Networking: kubeadm.Networking{ServiceSubnet: "bar"}, + CertificatesDir: testCertsDir, }, expected: []string{ "kube-apiserver", "--insecure-port=0", "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds", "--service-cluster-ip-range=bar", - "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/sa.pub", - "--client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--tls-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.crt", - "--tls-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", - "--kubelet-client-certificate=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-kubelet-client.crt", - "--kubelet-client-key=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-kubelet-client.key", - "--token-auth-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/tokens.csv", + "--service-account-key-file=" + testCertsDir + "/sa.pub", + "--client-ca-file=" + testCertsDir + "/ca.crt", + "--tls-cert-file=" + testCertsDir + "/apiserver.crt", + "--tls-private-key-file=" + testCertsDir + "/apiserver.key", + "--kubelet-client-certificate=" + testCertsDir + "/apiserver-kubelet-client.crt", + "--kubelet-client-key=" + testCertsDir + "/apiserver-kubelet-client.key", + "--token-auth-file=" + testCertsDir + "/tokens.csv", fmt.Sprintf("--secure-port=%d", 123), "--allow-privileged=true", "--storage-backend=etcd3", @@ -402,7 +405,7 @@ func TestGetAPIServerCommand(t *testing.T) { "--requestheader-username-headers=X-Remote-User", "--requestheader-group-headers=X-Remote-Group", "--requestheader-extra-headers-prefix=X-Remote-Extra-", - "--requestheader-client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/front-proxy-ca.crt", + "--requestheader-client-ca-file=" + testCertsDir + "/front-proxy-ca.crt", "--requestheader-allowed-names=front-proxy-client", "--authorization-mode=RBAC", "--advertise-address=1.2.3.4", @@ -411,21 +414,22 @@ func TestGetAPIServerCommand(t *testing.T) { }, { cfg: &kubeadmapi.MasterConfiguration{ - API: kubeadm.API{BindPort: 123, AdvertiseAddress: "4.3.2.1"}, - Networking: kubeadm.Networking{ServiceSubnet: "bar"}, + API: kubeadm.API{BindPort: 123, AdvertiseAddress: "4.3.2.1"}, + Networking: kubeadm.Networking{ServiceSubnet: "bar"}, + CertificatesDir: testCertsDir, }, expected: []string{ "kube-apiserver", "--insecure-port=0", "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds", "--service-cluster-ip-range=bar", - "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/sa.pub", - "--client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--tls-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.crt", - "--tls-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", - "--kubelet-client-certificate=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-kubelet-client.crt", - "--kubelet-client-key=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-kubelet-client.key", - "--token-auth-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/tokens.csv", + "--service-account-key-file=" + testCertsDir + "/sa.pub", + "--client-ca-file=" + testCertsDir + "/ca.crt", + "--tls-cert-file=" + testCertsDir + "/apiserver.crt", + "--tls-private-key-file=" + testCertsDir + "/apiserver.key", + "--kubelet-client-certificate=" + testCertsDir + "/apiserver-kubelet-client.crt", + "--kubelet-client-key=" + testCertsDir + "/apiserver-kubelet-client.key", + "--token-auth-file=" + testCertsDir + "/tokens.csv", fmt.Sprintf("--secure-port=%d", 123), "--allow-privileged=true", "--storage-backend=etcd3", @@ -433,7 +437,7 @@ func TestGetAPIServerCommand(t *testing.T) { "--requestheader-username-headers=X-Remote-User", "--requestheader-group-headers=X-Remote-Group", "--requestheader-extra-headers-prefix=X-Remote-Extra-", - "--requestheader-client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/front-proxy-ca.crt", + "--requestheader-client-ca-file=" + testCertsDir + "/front-proxy-ca.crt", "--requestheader-allowed-names=front-proxy-client", "--authorization-mode=RBAC", "--advertise-address=4.3.2.1", @@ -442,22 +446,23 @@ func TestGetAPIServerCommand(t *testing.T) { }, { cfg: &kubeadmapi.MasterConfiguration{ - API: kubeadm.API{BindPort: 123, AdvertiseAddress: "4.3.2.1"}, - Networking: kubeadm.Networking{ServiceSubnet: "bar"}, - Etcd: kubeadm.Etcd{CertFile: "fiz", KeyFile: "faz"}, + API: kubeadm.API{BindPort: 123, AdvertiseAddress: "4.3.2.1"}, + Networking: kubeadm.Networking{ServiceSubnet: "bar"}, + Etcd: kubeadm.Etcd{CertFile: "fiz", KeyFile: "faz"}, + CertificatesDir: testCertsDir, }, expected: []string{ "kube-apiserver", "--insecure-port=0", "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds", "--service-cluster-ip-range=bar", - "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/sa.pub", - "--client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--tls-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.crt", - "--tls-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", - "--kubelet-client-certificate=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-kubelet-client.crt", - "--kubelet-client-key=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-kubelet-client.key", - "--token-auth-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/tokens.csv", + "--service-account-key-file=" + testCertsDir + "/sa.pub", + "--client-ca-file=" + testCertsDir + "/ca.crt", + "--tls-cert-file=" + testCertsDir + "/apiserver.crt", + "--tls-private-key-file=" + testCertsDir + "/apiserver.key", + "--kubelet-client-certificate=" + testCertsDir + "/apiserver-kubelet-client.crt", + "--kubelet-client-key=" + testCertsDir + "/apiserver-kubelet-client.key", + "--token-auth-file=" + testCertsDir + "/tokens.csv", fmt.Sprintf("--secure-port=%d", 123), "--allow-privileged=true", "--storage-backend=etcd3", @@ -465,7 +470,7 @@ func TestGetAPIServerCommand(t *testing.T) { "--requestheader-username-headers=X-Remote-User", "--requestheader-group-headers=X-Remote-Group", "--requestheader-extra-headers-prefix=X-Remote-Extra-", - "--requestheader-client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/front-proxy-ca.crt", + "--requestheader-client-ca-file=" + testCertsDir + "/front-proxy-ca.crt", "--requestheader-allowed-names=front-proxy-client", "--authorization-mode=RBAC", "--advertise-address=4.3.2.1", @@ -492,47 +497,55 @@ func TestGetControllerManagerCommand(t *testing.T) { expected []string }{ { - cfg: &kubeadmapi.MasterConfiguration{}, + cfg: &kubeadmapi.MasterConfiguration{ + CertificatesDir: testCertsDir, + }, expected: []string{ "kube-controller-manager", "--address=127.0.0.1", "--leader-elect=true", "--kubeconfig=" + kubeadmapi.GlobalEnvParams.KubernetesDir + "/controller-manager.conf", - "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/sa.key", - "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.key", + "--root-ca-file=" + testCertsDir + "/ca.crt", + "--service-account-private-key-file=" + testCertsDir + "/sa.key", + "--cluster-signing-cert-file=" + testCertsDir + "/ca.crt", + "--cluster-signing-key-file=" + testCertsDir + "/ca.key", "--insecure-experimental-approve-all-kubelet-csrs-for-group=kubeadm:kubelet-bootstrap", "--use-service-account-credentials=true", }, }, { - cfg: &kubeadmapi.MasterConfiguration{CloudProvider: "foo"}, + cfg: &kubeadmapi.MasterConfiguration{ + CloudProvider: "foo", + CertificatesDir: testCertsDir, + }, expected: []string{ "kube-controller-manager", "--address=127.0.0.1", "--leader-elect=true", "--kubeconfig=" + kubeadmapi.GlobalEnvParams.KubernetesDir + "/controller-manager.conf", - "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/sa.key", - "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.key", + "--root-ca-file=" + testCertsDir + "/ca.crt", + "--service-account-private-key-file=" + testCertsDir + "/sa.key", + "--cluster-signing-cert-file=" + testCertsDir + "/ca.crt", + "--cluster-signing-key-file=" + testCertsDir + "/ca.key", "--insecure-experimental-approve-all-kubelet-csrs-for-group=kubeadm:kubelet-bootstrap", "--use-service-account-credentials=true", "--cloud-provider=foo", }, }, { - cfg: &kubeadmapi.MasterConfiguration{Networking: kubeadm.Networking{PodSubnet: "bar"}}, + cfg: &kubeadmapi.MasterConfiguration{ + Networking: kubeadm.Networking{PodSubnet: "bar"}, + CertificatesDir: testCertsDir, + }, expected: []string{ "kube-controller-manager", "--address=127.0.0.1", "--leader-elect=true", "--kubeconfig=" + kubeadmapi.GlobalEnvParams.KubernetesDir + "/controller-manager.conf", - "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/sa.key", - "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", - "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.key", + "--root-ca-file=" + testCertsDir + "/ca.crt", + "--service-account-private-key-file=" + testCertsDir + "/sa.key", + "--cluster-signing-cert-file=" + testCertsDir + "/ca.crt", + "--cluster-signing-key-file=" + testCertsDir + "/ca.key", "--insecure-experimental-approve-all-kubelet-csrs-for-group=kubeadm:kubelet-bootstrap", "--use-service-account-credentials=true", "--allocate-node-cidrs=true", diff --git a/cmd/kubeadm/app/phases/certs/BUILD b/cmd/kubeadm/app/phases/certs/BUILD index 2e0644385e3..f6b6346d358 100644 --- a/cmd/kubeadm/app/phases/certs/BUILD +++ b/cmd/kubeadm/app/phases/certs/BUILD @@ -32,6 +32,7 @@ go_library( "//cmd/kubeadm/app/phases/certs/pkiutil:go_default_library", "//pkg/registry/core/service/ipallocator:go_default_library", "//vendor:k8s.io/apimachinery/pkg/util/sets", + "//vendor:k8s.io/apimachinery/pkg/util/validation", "//vendor:k8s.io/client-go/util/cert", ], ) diff --git a/cmd/kubeadm/app/phases/certs/certs.go b/cmd/kubeadm/app/phases/certs/certs.go index d46b9e97fd3..a19f7e60452 100644 --- a/cmd/kubeadm/app/phases/certs/certs.go +++ b/cmd/kubeadm/app/phases/certs/certs.go @@ -24,6 +24,7 @@ import ( "os" setutil "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" certutil "k8s.io/client-go/util/cert" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" @@ -40,34 +41,22 @@ import ( // CreatePKIAssets will create and write to disk all PKI assets necessary to establish the control plane. // It generates a self-signed CA certificate and a server certificate (signed by the CA) -func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration, pkiDir string) error { - altNames := certutil.AltNames{} - - // First, define all domains this cert should be signed for - internalAPIServerFQDN := []string{ - "kubernetes", - "kubernetes.default", - "kubernetes.default.svc", - fmt.Sprintf("kubernetes.default.svc.%s", cfg.Networking.DNSDomain), - } +func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration) error { + pkiDir := cfg.CertificatesDir hostname, err := os.Hostname() if err != nil { return fmt.Errorf("couldn't get the hostname: %v", err) } - altNames.DNSNames = append(cfg.API.ExternalDNSNames, hostname) - altNames.DNSNames = append(altNames.DNSNames, internalAPIServerFQDN...) - // and lastly, extract the internal IP address for the API server - _, n, err := net.ParseCIDR(cfg.Networking.ServiceSubnet) + _, svcSubnet, err := net.ParseCIDR(cfg.Networking.ServiceSubnet) if err != nil { return fmt.Errorf("error parsing CIDR %q: %v", cfg.Networking.ServiceSubnet, err) } - internalAPIServerVirtualIP, err := ipallocator.GetIndexedIP(n, 1) - if err != nil { - return fmt.Errorf("unable to allocate IP address for the API server from the given CIDR (%q) [%v]", &cfg.Networking.ServiceSubnet, err) - } - altNames.IPs = append(altNames.IPs, internalAPIServerVirtualIP, net.ParseIP(cfg.API.AdvertiseAddress)) + // Build the list of SANs + altNames := getAltNames(cfg.APIServerCertSANs, hostname, cfg.Networking.DNSDomain, svcSubnet) + // Append the address the API Server is advertising + altNames.IPs = append(altNames.IPs, net.ParseIP(cfg.API.AdvertiseAddress)) var caCert *x509.Certificate var caKey *rsa.PrivateKey @@ -126,6 +115,7 @@ func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration, pkiDir string) error { return fmt.Errorf("failure while saving API server certificate and key [%v]", err) } fmt.Println("[certificates] Generated API server certificate and key.") + fmt.Printf("[certificates] API Server serving cert is signed for DNS names %v and IPs %v\n", altNames.DNSNames, altNames.IPs) } // If at least one of them exists, we should try to load them @@ -158,9 +148,9 @@ func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration, pkiDir string) error { } // If the key exists, we should try to load it - if pkiutil.CertOrKeyExist(pkiDir, kubeadmconstants.ServiceAccountPrivateKeyName) { + if pkiutil.CertOrKeyExist(pkiDir, kubeadmconstants.ServiceAccountKeyBaseName) { // Try to load sa.key from the PKI directory - _, err := pkiutil.TryLoadKeyFromDisk(pkiDir, kubeadmconstants.ServiceAccountPrivateKeyName) + _, err := pkiutil.TryLoadKeyFromDisk(pkiDir, kubeadmconstants.ServiceAccountKeyBaseName) if err != nil { return fmt.Errorf("certificate and/or key existed but they could not be loaded properly [%v]", err) } @@ -176,12 +166,11 @@ func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration, pkiDir string) error { if err = pkiutil.WriteKey(pkiDir, kubeadmconstants.ServiceAccountKeyBaseName, saTokenSigningKey); err != nil { return fmt.Errorf("failure while saving service account token signing key [%v]", err) } - fmt.Println("[certificates] Generated service account token signing key.") if err = pkiutil.WritePublicKey(pkiDir, kubeadmconstants.ServiceAccountKeyBaseName, &saTokenSigningKey.PublicKey); err != nil { return fmt.Errorf("failure while saving service account token signing public key [%v]", err) } - fmt.Println("[certificates] Generated service account token signing public key.") + fmt.Println("[certificates] Generated service account token signing key and public key.") } // front proxy CA and client certs are used to secure a front proxy authenticator which is used to assert identity @@ -254,7 +243,7 @@ func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration, pkiDir string) error { return nil } -// Verify that the cert is valid for all IPs and DNS names it should be valid for +// checkAltNamesExist verifies that the cert is valid for all IPs and DNS names it should be valid for func checkAltNamesExist(IPs []net.IP, DNSNames []string, altNames certutil.AltNames) bool { dnsset := setutil.NewString(DNSNames...) @@ -279,3 +268,33 @@ func checkAltNamesExist(IPs []net.IP, DNSNames []string, altNames certutil.AltNa } return true } + +// getAltNames builds an AltNames object for the certutil to use when generating the certificates +func getAltNames(cfgAltNames []string, hostname, dnsdomain string, svcSubnet *net.IPNet) certutil.AltNames { + altNames := certutil.AltNames{ + DNSNames: []string{ + hostname, + "kubernetes", + "kubernetes.default", + "kubernetes.default.svc", + fmt.Sprintf("kubernetes.default.svc.%s", dnsdomain), + }, + } + + // Populate IPs/DNSNames from AltNames + for _, altname := range cfgAltNames { + if ip := net.ParseIP(altname); ip != nil { + altNames.IPs = append(altNames.IPs, ip) + } else if len(validation.IsDNS1123Subdomain(altname)) == 0 { + altNames.DNSNames = append(altNames.DNSNames, altname) + } + } + + // and lastly, extract the internal IP address for the API server + internalAPIServerVirtualIP, err := ipallocator.GetIndexedIP(svcSubnet, 1) + if err != nil { + fmt.Printf("[certs] WARNING: Unable to get first IP address from the given CIDR (%s): %v\n", svcSubnet.String(), err) + } + altNames.IPs = append(altNames.IPs, internalAPIServerVirtualIP) + return altNames +} diff --git a/cmd/kubeadm/app/phases/certs/certs_test.go b/cmd/kubeadm/app/phases/certs/certs_test.go index b0c4b6b46f2..8f6bdbaed0e 100644 --- a/cmd/kubeadm/app/phases/certs/certs_test.go +++ b/cmd/kubeadm/app/phases/certs/certs_test.go @@ -45,29 +45,32 @@ func TestCreatePKIAssets(t *testing.T) { { // CIDR too small cfg: &kubeadmapi.MasterConfiguration{ - API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4"}, - Networking: kubeadmapi.Networking{ServiceSubnet: "10.0.0.1/1"}, + API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4"}, + Networking: kubeadmapi.Networking{ServiceSubnet: "10.0.0.1/1"}, + CertificatesDir: fmt.Sprintf("%s/etc/kubernetes/pki", tmpdir), }, expected: false, }, { // CIDR invalid cfg: &kubeadmapi.MasterConfiguration{ - API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4"}, - Networking: kubeadmapi.Networking{ServiceSubnet: "invalid"}, + API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4"}, + Networking: kubeadmapi.Networking{ServiceSubnet: "invalid"}, + CertificatesDir: fmt.Sprintf("%s/etc/kubernetes/pki", tmpdir), }, expected: false, }, { cfg: &kubeadmapi.MasterConfiguration{ - API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4"}, - Networking: kubeadmapi.Networking{ServiceSubnet: "10.0.0.1/24"}, + API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4"}, + Networking: kubeadmapi.Networking{ServiceSubnet: "10.0.0.1/24"}, + CertificatesDir: fmt.Sprintf("%s/etc/kubernetes/pki", tmpdir), }, expected: true, }, } for _, rt := range tests { - actual := CreatePKIAssets(rt.cfg, fmt.Sprintf("%s/etc/kubernetes/pki", tmpdir)) + actual := CreatePKIAssets(rt.cfg) if (actual == nil) != rt.expected { t.Errorf( "failed CreatePKIAssets with an error:\n\texpected: %t\n\t actual: %t", @@ -126,3 +129,46 @@ func TestCheckAltNamesExist(t *testing.T) { } } } + +func TestGetAltNames(t *testing.T) { + var tests = []struct { + cfgaltnames []string + hostname string + dnsdomain string + servicecidr string + expectedIPs []string + expectedDNSNames []string + }{ + { + cfgaltnames: []string{"foo", "192.168.200.1", "bar.baz"}, + hostname: "my-node", + dnsdomain: "cluster.external", + servicecidr: "10.96.0.1/12", + expectedIPs: []string{"192.168.200.1", "10.96.0.1"}, + expectedDNSNames: []string{"my-node", "kubernetes", "kubernetes.default", "kubernetes.default.svc", "kubernetes.default.svc.cluster.external", "foo", "bar.baz"}, + }, + } + + for _, rt := range tests { + _, svcSubnet, _ := net.ParseCIDR(rt.servicecidr) + actual := getAltNames(rt.cfgaltnames, rt.hostname, rt.dnsdomain, svcSubnet) + for i := range actual.IPs { + if rt.expectedIPs[i] != actual.IPs[i].String() { + t.Errorf( + "failed getAltNames:\n\texpected: %s\n\t actual: %s", + rt.expectedIPs[i], + actual.IPs[i].String(), + ) + } + } + for i := range actual.DNSNames { + if rt.expectedDNSNames[i] != actual.DNSNames[i] { + t.Errorf( + "failed getAltNames:\n\texpected: %s\n\t actual: %s", + rt.expectedDNSNames[i], + actual.DNSNames[i], + ) + } + } + } +} diff --git a/cmd/kubeadm/app/phases/certs/doc.go b/cmd/kubeadm/app/phases/certs/doc.go index a3ebb2881fe..baf35b138be 100644 --- a/cmd/kubeadm/app/phases/certs/doc.go +++ b/cmd/kubeadm/app/phases/certs/doc.go @@ -22,16 +22,25 @@ package certs INPUTS: From MasterConfiguration - .API.ExternalDNSNames is needed for knowing which DNS names the certs should be signed for + .API.AdvertiseAddress is an optional parameter that can be passed for an extra addition to the SAN IPs + .APIServerCertSANs is needed for knowing which DNS names and IPs the API Server serving cert should be valid for .Networking.DNSDomain is needed for knowing which DNS name the internal kubernetes service has .Networking.ServiceSubnet is needed for knowing which IP the internal kubernetes service is going to point to - The PKIPath is required for knowing where all certificates should be stored + .CertificatesDir is required for knowing where all certificates should be stored OUTPUTS: - Files to PKIPath (default /etc/kubernetes/pki): + Files to .CertificatesDir (default /etc/kubernetes/pki): - ca.crt - ca.key - apiserver.crt - apiserver.key + - apiserver-kubelet-client.crt + - apiserver-kubelet-client.key + - sa.pub + - sa.key + - front-proxy-ca.crt + - front-proxy-ca.key + - front-proxy-client.crt + - front-proxy-client.key */ diff --git a/cmd/kubeadm/app/phases/token/csv.go b/cmd/kubeadm/app/phases/token/csv.go index 914e6b79930..765f5ff4263 100644 --- a/cmd/kubeadm/app/phases/token/csv.go +++ b/cmd/kubeadm/app/phases/token/csv.go @@ -23,16 +23,15 @@ import ( "path" "k8s.io/apimachinery/pkg/util/uuid" - kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) // CreateTokenAuthFile creates the CSV file that can be used for allowing users with tokens access to the API Server -func CreateTokenAuthFile(bt string) error { - tokenAuthFilePath := path.Join(kubeadmapi.GlobalEnvParams.HostPKIPath, kubeadmconstants.CSVTokenFileName) - if err := os.MkdirAll(kubeadmapi.GlobalEnvParams.HostPKIPath, 0700); err != nil { - return fmt.Errorf("failed to create directory %q [%v]", kubeadmapi.GlobalEnvParams.HostPKIPath, err) +func CreateTokenAuthFile(certsDir, bt string) error { + tokenAuthFilePath := path.Join(certsDir, kubeadmconstants.CSVTokenFileName) + if err := os.MkdirAll(certsDir, 0700); err != nil { + return fmt.Errorf("failed to create directory %q [%v]", certsDir, err) } serialized := []byte(fmt.Sprintf("%s,%s,%s,%s\n", bt, kubeadmconstants.CSVTokenBootstrapUser, uuid.NewUUID(), kubeadmconstants.CSVTokenBootstrapGroup)) // DumpReaderToFile create a file with mode 0600 diff --git a/cmd/kubeadm/app/preflight/checks.go b/cmd/kubeadm/app/preflight/checks.go index ead55d590d7..101c520b872 100644 --- a/cmd/kubeadm/app/preflight/checks.go +++ b/cmd/kubeadm/app/preflight/checks.go @@ -540,7 +540,7 @@ func RunJoinNodeChecks(cfg *kubeadmapi.NodeConfiguration) error { PortOpenCheck{port: 10250}, DirAvailableCheck{Path: filepath.Join(kubeadmapi.GlobalEnvParams.KubernetesDir, "manifests")}, DirAvailableCheck{Path: "/var/lib/kubelet"}, - FileAvailableCheck{Path: filepath.Join(kubeadmapi.GlobalEnvParams.HostPKIPath, kubeadmconstants.CACertName)}, + FileAvailableCheck{Path: cfg.CACertPath}, FileAvailableCheck{Path: filepath.Join(kubeadmapi.GlobalEnvParams.KubernetesDir, kubeadmconstants.KubeletKubeConfigFileName)}, FileContentCheck{Path: bridgenf, Content: []byte{'1'}}, InPathCheck{executable: "ip", mandatory: true}, diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 492b748993e..8262ba9d85f 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -20,6 +20,7 @@ apiserver-advertise-address apiserver-arg-overrides apiserver-arg-overrides apiserver-bind-port +apiserver-cert-extra-sans apiserver-count apiserver-count apiserver-count @@ -67,6 +68,7 @@ build-only build-tag ca-cert-path cadvisor-port +cert-altnames cert-dir certificate-authority cgroup-driver @@ -158,6 +160,7 @@ discovery-file discovery-port discovery-token dns-bind-address +dns-domain dns-port dns-provider dns-provider-config