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 }