diff --git a/cmd/kubeadm/app/BUILD b/cmd/kubeadm/app/BUILD index 97bf3871fe4..8f49b8be3a7 100644 --- a/cmd/kubeadm/app/BUILD +++ b/cmd/kubeadm/app/BUILD @@ -32,6 +32,7 @@ filegroup( ":package-srcs", "//cmd/kubeadm/app/apis/kubeadm:all-srcs", "//cmd/kubeadm/app/cmd:all-srcs", + "//cmd/kubeadm/app/constants:all-srcs", "//cmd/kubeadm/app/discovery:all-srcs", "//cmd/kubeadm/app/images:all-srcs", "//cmd/kubeadm/app/master:all-srcs", diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index f08c47b59a2..be3929bf01a 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -191,7 +191,7 @@ func (i *Init) Validate() error { func (i *Init) Run(out io.Writer) error { // PHASE 1: Generate certificates - caCert, err := certphase.CreatePKIAssets(i.cfg, kubeadmapi.GlobalEnvParams.HostPKIPath) + err := certphase.CreatePKIAssets(i.cfg, kubeadmapi.GlobalEnvParams.HostPKIPath) if err != nil { return err } @@ -249,7 +249,7 @@ func (i *Init) Run(out io.Writer) error { if i.cfg.Discovery.Token != nil { fmt.Printf("[token-discovery] Using token: %s\n", kubeadmutil.BearerToken(i.cfg.Discovery.Token)) - if err := kubemaster.CreateDiscoveryDeploymentAndSecret(i.cfg, client, caCert); err != nil { + if err := kubemaster.CreateDiscoveryDeploymentAndSecret(i.cfg, client); err != nil { return err } if err := kubeadmutil.UpdateOrCreateToken(client, i.cfg.Discovery.Token, kubeadmutil.DefaultTokenDuration); err != nil { diff --git a/cmd/kubeadm/app/constants/BUILD b/cmd/kubeadm/app/constants/BUILD new file mode 100644 index 00000000000..04ffa549252 --- /dev/null +++ b/cmd/kubeadm/app/constants/BUILD @@ -0,0 +1,27 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = ["constants.go"], + tags = ["automanaged"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go new file mode 100644 index 00000000000..6851df8129f --- /dev/null +++ b/cmd/kubeadm/app/constants/constants.go @@ -0,0 +1,27 @@ +/* +Copyright 2016 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 constants + +const ( + CACertAndKeyBaseName = "ca" + CACertName = "ca.crt" + CAKeyName = "ca.key" + + APIServerCertAndKeyBaseName = "apiserver" + APIServerCertName = "apiserver.crt" + APIServerKeyName = "apiserver.key" +) diff --git a/cmd/kubeadm/app/master/BUILD b/cmd/kubeadm/app/master/BUILD index af147376b46..a7d3a811da6 100644 --- a/cmd/kubeadm/app/master/BUILD +++ b/cmd/kubeadm/app/master/BUILD @@ -22,6 +22,7 @@ go_library( deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", + "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/images:go_default_library", "//cmd/kubeadm/app/phases/kubeconfig:go_default_library", "//cmd/kubeadm/app/util:go_default_library", diff --git a/cmd/kubeadm/app/master/apiclient.go b/cmd/kubeadm/app/master/apiclient.go index b72f948d85c..c36fa9c67c6 100644 --- a/cmd/kubeadm/app/master/apiclient.go +++ b/cmd/kubeadm/app/master/apiclient.go @@ -36,6 +36,7 @@ import ( const apiCallRetryInterval = 500 * time.Millisecond +// TODO: This method shouldn't exist as a standalone function but be integrated into CreateClientFromFile func createAPIClient(adminKubeconfig *clientcmdapi.Config) (*clientset.Clientset, error) { adminClientConfig, err := clientcmd.NewDefaultClientConfig( *adminKubeconfig, diff --git a/cmd/kubeadm/app/master/manifests.go b/cmd/kubeadm/app/master/manifests.go index 65e9dbd1409..1779fa72781 100644 --- a/cmd/kubeadm/app/master/manifests.go +++ b/cmd/kubeadm/app/master/manifests.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/images" "k8s.io/kubernetes/pkg/api/resource" api "k8s.io/kubernetes/pkg/api/v1" @@ -301,6 +302,10 @@ 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 @@ -313,10 +318,10 @@ func getAPIServerCommand(cfg *kubeadmapi.MasterConfiguration, selfHosted bool) [ "--insecure-bind-address=127.0.0.1", "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota", "--service-cluster-ip-range="+cfg.Networking.ServiceSubnet, - "--service-account-key-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/apiserver-key.pem", - "--client-ca-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/ca.pem", - "--tls-cert-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/apiserver.pem", - "--tls-private-key-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/apiserver-key.pem", + "--service-account-key-file="+getCertFilePath(kubeadmconstants.APIServerKeyName), + "--client-ca-file="+getCertFilePath(kubeadmconstants.CACertName), + "--tls-cert-file="+getCertFilePath(kubeadmconstants.APIServerCertName), + "--tls-private-key-file="+getCertFilePath(kubeadmconstants.APIServerKeyName), "--token-auth-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/tokens.csv", fmt.Sprintf("--secure-port=%d", cfg.API.Port), "--allow-privileged", @@ -400,10 +405,10 @@ func getControllerManagerCommand(cfg *kubeadmapi.MasterConfiguration, selfHosted "--leader-elect", "--master=127.0.0.1:8080", "--cluster-name="+DefaultClusterName, - "--root-ca-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/ca.pem", - "--service-account-private-key-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/apiserver-key.pem", - "--cluster-signing-cert-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/ca.pem", - "--cluster-signing-key-file="+kubeadmapi.GlobalEnvParams.HostPKIPath+"/ca-key.pem", + "--root-ca-file="+getCertFilePath(kubeadmconstants.CACertName), + "--service-account-private-key-file="+getCertFilePath(kubeadmconstants.APIServerKeyName), + "--cluster-signing-cert-file="+getCertFilePath(kubeadmconstants.CACertName), + "--cluster-signing-key-file="+getCertFilePath(kubeadmconstants.CAKeyName), "--insecure-experimental-approve-all-kubelet-csrs-for-group="+KubeletBootstrapGroup, ) diff --git a/cmd/kubeadm/app/master/manifests_test.go b/cmd/kubeadm/app/master/manifests_test.go index 72d5bb055b2..67fd1094288 100644 --- a/cmd/kubeadm/app/master/manifests_test.go +++ b/cmd/kubeadm/app/master/manifests_test.go @@ -372,10 +372,10 @@ func TestGetAPIServerCommand(t *testing.T) { "--insecure-bind-address=127.0.0.1", "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota", "--service-cluster-ip-range=bar", - "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", - "--client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--tls-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.pem", - "--tls-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", + "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", + "--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", "--token-auth-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/tokens.csv", fmt.Sprintf("--secure-port=%d", 123), "--allow-privileged", @@ -392,10 +392,10 @@ func TestGetAPIServerCommand(t *testing.T) { "--insecure-bind-address=127.0.0.1", "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota", "--service-cluster-ip-range=bar", - "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", - "--client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--tls-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.pem", - "--tls-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", + "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", + "--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", "--token-auth-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/tokens.csv", fmt.Sprintf("--secure-port=%d", 123), "--allow-privileged", @@ -414,10 +414,10 @@ func TestGetAPIServerCommand(t *testing.T) { "--insecure-bind-address=127.0.0.1", "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota", "--service-cluster-ip-range=bar", - "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", - "--client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--tls-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.pem", - "--tls-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", + "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", + "--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", "--token-auth-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/tokens.csv", fmt.Sprintf("--secure-port=%d", 123), "--allow-privileged", @@ -438,10 +438,10 @@ func TestGetAPIServerCommand(t *testing.T) { "--insecure-bind-address=127.0.0.1", "--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota", "--service-cluster-ip-range=bar", - "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", - "--client-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--tls-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.pem", - "--tls-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", + "--service-account-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", + "--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", "--token-auth-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/tokens.csv", fmt.Sprintf("--secure-port=%d", 123), "--allow-privileged", @@ -480,10 +480,10 @@ func TestGetControllerManagerCommand(t *testing.T) { "--leader-elect", "--master=127.0.0.1:8080", "--cluster-name=" + DefaultClusterName, - "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", - "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca-key.pem", + "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", + "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", + "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", + "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.key", "--insecure-experimental-approve-all-kubelet-csrs-for-group=kubeadm:kubelet-bootstrap", }, }, @@ -495,10 +495,10 @@ func TestGetControllerManagerCommand(t *testing.T) { "--leader-elect", "--master=127.0.0.1:8080", "--cluster-name=" + DefaultClusterName, - "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", - "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca-key.pem", + "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", + "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", + "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", + "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.key", "--insecure-experimental-approve-all-kubelet-csrs-for-group=kubeadm:kubelet-bootstrap", "--cloud-provider=foo", }, @@ -511,10 +511,10 @@ func TestGetControllerManagerCommand(t *testing.T) { "--leader-elect", "--master=127.0.0.1:8080", "--cluster-name=" + DefaultClusterName, - "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver-key.pem", - "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.pem", - "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca-key.pem", + "--root-ca-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", + "--service-account-private-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/apiserver.key", + "--cluster-signing-cert-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.crt", + "--cluster-signing-key-file=" + kubeadmapi.GlobalEnvParams.HostPKIPath + "/ca.key", "--insecure-experimental-approve-all-kubelet-csrs-for-group=kubeadm:kubelet-bootstrap", "--allocate-node-cidrs=true", "--cluster-cidr=bar", diff --git a/cmd/kubeadm/app/phases/certs/BUILD b/cmd/kubeadm/app/phases/certs/BUILD index 21f563d1288..bbab6f74943 100644 --- a/cmd/kubeadm/app/phases/certs/BUILD +++ b/cmd/kubeadm/app/phases/certs/BUILD @@ -10,10 +10,7 @@ load( go_test( name = "go_default_test", - srcs = [ - "certs_test.go", - "pki_helpers_test.go", - ], + srcs = ["certs_test.go"], library = ":go_default_library", tags = ["automanaged"], deps = [ @@ -27,12 +24,14 @@ go_library( srcs = [ "certs.go", "doc.go", - "pki_helpers.go", ], tags = ["automanaged"], deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", + "//cmd/kubeadm/app/constants:go_default_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/client-go/pkg/util/cert", ], ) @@ -46,6 +45,9 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//cmd/kubeadm/app/phases/certs/pkiutil:all-srcs", + ], tags = ["automanaged"], ) diff --git a/cmd/kubeadm/app/phases/certs/certs.go b/cmd/kubeadm/app/phases/certs/certs.go index 186c5d3998d..abc3527c345 100644 --- a/cmd/kubeadm/app/phases/certs/certs.go +++ b/cmd/kubeadm/app/phases/certs/certs.go @@ -17,21 +17,30 @@ limitations under the License. package certs import ( + "crypto/rsa" "crypto/x509" "fmt" "net" "os" + setutil "k8s.io/apimachinery/pkg/util/sets" certutil "k8s.io/client-go/pkg/util/cert" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" "k8s.io/kubernetes/pkg/registry/core/service/ipallocator" ) +// TODO: Integration test cases +// no files exist => create all four files +// valid ca.{crt,key} exists => create apiserver.{crt,key} +// valid ca.{crt,key} and apiserver.{crt,key} exists => do nothing +// invalid ca.{crt,key} exists => error +// only one of the .crt or .key file exists => error + // CreatePKIAssets will create and write to disk all PKI assets necessary to establish the control plane. -// It first generates a self-signed CA certificate, a server certificate (signed by the CA) and a key for -// signing service account tokens. It returns CA key and certificate, which is convenient for use with -// client config funcs. -func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration, pkiPath string) (*x509.Certificate, error) { +// 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 @@ -43,7 +52,7 @@ func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration, pkiPath string) (*x509 } hostname, err := os.Hostname() if err != nil { - return nil, fmt.Errorf("couldn't get the hostname: %v", err) + return fmt.Errorf("couldn't get the hostname: %v", err) } altNames.DNSNames = append(cfg.API.ExternalDNSNames, hostname) altNames.DNSNames = append(altNames.DNSNames, internalAPIServerFQDN...) @@ -53,50 +62,107 @@ func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration, pkiPath string) (*x509 if ip := net.ParseIP(a); ip != nil { altNames.IPs = append(altNames.IPs, ip) } else { - return nil, fmt.Errorf("could not parse ip %q", a) + return fmt.Errorf("could not parse ip %q", a) } } // and lastly, extract the internal IP address for the API server _, n, err := net.ParseCIDR(cfg.Networking.ServiceSubnet) if err != nil { - return nil, fmt.Errorf("error parsing CIDR %q: %v", cfg.Networking.ServiceSubnet, err) + return fmt.Errorf("error parsing CIDR %q: %v", cfg.Networking.ServiceSubnet, err) } internalAPIServerVirtualIP, err := ipallocator.GetIndexedIP(n, 1) if err != nil { - return nil, fmt.Errorf("unable to allocate IP address for the API server from the given CIDR (%q) [%v]", &cfg.Networking.ServiceSubnet, err) + 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) - caKey, caCert, err := newCertificateAuthority() - if err != nil { - return nil, fmt.Errorf("failure while creating CA keys and certificate [%v]", err) + var caCert *x509.Certificate + var caKey *rsa.PrivateKey + // If at least one of them exists, we should try to load them + // In the case that only one exists, there will most likely be an error anyway + if pkiutil.CertOrKeyExist(pkiDir, kubeadmconstants.CACertAndKeyBaseName) { + // Try to load ca.crt and ca.key from the PKI directory + caCert, caKey, err = pkiutil.TryLoadCertAndKeyFromDisk(pkiDir, kubeadmconstants.CACertAndKeyBaseName) + if err != nil || caCert == nil || caKey == nil { + return fmt.Errorf("certificate and/or key existed but they could not be loaded properly") + } + + // The certificate and key could be loaded, but the certificate is not a CA + if !caCert.IsCA { + return fmt.Errorf("certificate and key could be loaded but the certificate is not a CA") + } + + fmt.Println("[certificates] Using the existing CA certificate and key.") + } else { + // The certificate and the key did NOT exist, let's generate them now + caCert, caKey, err = pkiutil.NewCertificateAuthority() + if err != nil { + return fmt.Errorf("failure while generating CA certificate and key [%v]", err) + } + + if err = pkiutil.WriteCertAndKey(pkiDir, kubeadmconstants.CACertAndKeyBaseName, caCert, caKey); err != nil { + return fmt.Errorf("failure while saving CA certificate and key [%v]", err) + } + fmt.Println("[certificates] Generated CA certificate and key.") } - if err := writeKeysAndCert(pkiPath, "ca", caKey, caCert); err != nil { - return nil, fmt.Errorf("failure while saving CA keys and certificate [%v]", err) - } - fmt.Println("[certificates] Generated Certificate Authority key and certificate.") + // If at least one of them exists, we should try to load them + // In the case that only one exists, there will most likely be an error anyway + if pkiutil.CertOrKeyExist(pkiDir, kubeadmconstants.APIServerCertAndKeyBaseName) { + // Try to load ca.crt and ca.key from the PKI directory + apiCert, apiKey, err := pkiutil.TryLoadCertAndKeyFromDisk(pkiDir, kubeadmconstants.APIServerCertAndKeyBaseName) + if err != nil || apiCert == nil || apiKey == nil { + return fmt.Errorf("certificate and/or key existed but they could not be loaded properly") + } - apiKey, apiCert, err := newServerKeyAndCert(caCert, caKey, altNames) - if err != nil { - return nil, fmt.Errorf("failure while creating API server keys and certificate [%v]", err) + fmt.Println("[certificates] Using the existing API Server certificate and key.") + } else { + // The certificate and the key did NOT exist, let's generate them now + // TODO: Add a test case to verify that this cert has the x509.ExtKeyUsageServerAuth flag + config := certutil.Config{ + CommonName: "kube-apiserver", + AltNames: altNames, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + apiCert, apiKey, err := pkiutil.NewCertAndKey(caCert, caKey, config) + if err != nil { + return fmt.Errorf("failure while creating API server key and certificate [%v]", err) + } + + if err = pkiutil.WriteCertAndKey(pkiDir, kubeadmconstants.APIServerCertAndKeyBaseName, apiCert, apiKey); err != nil { + return fmt.Errorf("failure while saving API server certificate and key [%v]", err) + } + fmt.Println("[certificates] Generated API server certificate and key.") } - if err := writeKeysAndCert(pkiPath, "apiserver", apiKey, apiCert); err != nil { - return nil, fmt.Errorf("failure while saving API server keys and certificate [%v]", err) - } - fmt.Println("[certificates] Generated API Server key and certificate") + fmt.Printf("[certificates] Valid certificates and keys now exist in %q\n", pkiDir) - // Generate a private key for service accounts - saKey, err := certutil.NewPrivateKey() - if err != nil { - return nil, fmt.Errorf("failure while creating service account signing keys [%v]", err) - } - if err := writeKeysAndCert(pkiPath, "sa", saKey, nil); err != nil { - return nil, fmt.Errorf("failure while saving service account signing keys [%v]", err) - } - fmt.Println("[certificates] Generated Service Account signing keys") - fmt.Printf("[certificates] Created keys and certificates in %q\n", pkiPath) - return caCert, nil + return nil +} + +// Verify 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...) + + for _, dnsNameThatShouldExist := range altNames.DNSNames { + if !dnsset.Has(dnsNameThatShouldExist) { + return false + } + } + + for _, ipThatShouldExist := range altNames.IPs { + found := false + for _, ip := range IPs { + if ip.Equal(ipThatShouldExist) { + found = true + break + } + } + + if !found { + return false + } + } + return true } diff --git a/cmd/kubeadm/app/phases/certs/certs_test.go b/cmd/kubeadm/app/phases/certs/certs_test.go index 2377646fc69..14ffee522cf 100644 --- a/cmd/kubeadm/app/phases/certs/certs_test.go +++ b/cmd/kubeadm/app/phases/certs/certs_test.go @@ -19,9 +19,11 @@ package certs import ( "fmt" "io/ioutil" + "net" "os" "testing" + certutil "k8s.io/client-go/pkg/util/cert" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" ) @@ -65,7 +67,7 @@ func TestCreatePKIAssets(t *testing.T) { }, } for _, rt := range tests { - _, actual := CreatePKIAssets(rt.cfg, fmt.Sprintf("%s/etc/kubernetes/pki", tmpdir)) + actual := CreatePKIAssets(rt.cfg, fmt.Sprintf("%s/etc/kubernetes/pki", tmpdir)) if (actual == nil) != rt.expected { t.Errorf( "failed CreatePKIAssets with an error:\n\texpected: %t\n\t actual: %t", @@ -75,3 +77,52 @@ func TestCreatePKIAssets(t *testing.T) { } } } + +func TestCheckAltNamesExist(t *testing.T) { + var tests = []struct { + IPs []net.IP + DNSNames []string + requiredAltNames certutil.AltNames + succeed bool + }{ + { + // equal + requiredAltNames: certutil.AltNames{IPs: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("192.168.1.2")}, DNSNames: []string{"foo", "bar", "baz"}}, + IPs: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("192.168.1.2")}, + DNSNames: []string{"foo", "bar", "baz"}, + succeed: true, + }, + { + // the loaded cert has more ips than required, ok + requiredAltNames: certutil.AltNames{IPs: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("192.168.1.2")}, DNSNames: []string{"foo", "bar", "baz"}}, + IPs: []net.IP{net.ParseIP("192.168.2.5"), net.ParseIP("1.1.1.1"), net.ParseIP("192.168.1.2")}, + DNSNames: []string{"a", "foo", "b", "bar", "baz"}, + succeed: true, + }, + { + // the loaded cert doesn't have all ips + requiredAltNames: certutil.AltNames{IPs: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("192.168.2.5"), net.ParseIP("192.168.1.2")}, DNSNames: []string{"foo", "bar", "baz"}}, + IPs: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("192.168.1.2")}, + DNSNames: []string{"foo", "bar", "baz"}, + succeed: false, + }, + { + // the loaded cert doesn't have all ips + requiredAltNames: certutil.AltNames{IPs: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("192.168.1.2")}, DNSNames: []string{"foo", "bar", "b", "baz"}}, + IPs: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("192.168.1.2")}, + DNSNames: []string{"foo", "bar", "baz"}, + succeed: false, + }, + } + + for _, rt := range tests { + succeeded := checkAltNamesExist(rt.IPs, rt.DNSNames, rt.requiredAltNames) + if succeeded != rt.succeed { + t.Errorf( + "failed checkAltNamesExist:\n\texpected: %t\n\t actual: %t", + rt.succeed, + succeeded, + ) + } + } +} diff --git a/cmd/kubeadm/app/phases/certs/doc.go b/cmd/kubeadm/app/phases/certs/doc.go index e92cf488e62..9978665750d 100644 --- a/cmd/kubeadm/app/phases/certs/doc.go +++ b/cmd/kubeadm/app/phases/certs/doc.go @@ -30,12 +30,9 @@ package certs OUTPUTS: Files to PKIPath (default /etc/kubernetes/pki): - - apiserver-key.pem - - apiserver-pub.pem - - apiserver.pem - - ca-key.pem - - ca-pub.pem - - ca.pem - - sa-key.pem - - sa-pub.pem + - ca.crt + - ca.key + - apiserver.crt + - apiserver.key + */ diff --git a/cmd/kubeadm/app/phases/certs/pki_helpers.go b/cmd/kubeadm/app/phases/certs/pki_helpers.go deleted file mode 100644 index dac7379b4b3..00000000000 --- a/cmd/kubeadm/app/phases/certs/pki_helpers.go +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright 2016 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 certs - -import ( - "crypto/rsa" - "crypto/x509" - "fmt" - "path" - - certutil "k8s.io/client-go/pkg/util/cert" -) - -func newCertificateAuthority() (*rsa.PrivateKey, *x509.Certificate, error) { - key, err := certutil.NewPrivateKey() - if err != nil { - return nil, nil, fmt.Errorf("unable to create private key [%v]", err) - } - - config := certutil.Config{ - CommonName: "kubernetes", - } - cert, err := certutil.NewSelfSignedCACert(config, key) - if err != nil { - return nil, nil, fmt.Errorf("unable to create self-signed certificate [%v]", err) - } - - return key, cert, nil -} - -func newServerKeyAndCert(caCert *x509.Certificate, caKey *rsa.PrivateKey, altNames certutil.AltNames) (*rsa.PrivateKey, *x509.Certificate, error) { - key, err := certutil.NewPrivateKey() - if err != nil { - return nil, nil, fmt.Errorf("unable to create private key [%v]", err) - } - - config := certutil.Config{ - CommonName: "kube-apiserver", - AltNames: altNames, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - } - cert, err := certutil.NewSignedCert(config, key, caCert, caKey) - if err != nil { - return nil, nil, fmt.Errorf("unable to sign certificate [%v]", err) - } - - return key, cert, nil -} - -func NewClientKeyAndCert(config *certutil.Config, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*rsa.PrivateKey, *x509.Certificate, error) { - key, err := certutil.NewPrivateKey() - if err != nil { - return nil, nil, fmt.Errorf("unable to create private key [%v]", err) - } - // force usage to client usage - configCopy := *config - configCopy.Usages = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} - cert, err := certutil.NewSignedCert(configCopy, key, caCert, caKey) - if err != nil { - return nil, nil, fmt.Errorf("unable to sign certificate [%v]", err) - } - - return key, cert, nil -} - -func writeKeysAndCert(pkiPath string, name string, key *rsa.PrivateKey, cert *x509.Certificate) error { - publicKeyPath, privateKeyPath, certificatePath := pathsKeysCerts(pkiPath, name) - - if key != nil { - if err := certutil.WriteKey(privateKeyPath, certutil.EncodePrivateKeyPEM(key)); err != nil { - return fmt.Errorf("unable to write private key file (%q) [%v]", privateKeyPath, err) - } - if pubKey, err := certutil.EncodePublicKeyPEM(&key.PublicKey); err == nil { - if err := certutil.WriteKey(publicKeyPath, pubKey); err != nil { - return fmt.Errorf("unable to write public key file (%q) [%v]", publicKeyPath, err) - } - } else { - return fmt.Errorf("unable to encode public key to PEM [%v]", err) - } - } - - if cert != nil { - if err := certutil.WriteCert(certificatePath, certutil.EncodeCertPEM(cert)); err != nil { - return fmt.Errorf("unable to write certificate file (%q) [%v]", certificatePath, err) - } - } - - return nil -} - -func pathsKeysCerts(pkiPath, name string) (string, string, string) { - return path.Join(pkiPath, fmt.Sprintf("%s-pub.pem", name)), - path.Join(pkiPath, fmt.Sprintf("%s-key.pem", name)), - path.Join(pkiPath, fmt.Sprintf("%s.pem", name)) -} diff --git a/cmd/kubeadm/app/phases/certs/pki_helpers_test.go b/cmd/kubeadm/app/phases/certs/pki_helpers_test.go deleted file mode 100644 index ea593fcded2..00000000000 --- a/cmd/kubeadm/app/phases/certs/pki_helpers_test.go +++ /dev/null @@ -1,175 +0,0 @@ -/* -Copyright 2016 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 certs - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "io/ioutil" - "os" - "testing" - - certutil "k8s.io/client-go/pkg/util/cert" -) - -func TestNewCertificateAuthority(t *testing.T) { - r, x, err := newCertificateAuthority() - - if r == nil { - t.Errorf( - "failed newCertificateAuthority, rsa key == nil", - ) - } - if x == nil { - t.Errorf( - "failed newCertificateAuthority, x509 cert == nil", - ) - } - if err != nil { - t.Errorf( - "failed newCertificateAuthority with an error: %v", - err, - ) - } -} - -func TestNewServerKeyAndCert(t *testing.T) { - var tests = []struct { - caKeySize int - expected bool - }{ - { - // RSA key too small - caKeySize: 128, - expected: false, - }, - { - // Should succeed - caKeySize: 2048, - expected: true, - }, - } - - for _, rt := range tests { - caKey, err := rsa.GenerateKey(rand.Reader, rt.caKeySize) - if err != nil { - t.Fatalf("Couldn't create rsa Private Key") - } - caCert := &x509.Certificate{} - altNames := certutil.AltNames{} - _, _, actual := newServerKeyAndCert(caCert, caKey, altNames) - if (actual == nil) != rt.expected { - t.Errorf( - "failed newServerKeyAndCert:\n\texpected: %t\n\t actual: %t", - rt.expected, - (actual == nil), - ) - } - } -} - -func TestNewClientKeyAndCert(t *testing.T) { - var tests = []struct { - caKeySize int - expected bool - }{ - { - // RSA key too small - caKeySize: 128, - expected: false, - }, - { - caKeySize: 2048, - expected: true, - }, - } - - for _, rt := range tests { - caKey, err := rsa.GenerateKey(rand.Reader, rt.caKeySize) - if err != nil { - t.Fatalf("Couldn't create rsa Private Key") - } - caCert := &x509.Certificate{} - config := &certutil.Config{ - CommonName: "test", - Organization: []string{"test"}, - } - _, _, actual := NewClientKeyAndCert(config, caCert, caKey) - if (actual == nil) != rt.expected { - t.Errorf( - "failed NewClientKeyAndCert:\n\texpected: %t\n\t actual: %t", - rt.expected, - (actual == nil), - ) - } - } -} - -func TestWriteKeysAndCert(t *testing.T) { - tmpdir, err := ioutil.TempDir("", "") - if err != nil { - t.Fatalf("Couldn't create tmpdir") - } - defer os.RemoveAll(tmpdir) - - caKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("Couldn't create rsa Private Key") - } - caCert := &x509.Certificate{} - actual := writeKeysAndCert(tmpdir, "foo", caKey, caCert) - if actual != nil { - t.Errorf( - "failed writeKeysAndCert with an error: %v", - actual, - ) - } -} - -func TestPathsKeysCerts(t *testing.T) { - var tests = []struct { - pkiPath string - name string - expected []string - }{ - { - pkiPath: "foo", - name: "bar", - expected: []string{"foo/bar-pub.pem", "foo/bar-key.pem", "foo/bar.pem"}, - }, - { - pkiPath: "bar", - name: "foo", - expected: []string{"bar/foo-pub.pem", "bar/foo-key.pem", "bar/foo.pem"}, - }, - } - - for _, rt := range tests { - a, b, c := pathsKeysCerts(rt.pkiPath, rt.name) - all := []string{a, b, c} - for i := range all { - if all[i] != rt.expected[i] { - t.Errorf( - "failed pathsKeysCerts:\n\texpected: %s\n\t actual: %s", - rt.expected[i], - all[i], - ) - } - } - } -} diff --git a/cmd/kubeadm/app/phases/certs/pkiutil/BUILD b/cmd/kubeadm/app/phases/certs/pkiutil/BUILD new file mode 100644 index 00000000000..d340676ff82 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/pkiutil/BUILD @@ -0,0 +1,37 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = ["pki_helpers_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//vendor:k8s.io/client-go/pkg/util/cert"], +) + +go_library( + name = "go_default_library", + srcs = ["pki_helpers.go"], + tags = ["automanaged"], + deps = ["//vendor:k8s.io/client-go/pkg/util/cert"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers.go b/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers.go new file mode 100644 index 00000000000..483db37472a --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers.go @@ -0,0 +1,145 @@ +/* +Copyright 2016 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 pkiutil + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "fmt" + "os" + "path" + "time" + + certutil "k8s.io/client-go/pkg/util/cert" +) + +// TODO: It should be able to generate different types of private keys, at least: RSA and ECDSA (and in the future maybe Ed25519 as well) +// TODO: See if it makes sense to move this package directly to pkg/util/cert + +func NewCertificateAuthority() (*x509.Certificate, *rsa.PrivateKey, error) { + key, err := certutil.NewPrivateKey() + if err != nil { + return nil, nil, fmt.Errorf("unable to create private key [%v]", err) + } + + config := certutil.Config{ + CommonName: "kubernetes", + } + cert, err := certutil.NewSelfSignedCACert(config, key) + if err != nil { + return nil, nil, fmt.Errorf("unable to create self-signed certificate [%v]", err) + } + + return cert, key, nil +} + +func NewCertAndKey(caCert *x509.Certificate, caKey *rsa.PrivateKey, config certutil.Config) (*x509.Certificate, *rsa.PrivateKey, error) { + key, err := certutil.NewPrivateKey() + if err != nil { + return nil, nil, fmt.Errorf("unable to create private key [%v]", err) + } + + cert, err := certutil.NewSignedCert(config, key, caCert, caKey) + if err != nil { + return nil, nil, fmt.Errorf("unable to sign certificate [%v]", err) + } + + return cert, key, nil +} + +func WriteCertAndKey(pkiPath string, name string, cert *x509.Certificate, key *rsa.PrivateKey) error { + certificatePath, privateKeyPath := pathsForCertAndKey(pkiPath, name) + + if cert == nil { + return fmt.Errorf("certificate cannot be nil when writing to file") + } + if cert == nil { + return fmt.Errorf("private key cannot be nil when writing to file") + } + + if err := certutil.WriteKey(privateKeyPath, certutil.EncodePrivateKeyPEM(key)); err != nil { + return fmt.Errorf("unable to write private key to file %q: [%v]", privateKeyPath, err) + } + + if err := certutil.WriteCert(certificatePath, certutil.EncodeCertPEM(cert)); err != nil { + return fmt.Errorf("unable to write certificate to file %q: [%v]", certificatePath, err) + } + + return nil +} + +// CertOrKeyExist retuns a boolean whether the cert or the key exists +func CertOrKeyExist(pkiPath, name string) bool { + certificatePath, privateKeyPath := pathsForCertAndKey(pkiPath, name) + + _, certErr := os.Stat(certificatePath) + _, keyErr := os.Stat(privateKeyPath) + if os.IsNotExist(certErr) && os.IsNotExist(keyErr) { + // The cert or the key did not exist + return false + } + + // Both files exist or one of them + return true +} + +// TryLoadCertAndKeyFromDisk tries to load a cert and a key from the disk and validates that they are valid +func TryLoadCertAndKeyFromDisk(pkiPath, name string) (*x509.Certificate, *rsa.PrivateKey, error) { + certificatePath, privateKeyPath := pathsForCertAndKey(pkiPath, name) + + certs, err := certutil.CertsFromFile(certificatePath) + if err != nil { + return nil, nil, fmt.Errorf("couldn't load the certificate file %s: %v", certificatePath, err) + } + + // We are only putting one certificate in the certificate pem file, so it's safe to just pick the first one + // TODO: Support multiple certs here in order to be able to rotate certs + cert := certs[0] + + // Parse the private key from a file + privKey, err := certutil.PrivateKeyFromFile(privateKeyPath) + if err != nil { + return nil, nil, fmt.Errorf("couldn't load the private key file %s: %v", privateKeyPath, err) + } + var key *rsa.PrivateKey + switch k := privKey.(type) { + case *rsa.PrivateKey: + key = k + case *ecdsa.PrivateKey: + // TODO: Abstract rsa.PrivateKey away and make certutil.NewSignedCert accept a ecdsa.PrivateKey as well + // After that, we can support generating kubeconfig files from ecdsa private keys as well + return nil, nil, fmt.Errorf("the private key file %s isn't in RSA format", privateKeyPath) + default: + return nil, nil, fmt.Errorf("the private key file %s isn't in RSA format", privateKeyPath) + } + + // Check so that the certificate is valid now + now := time.Now() + if now.Before(cert.NotBefore) { + return nil, nil, fmt.Errorf("the certificate is not valid yet") + } + if now.After(cert.NotAfter) { + return nil, nil, fmt.Errorf("the certificate is has expired") + } + + return cert, key, nil +} + +func pathsForCertAndKey(pkiPath, name string) (string, string) { + return path.Join(pkiPath, fmt.Sprintf("%s.crt", name)), path.Join(pkiPath, fmt.Sprintf("%s.key", name)) +} diff --git a/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers_test.go b/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers_test.go new file mode 100644 index 00000000000..6173be60b1a --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/pkiutil/pki_helpers_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2016 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 pkiutil + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "io/ioutil" + "os" + "testing" + + certutil "k8s.io/client-go/pkg/util/cert" +) + +func TestNewCertificateAuthority(t *testing.T) { + cert, key, err := NewCertificateAuthority() + + if cert == nil { + t.Errorf( + "failed NewCertificateAuthority, cert == nil", + ) + } + if key == nil { + t.Errorf( + "failed NewCertificateAuthority, key == nil", + ) + } + if err != nil { + t.Errorf( + "failed NewCertificateAuthority with an error: %v", + err, + ) + } +} + +func TestNewCertAndKey(t *testing.T) { + var tests = []struct { + caKeySize int + expected bool + }{ + { + // RSA key too small + caKeySize: 128, + expected: false, + }, + { + // Should succeed + caKeySize: 2048, + expected: true, + }, + } + + for _, rt := range tests { + caKey, err := rsa.GenerateKey(rand.Reader, rt.caKeySize) + if err != nil { + t.Fatalf("Couldn't create rsa Private Key") + } + caCert := &x509.Certificate{} + config := certutil.Config{ + CommonName: "test", + Organization: []string{"test"}, + } + _, _, actual := NewCertAndKey(caCert, caKey, config) + if (actual == nil) != rt.expected { + t.Errorf( + "failed NewCertAndKey:\n\texpected: %t\n\t actual: %t", + rt.expected, + (actual == nil), + ) + } + } +} + +func TestWriteCertAndKey(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Couldn't create tmpdir") + } + defer os.Remove(tmpdir) + + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Couldn't create rsa Private Key") + } + caCert := &x509.Certificate{} + actual := WriteCertAndKey(tmpdir, "foo", caCert, caKey) + if actual != nil { + t.Errorf( + "failed WriteCertAndKey with an error: %v", + actual, + ) + } +} diff --git a/cmd/kubeadm/app/phases/kubeconfig/BUILD b/cmd/kubeadm/app/phases/kubeconfig/BUILD index 2cae35fc129..796fe7db9f3 100644 --- a/cmd/kubeadm/app/phases/kubeconfig/BUILD +++ b/cmd/kubeadm/app/phases/kubeconfig/BUILD @@ -24,7 +24,8 @@ go_library( ], tags = ["automanaged"], deps = [ - "//cmd/kubeadm/app/phases/certs:go_default_library", + "//cmd/kubeadm/app/constants:go_default_library", + "//cmd/kubeadm/app/phases/certs/pkiutil:go_default_library", "//pkg/client/unversioned/clientcmd:go_default_library", "//vendor:k8s.io/client-go/pkg/util/cert", "//vendor:k8s.io/client-go/tools/clientcmd/api", diff --git a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go index 93ae3b39b8a..2adaf9a22e9 100644 --- a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go +++ b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go @@ -17,90 +17,85 @@ limitations under the License. package kubeconfig import ( - "crypto/ecdsa" + "bytes" "crypto/rsa" "crypto/x509" "fmt" + "io/ioutil" "os" - "path" + "path/filepath" certutil "k8s.io/client-go/pkg/util/cert" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - certphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" ) const ( KubernetesDirPermissions = 0700 + KubeConfigFilePermissions = 0600 AdminKubeConfigFileName = "admin.conf" AdminKubeConfigClientName = "kubernetes-admin" KubeletKubeConfigFileName = "kubelet.conf" KubeletKubeConfigClientName = "kubelet" ) -// This function is called from the main init and does the work for the default phase behaviour // TODO: Make an integration test for this function that runs after the certificates phase // and makes sure that those two phases work well together... -func CreateAdminAndKubeletKubeConfig(masterEndpoint, pkiDir, outDir string) error { - // Parse the certificate from a file - caCertPath := path.Join(pkiDir, "ca.pem") - caCerts, err := certutil.CertsFromFile(caCertPath) - if err != nil { - return fmt.Errorf("couldn't load the CA cert file %s: %v", caCertPath, err) - } - // We are only putting one certificate in the CA certificate pem file, so it's safe to just use the first one - caCert := caCerts[0] - // Parse the rsa private key from a file - caKeyPath := path.Join(pkiDir, "ca-key.pem") - priv, err := certutil.PrivateKeyFromFile(caKeyPath) - if err != nil { - return fmt.Errorf("couldn't load the CA private key file %s: %v", caKeyPath, err) - } - var caKey *rsa.PrivateKey - switch k := priv.(type) { - case *rsa.PrivateKey: - caKey = k - case *ecdsa.PrivateKey: - // TODO: Abstract rsa.PrivateKey away and make certutil.NewSignedCert accept a ecdsa.PrivateKey as well - // After that, we can support generating kubeconfig files from ecdsa private keys as well - return fmt.Errorf("the CA private key file %s isn't in RSA format", caKeyPath) - default: - return fmt.Errorf("the CA private key file %s isn't in RSA format", caKeyPath) +// TODO: Integration test cases: +// /etc/kubernetes/{admin,kubelet}.conf don't exist => generate kubeconfig files +// /etc/kubernetes/{admin,kubelet}.conf and certs in /etc/kubernetes/pki exist => don't touch anything as long as everything's valid +// /etc/kubernetes/{admin,kubelet}.conf exist but the server URL is invalid in those files => error +// /etc/kubernetes/{admin,kubelet}.conf exist but the CA cert doesn't match what's in the pki dir => error +// /etc/kubernetes/{admin,kubelet}.conf exist but not certs => certs will be generated and conflict with the kubeconfig files => error + +// CreateAdminAndKubeletKubeConfig is called from the main init and does the work for the default phase behaviour +func CreateAdminAndKubeletKubeConfig(masterEndpoint, pkiDir, outDir string) error { + + // Try to load ca.crt and ca.key from the PKI directory + caCert, caKey, err := pkiutil.TryLoadCertAndKeyFromDisk(pkiDir, kubeadmconstants.CACertAndKeyBaseName) + if err != nil || caCert == nil || caKey == nil { + return fmt.Errorf("couldn't create a kubeconfig; the CA files couldn't be loaded: %v", err) } // User admin should have full access to the cluster - adminCertConfig := &certutil.Config{ + // TODO: Add test case that make sure this cert has the x509.ExtKeyUsageClientAuth flag + adminCertConfig := certutil.Config{ CommonName: AdminKubeConfigClientName, Organization: []string{"system:masters"}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } - adminKubeConfigFilePath := path.Join(outDir, AdminKubeConfigFileName) + adminKubeConfigFilePath := filepath.Join(outDir, AdminKubeConfigFileName) if err := createKubeConfigFileForClient(masterEndpoint, adminKubeConfigFilePath, adminCertConfig, caCert, caKey); err != nil { return fmt.Errorf("couldn't create config for %s: %v", AdminKubeConfigClientName, err) } - // The kubelet should have limited access to the cluster - kubeletCertConfig := &certutil.Config{ + // TODO: The kubelet should have limited access to the cluster. Right now, this gives kubelet basically root access + // and we do need that in the bootstrap phase, but we should swap it out after the control plane is up + // TODO: Add test case that make sure this cert has the x509.ExtKeyUsageClientAuth flag + kubeletCertConfig := certutil.Config{ CommonName: KubeletKubeConfigClientName, Organization: []string{"system:nodes"}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } - kubeletKubeConfigFilePath := path.Join(outDir, KubeletKubeConfigFileName) + kubeletKubeConfigFilePath := filepath.Join(outDir, KubeletKubeConfigFileName) if err := createKubeConfigFileForClient(masterEndpoint, kubeletKubeConfigFilePath, kubeletCertConfig, caCert, caKey); err != nil { - return fmt.Errorf("couldn't create config for %s: %v", KubeletKubeConfigClientName, err) + return fmt.Errorf("couldn't create a kubeconfig file for %s: %v", KubeletKubeConfigClientName, err) } - // TODO make credentials for the controller manager and kube proxy - + // TODO make credentials for the controller-manager, scheduler and kube-proxy return nil } -func createKubeConfigFileForClient(masterEndpoint, kubeConfigFilePath string, config *certutil.Config, caCert *x509.Certificate, caKey *rsa.PrivateKey) error { - key, cert, err := certphase.NewClientKeyAndCert(config, caCert, caKey) +func createKubeConfigFileForClient(masterEndpoint, kubeConfigFilePath string, config certutil.Config, caCert *x509.Certificate, caKey *rsa.PrivateKey) error { + cert, key, err := pkiutil.NewCertAndKey(caCert, caKey, config) if err != nil { return fmt.Errorf("failure while creating %s client certificate [%v]", config.CommonName, err) } - kubeConfig := MakeClientConfigWithCerts( + kubeconfig := MakeClientConfigWithCerts( masterEndpoint, "kubernetes", config.CommonName, @@ -109,26 +104,68 @@ func createKubeConfigFileForClient(masterEndpoint, kubeConfigFilePath string, co certutil.EncodeCertPEM(cert), ) - // Write it now to a file - return WriteKubeconfigToDisk(kubeConfigFilePath, kubeConfig) + // Write it now to a file if there already isn't a valid one + return writeKubeconfigToDiskIfNotExists(kubeConfigFilePath, kubeconfig) } -func WriteKubeconfigToDisk(filepath string, kubeconfig *clientcmdapi.Config) error { - // Make sure the dir exists or can be created - if err := os.MkdirAll(path.Dir(filepath), KubernetesDirPermissions); err != nil { - return fmt.Errorf("failed to create directory %q [%v]", path.Dir(filepath), err) +func WriteKubeconfigToDisk(filename string, kubeconfig *clientcmdapi.Config) error { + // Convert the KubeConfig object to a byte array + content, err := clientcmd.Write(*kubeconfig) + if err != nil { + return err } - // If err == nil, the file exists. Oops, we don't allow the file to exist already, fail. - if _, err := os.Stat(filepath); err == nil { - return fmt.Errorf("kubeconfig file %s already exists, but must not exist.", filepath) + // Create the directory if it does not exist + dir := filepath.Dir(filename) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err = os.MkdirAll(dir, KubernetesDirPermissions); err != nil { + return err + } } - if err := clientcmd.WriteToFile(*kubeconfig, filepath); err != nil { - return fmt.Errorf("failed to write to %q [%v]", filepath, err) + // No such kubeconfig file exists; write that kubeconfig down to disk then + if err := ioutil.WriteFile(filename, content, KubeConfigFilePermissions); err != nil { + return err } - fmt.Printf("[kubeconfig] Wrote KubeConfig file to disk: %q\n", filepath) + fmt.Printf("[kubeconfig] Wrote KubeConfig file to disk: %q\n", filename) + return nil +} + +// writeKubeconfigToDiskIfNotExists saves the KubeConfig struct to disk if there isn't any file at the given path +// If there already is a KubeConfig file at the given path; kubeadm tries to load it and check if the values in the +// existing and the expected config equals. If they do; kubeadm will just skip writing the file as it's up-to-date, +// but if a file exists but has old content or isn't a kubeconfig file, this function returns an error. +func writeKubeconfigToDiskIfNotExists(filename string, expectedConfig *clientcmdapi.Config) error { + // Check if the file exist, and if it doesn't, just write it to disk + if _, err := os.Stat(filename); os.IsNotExist(err) { + return WriteKubeconfigToDisk(filename, expectedConfig) + } + + // The kubeconfig already exists, let's check if it has got the same CA and server URL + currentConfig, err := clientcmd.LoadFromFile(filename) + if err != nil { + return fmt.Errorf("failed to load kubeconfig that already exists on disk [%v]", err) + } + + expectedCtx := expectedConfig.CurrentContext + expectedCluster := expectedConfig.Contexts[expectedCtx].Cluster + currentCtx := currentConfig.CurrentContext + currentCluster := currentConfig.Contexts[currentCtx].Cluster + // If the current CA cert on disk doesn't match the expected CA cert, error out because we have a file, but it's stale + if !bytes.Equal(currentConfig.Clusters[currentCluster].CertificateAuthorityData, expectedConfig.Clusters[expectedCluster].CertificateAuthorityData) { + return fmt.Errorf("a kubeconfig file %q exists already but has got the wrong CA cert", filename) + } + // If the current API Server location on disk doesn't match the expected API server, error out because we have a file, but it's stale + if currentConfig.Clusters[currentCluster].Server != expectedConfig.Clusters[expectedCluster].Server { + return fmt.Errorf("a kubeconfig file %q exists already but has got the wrong API Server URL", filename) + } + + // kubeadm doesn't validate the existing kubeconfig file more than this (kubeadm trusts the client certs to be valid) + // Basically, if we find a kubeconfig file with the same path; the same CA cert and the same server URL; + // kubeadm thinks those files are equal and doesn't bother writing a new file + fmt.Printf("[kubeconfig] Using existing up-to-date KubeConfig file: %q\n", filename) + return nil }