github.com/alibaba/sealer@v0.8.6-0.20220430115802-37a2bdaa8173/pkg/cert/kubeconfig.go (about)

     1  // Copyright 2016 The Kubernetes Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cert
    16  
    17  import (
    18  	"bytes"
    19  	"crypto"
    20  	"crypto/x509"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	"github.com/pkg/errors"
    27  	"k8s.io/client-go/tools/clientcmd"
    28  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    29  	"k8s.io/client-go/util/keyutil"
    30  
    31  	"github.com/alibaba/sealer/logger"
    32  )
    33  
    34  // clientCertAuth struct holds info required to build a client certificate to provide authentication info in a kubeconfig object
    35  type clientCertAuth struct {
    36  	CAKey         crypto.Signer
    37  	Organizations []string
    38  }
    39  
    40  // tokenAuth struct holds info required to use a token to provide authentication info in a kubeconfig object
    41  type tokenAuth struct {
    42  	Token string
    43  }
    44  
    45  // kubeConfigSpec struct holds info required to build a KubeConfig object
    46  type kubeConfigSpec struct {
    47  	CACert         *x509.Certificate
    48  	APIServer      string
    49  	ClientName     string
    50  	TokenAuth      *tokenAuth
    51  	ClientCertAuth *clientCertAuth
    52  }
    53  
    54  // CreateJoinControlPlaneKubeConfigFiles will create and write to disk the kubeconfig files required by kubeadm
    55  // join --control-plane workflow, plus the admin kubeconfig file used by the administrator and kubeadm itself; the
    56  // kubelet.conf file must not be created because it will be created and signed by the kubelet TLS bootstrap process.
    57  // If any kubeconfig files already exists, it used only if evaluated equal; otherwise an error is returned.
    58  func CreateJoinControlPlaneKubeConfigFiles(outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string) error {
    59  	return createKubeConfigFiles(
    60  		outDir,
    61  		cfg,
    62  		nodeName,
    63  		controlPlaneEndpoint,
    64  		clusterName,
    65  		"admin.conf",
    66  		"controller-manager.conf",
    67  		"scheduler.conf",
    68  		"kubelet.conf",
    69  	)
    70  }
    71  
    72  // cmd/kubeadm/app/cmd/phases/init/kubeconfig.go
    73  // cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go
    74  func CreateKubeConfigFile(kubeConfigFileName string, outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string) error {
    75  	logger.Info("creating kubeconfig file for %s", kubeConfigFileName)
    76  	return createKubeConfigFiles(outDir, cfg, kubeConfigFileName, nodeName, controlPlaneEndpoint, clusterName)
    77  }
    78  
    79  // createKubeConfigFiles creates all the requested kubeconfig files.
    80  // If kubeconfig files already exists, they are used only if evaluated equal; otherwise an error is returned.
    81  func createKubeConfigFiles(outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string, kubeConfigFileNames ...string) error {
    82  	// gets the KubeConfigSpecs, actualized for the current InitConfiguration
    83  	specs, err := getKubeConfigSpecs(cfg, nodeName, controlPlaneEndpoint)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	for _, kubeConfigFileName := range kubeConfigFileNames {
    89  		// retrieves the KubeConfigSpec for given kubeConfigFileName
    90  		spec, exists := specs[kubeConfigFileName]
    91  		if !exists {
    92  			return errors.Errorf("couldn't retrieve KubeConfigSpec for %s", kubeConfigFileName)
    93  		}
    94  
    95  		// builds the KubeConfig object
    96  		config, err := buildKubeConfigFromSpec(spec, clusterName)
    97  		if err != nil {
    98  			return err
    99  		}
   100  
   101  		// writes the kubeconfig to disk if it not exists
   102  		if err = createKubeConfigFileIfNotExists(outDir, kubeConfigFileName, config); err != nil {
   103  			return err
   104  		}
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  // getKubeConfigSpecs returns all KubeConfigSpecs actualized to the context of the current InitConfiguration
   111  // NB. this methods holds the information about how kubeadm creates kubeconfig files.
   112  func getKubeConfigSpecs(cfg Config, nodeName, controlPlaneEndpoint string) (map[string]*kubeConfigSpec, error) {
   113  	caCert, caKey, err := LoadCaCertAndKeyFromDisk(cfg)
   114  	if err != nil {
   115  		return nil, errors.Wrap(err, "couldn't create a kubeconfig; the CA files couldn't be loaded")
   116  	}
   117  
   118  	if len(nodeName) == 0 {
   119  		return nil, errors.New("nodeName can not be empty")
   120  	}
   121  
   122  	if len(controlPlaneEndpoint) == 0 {
   123  		return nil, errors.New("controlPlaneEndpoint  can not be empty")
   124  	}
   125  
   126  	var kubeConfigSpec = map[string]*kubeConfigSpec{
   127  		"admin.conf": {
   128  			CACert:     caCert,
   129  			APIServer:  controlPlaneEndpoint,
   130  			ClientName: "kubernetes-admin",
   131  			ClientCertAuth: &clientCertAuth{
   132  				CAKey:         caKey,
   133  				Organizations: []string{"system:masters"},
   134  			},
   135  		},
   136  		"kubelet.conf": {
   137  			CACert:     caCert,
   138  			APIServer:  controlPlaneEndpoint,
   139  			ClientName: fmt.Sprintf("%s%s", "system:node:", nodeName),
   140  			ClientCertAuth: &clientCertAuth{
   141  				CAKey:         caKey,
   142  				Organizations: []string{"system:nodes"},
   143  			},
   144  		},
   145  		"controller-manager.conf": {
   146  			CACert:     caCert,
   147  			APIServer:  controlPlaneEndpoint,
   148  			ClientName: "system:kube-controller-manager",
   149  			ClientCertAuth: &clientCertAuth{
   150  				CAKey: caKey,
   151  			},
   152  		},
   153  		"scheduler.conf": {
   154  			CACert:     caCert,
   155  			APIServer:  controlPlaneEndpoint,
   156  			ClientName: "system:kube-scheduler",
   157  			ClientCertAuth: &clientCertAuth{
   158  				CAKey: caKey,
   159  			},
   160  		},
   161  	}
   162  
   163  	return kubeConfigSpec, nil
   164  }
   165  
   166  // buildKubeConfigFromSpec creates a kubeconfig object for the given kubeConfigSpec
   167  func buildKubeConfigFromSpec(spec *kubeConfigSpec, clustername string) (*clientcmdapi.Config, error) {
   168  	// If this kubeconfig should use token
   169  	if spec.TokenAuth != nil {
   170  		// create a kubeconfig with a token
   171  		return CreateWithToken(
   172  			spec.APIServer,
   173  			clustername,
   174  			spec.ClientName,
   175  			EncodeCertPEM(spec.CACert),
   176  			spec.TokenAuth.Token,
   177  		), nil
   178  	}
   179  
   180  	// otherwise, create a client certs
   181  	clientCertConfig := Config{
   182  		CommonName:   spec.ClientName,
   183  		Organization: spec.ClientCertAuth.Organizations,
   184  		Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
   185  		Year:         100,
   186  	}
   187  
   188  	clientCert, clientKey, err := NewCaCertAndKeyFromRoot(clientCertConfig, spec.CACert, spec.ClientCertAuth.CAKey)
   189  	if err != nil {
   190  		return nil, errors.Wrapf(err, "failure while creating %s client certificate", spec.ClientName)
   191  	}
   192  
   193  	encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(clientKey)
   194  	if err != nil {
   195  		return nil, errors.Wrapf(err, "failed to marshal private key to PEM")
   196  	}
   197  	// create a kubeconfig with the client certs
   198  	return CreateWithCerts(
   199  		spec.APIServer,
   200  		clustername,
   201  		spec.ClientName,
   202  		EncodeCertPEM(spec.CACert),
   203  		encodedClientKey,
   204  		EncodeCertPEM(clientCert),
   205  	), nil
   206  }
   207  
   208  // validateKubeConfig check if the kubeconfig file exist and has the expected CA and server URL
   209  func validateKubeConfig(outDir, filename string, config *clientcmdapi.Config) error {
   210  	kubeConfigFilePath := filepath.Join(outDir, filename)
   211  
   212  	if _, err := os.Stat(kubeConfigFilePath); err != nil {
   213  		return err
   214  	}
   215  
   216  	// The kubeconfig already exists, let's check if it has got the same CA and server URL
   217  	currentConfig, err := clientcmd.LoadFromFile(kubeConfigFilePath)
   218  	if err != nil {
   219  		return errors.Wrapf(err, "failed to load kubeconfig file %s that already exists on disk", kubeConfigFilePath)
   220  	}
   221  
   222  	expectedCtx, exists := config.Contexts[config.CurrentContext]
   223  	if !exists {
   224  		return errors.Errorf("failed to find expected context %s", config.CurrentContext)
   225  	}
   226  	expectedCluster := expectedCtx.Cluster
   227  	currentCtx, exists := currentConfig.Contexts[currentConfig.CurrentContext]
   228  	if !exists {
   229  		return errors.Errorf("failed to find CurrentContext in Contexts of the kubeconfig file %s", kubeConfigFilePath)
   230  	}
   231  	currentCluster := currentCtx.Cluster
   232  	if currentConfig.Clusters[currentCluster] == nil {
   233  		return errors.Errorf("failed to find the given CurrentContext Cluster in Clusters of the kubeconfig file %s", kubeConfigFilePath)
   234  	}
   235  
   236  	// Make sure the compared CAs are whitespace-trimmed. The function clientcmd.LoadFromFile() just decodes
   237  	// the base64 CA and places it raw in the v1.Config object. In case the user has extra whitespace
   238  	// in the CA they used to create a kubeconfig this comparison to a generated v1.Config will otherwise fail.
   239  	caCurrent := bytes.TrimSpace(currentConfig.Clusters[currentCluster].CertificateAuthorityData)
   240  	caExpected := bytes.TrimSpace(config.Clusters[expectedCluster].CertificateAuthorityData)
   241  
   242  	// 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
   243  	if !bytes.Equal(caCurrent, caExpected) {
   244  		return errors.Errorf("a kubeconfig file %q exists already but has got the wrong CA cert", kubeConfigFilePath)
   245  	}
   246  	// 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
   247  	if currentConfig.Clusters[currentCluster].Server != config.Clusters[expectedCluster].Server {
   248  		return errors.Errorf("a kubeconfig file %q exists already but has got the wrong API Server URL", kubeConfigFilePath)
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  // createKubeConfigFileIfNotExists saves the KubeConfig object into a file if there isn't any file at the given path.
   255  // If there already is a kubeconfig file at the given path; kubeadm tries to load it and check if the values in the
   256  // existing and the expected config equals. If they do; kubeadm will just skip writing the file as it's up-to-date,
   257  // but if a file exists but has old content or isn't a kubeconfig file, this function returns an error.
   258  func createKubeConfigFileIfNotExists(outDir, filename string, config *clientcmdapi.Config) error {
   259  	kubeConfigFilePath := filepath.Join(outDir, filename)
   260  
   261  	err := validateKubeConfig(outDir, filename, config)
   262  	if err != nil {
   263  		// Check if the file exist, and if it doesn't, just write it to disk
   264  		if !os.IsNotExist(err) {
   265  			return err
   266  		}
   267  		logger.Info("[kubeconfig] Writing %q kubeconfig file\n", filename)
   268  		err = WriteToDisk(kubeConfigFilePath, config)
   269  		if err != nil {
   270  			return errors.Wrapf(err, "failed to save kubeconfig file %q on disk", kubeConfigFilePath)
   271  		}
   272  		return nil
   273  	}
   274  	// kubeadm doesn't validate the existing kubeconfig file more than this (kubeadm trusts the client certs to be valid)
   275  	// Basically, if we find a kubeconfig file with the same path; the same CA cert and the same server URL;
   276  	// kubeadm thinks those files are equal and doesn't bother writing a new file
   277  	logger.Info("[kubeconfig] Using existing kubeconfig file: %q\n", kubeConfigFilePath)
   278  
   279  	return nil
   280  }
   281  
   282  // WriteKubeConfigWithClientCert writes a kubeconfig file - with a client certificate as authentication info  - to the given writer.
   283  func WriteKubeConfigWithClientCert(out io.Writer, cfg Config, clientName, controlPlaneEndpoint, clusterName string, organizations []string) error {
   284  	// creates the KubeConfigSpecs, actualized for the current InitConfiguration
   285  	caCert, caKey, err := LoadCaCertAndKeyFromDisk(cfg)
   286  	if err != nil {
   287  		return errors.Wrap(err, "couldn't create a kubeconfig; the CA files couldn't be loaded")
   288  	}
   289  
   290  	if len(controlPlaneEndpoint) == 0 {
   291  		return errors.New("controlPlaneEndpoint  can not be empty")
   292  	}
   293  
   294  	spec := &kubeConfigSpec{
   295  		ClientName: clientName,
   296  		APIServer:  controlPlaneEndpoint,
   297  		CACert:     caCert,
   298  		ClientCertAuth: &clientCertAuth{
   299  			CAKey:         caKey,
   300  			Organizations: organizations,
   301  		},
   302  	}
   303  
   304  	return writeKubeConfigFromSpec(out, spec, clusterName)
   305  }
   306  
   307  // WriteKubeConfigWithToken writes a kubeconfig file - with a token as client authentication info - to the given writer.
   308  func WriteKubeConfigWithToken(out io.Writer, cfg Config, clientName, controlPlaneEndpoint, clusterName, token string) error {
   309  	// creates the KubeConfigSpecs, actualized for the current InitConfiguration
   310  	caCert, _, err := LoadCaCertAndKeyFromDisk(cfg)
   311  	if err != nil {
   312  		return errors.Wrap(err, "couldn't create a kubeconfig; the CA files couldn't be loaded")
   313  	}
   314  
   315  	if len(controlPlaneEndpoint) == 0 {
   316  		return errors.New("controlPlaneEndpoint  can not be empty")
   317  	}
   318  
   319  	spec := &kubeConfigSpec{
   320  		ClientName: clientName,
   321  		APIServer:  controlPlaneEndpoint,
   322  		CACert:     caCert,
   323  		TokenAuth: &tokenAuth{
   324  			Token: token,
   325  		},
   326  	}
   327  
   328  	return writeKubeConfigFromSpec(out, spec, clusterName)
   329  }
   330  
   331  // writeKubeConfigFromSpec creates a kubeconfig object from a kubeConfigSpec and writes it to the given writer.
   332  func writeKubeConfigFromSpec(out io.Writer, spec *kubeConfigSpec, clustername string) error {
   333  	// builds the KubeConfig object
   334  	config, err := buildKubeConfigFromSpec(spec, clustername)
   335  	if err != nil {
   336  		return err
   337  	}
   338  
   339  	// writes the kubeconfig to disk if it not exists
   340  	configBytes, err := clientcmd.Write(*config)
   341  	if err != nil {
   342  		return errors.Wrap(err, "failure while serializing admin kubeconfig")
   343  	}
   344  
   345  	fmt.Fprintln(out, string(configBytes))
   346  	return nil
   347  }
   348  
   349  // ValidateKubeconfigsForExternalCA check if the kubeconfig file exist and has the expected CA and server URL using kubeadmapi.InitConfiguration.
   350  func ValidateKubeconfigsForExternalCA(outDir string, cfg Config, controlPlaneEndpoint string) error {
   351  	kubeConfigFileNames := []string{
   352  		"admin.conf",
   353  		"kubelet.conf",
   354  		"controller-manager.conf",
   355  		"scheduler.conf",
   356  	}
   357  
   358  	// Creates a kubeconfig file with the target CA and server URL
   359  	// to be used as a input for validating user provided kubeconfig files
   360  	caCert, _, err := LoadCaCertAndKeyFromDisk(cfg)
   361  	if err != nil {
   362  		return err
   363  	}
   364  
   365  	if len(controlPlaneEndpoint) == 0 {
   366  		return errors.New("controlPlaneEndpoint  can not be empty")
   367  	}
   368  
   369  	validationConfig := CreateBasic(controlPlaneEndpoint, "dummy", "dummy", EncodeCertPEM(caCert))
   370  
   371  	// validate user provided kubeconfig files
   372  	for _, kubeConfigFileName := range kubeConfigFileNames {
   373  		if err = validateKubeConfig(outDir, kubeConfigFileName, validationConfig); err != nil {
   374  			return errors.Wrapf(err, "the %s file does not exists or it is not valid", kubeConfigFileName)
   375  		}
   376  	}
   377  	return nil
   378  }
   379  
   380  // cmd/kubeadm/app/util/kubeconfig/kubeconfig.go
   381  // CreateBasic creates a basic, general KubeConfig object that then can be extended
   382  func CreateBasic(serverURL, clusterName, userName string, caCert []byte) *clientcmdapi.Config {
   383  	// Use the cluster and the username as the context name
   384  	contextName := fmt.Sprintf("%s@%s", userName, clusterName)
   385  
   386  	return &clientcmdapi.Config{
   387  		Clusters: map[string]*clientcmdapi.Cluster{
   388  			clusterName: {
   389  				Server:                   serverURL,
   390  				CertificateAuthorityData: caCert,
   391  			},
   392  		},
   393  		Contexts: map[string]*clientcmdapi.Context{
   394  			contextName: {
   395  				Cluster:  clusterName,
   396  				AuthInfo: userName,
   397  			},
   398  		},
   399  		AuthInfos:      map[string]*clientcmdapi.AuthInfo{},
   400  		CurrentContext: contextName,
   401  	}
   402  }
   403  
   404  // cmd/kubeadm/app/util/kubeconfig/kubeconfig.go
   405  // CreateWithToken creates a KubeConfig object with access to the API server with a token
   406  func CreateWithToken(serverURL, clusterName, userName string, caCert []byte, token string) *clientcmdapi.Config {
   407  	config := CreateBasic(serverURL, clusterName, userName, caCert)
   408  	config.AuthInfos[userName] = &clientcmdapi.AuthInfo{
   409  		Token: token,
   410  	}
   411  	return config
   412  }
   413  
   414  // cmd/kubeadm/app/util/kubeconfig/kubeconfig.go
   415  // CreateWithCerts creates a KubeConfig object with access to the API server with client certificates
   416  func CreateWithCerts(serverURL, clusterName, userName string, caCert []byte, clientKey []byte, clientCert []byte) *clientcmdapi.Config {
   417  	config := CreateBasic(serverURL, clusterName, userName, caCert)
   418  	config.AuthInfos[userName] = &clientcmdapi.AuthInfo{
   419  		ClientKeyData:         clientKey,
   420  		ClientCertificateData: clientCert,
   421  	}
   422  	return config
   423  }
   424  
   425  // WriteToDisk writes a KubeConfig object down to disk with mode 0600
   426  func WriteToDisk(filename string, kubeconfig *clientcmdapi.Config) error {
   427  	err := clientcmd.WriteToFile(*kubeconfig, filename)
   428  	if err != nil {
   429  		return err
   430  	}
   431  
   432  	return nil
   433  }