k8s.io/kubernetes@v1.29.3/pkg/kubelet/certificate/bootstrap/bootstrap.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package bootstrap 18 19 import ( 20 "context" 21 "crypto" 22 "crypto/rsa" 23 "crypto/sha512" 24 "crypto/x509" 25 "crypto/x509/pkix" 26 "encoding/base64" 27 "errors" 28 "fmt" 29 "os" 30 "path/filepath" 31 "time" 32 33 "k8s.io/klog/v2" 34 35 certificatesv1 "k8s.io/api/certificates/v1" 36 "k8s.io/apimachinery/pkg/types" 37 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 38 "k8s.io/apimachinery/pkg/util/wait" 39 clientset "k8s.io/client-go/kubernetes" 40 "k8s.io/client-go/kubernetes/scheme" 41 restclient "k8s.io/client-go/rest" 42 "k8s.io/client-go/tools/clientcmd" 43 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 44 "k8s.io/client-go/transport" 45 certutil "k8s.io/client-go/util/cert" 46 "k8s.io/client-go/util/certificate" 47 "k8s.io/client-go/util/certificate/csr" 48 "k8s.io/client-go/util/keyutil" 49 ) 50 51 const tmpPrivateKeyFile = "kubelet-client.key.tmp" 52 53 // LoadClientConfig tries to load the appropriate client config for retrieving certs and for use by users. 54 // If bootstrapPath is empty, only kubeconfigPath is checked. If bootstrap path is set and the contents 55 // of kubeconfigPath are valid, both certConfig and userConfig will point to that file. Otherwise the 56 // kubeconfigPath on disk is populated based on bootstrapPath but pointing to the location of the client cert 57 // in certDir. This preserves the historical behavior of bootstrapping where on subsequent restarts the 58 // most recent client cert is used to request new client certs instead of the initial token. 59 func LoadClientConfig(kubeconfigPath, bootstrapPath, certDir string) (certConfig, userConfig *restclient.Config, err error) { 60 if len(bootstrapPath) == 0 { 61 clientConfig, err := loadRESTClientConfig(kubeconfigPath) 62 if err != nil { 63 return nil, nil, fmt.Errorf("unable to load kubeconfig: %v", err) 64 } 65 klog.V(2).InfoS("No bootstrapping requested, will use kubeconfig") 66 return clientConfig, restclient.CopyConfig(clientConfig), nil 67 } 68 69 store, err := certificate.NewFileStore("kubelet-client", certDir, certDir, "", "") 70 if err != nil { 71 return nil, nil, fmt.Errorf("unable to build bootstrap cert store") 72 } 73 74 ok, err := isClientConfigStillValid(kubeconfigPath) 75 if err != nil { 76 return nil, nil, err 77 } 78 79 // use the current client config 80 if ok { 81 clientConfig, err := loadRESTClientConfig(kubeconfigPath) 82 if err != nil { 83 return nil, nil, fmt.Errorf("unable to load kubeconfig: %v", err) 84 } 85 klog.V(2).InfoS("Current kubeconfig file contents are still valid, no bootstrap necessary") 86 return clientConfig, restclient.CopyConfig(clientConfig), nil 87 } 88 89 bootstrapClientConfig, err := loadRESTClientConfig(bootstrapPath) 90 if err != nil { 91 return nil, nil, fmt.Errorf("unable to load bootstrap kubeconfig: %v", err) 92 } 93 94 clientConfig := restclient.AnonymousClientConfig(bootstrapClientConfig) 95 pemPath := store.CurrentPath() 96 clientConfig.KeyFile = pemPath 97 clientConfig.CertFile = pemPath 98 if err := writeKubeconfigFromBootstrapping(clientConfig, kubeconfigPath, pemPath); err != nil { 99 return nil, nil, err 100 } 101 klog.V(2).InfoS("Use the bootstrap credentials to request a cert, and set kubeconfig to point to the certificate dir") 102 return bootstrapClientConfig, clientConfig, nil 103 } 104 105 // LoadClientCert requests a client cert for kubelet if the kubeconfigPath file does not exist. 106 // The kubeconfig at bootstrapPath is used to request a client certificate from the API server. 107 // On success, a kubeconfig file referencing the generated key and obtained certificate is written to kubeconfigPath. 108 // The certificate and key file are stored in certDir. 109 func LoadClientCert(ctx context.Context, kubeconfigPath, bootstrapPath, certDir string, nodeName types.NodeName) error { 110 // Short-circuit if the kubeconfig file exists and is valid. 111 ok, err := isClientConfigStillValid(kubeconfigPath) 112 if err != nil { 113 return err 114 } 115 if ok { 116 klog.V(2).InfoS("Kubeconfig exists and is valid, skipping bootstrap", "path", kubeconfigPath) 117 return nil 118 } 119 120 klog.V(2).InfoS("Using bootstrap kubeconfig to generate TLS client cert, key and kubeconfig file") 121 122 bootstrapClientConfig, err := loadRESTClientConfig(bootstrapPath) 123 if err != nil { 124 return fmt.Errorf("unable to load bootstrap kubeconfig: %v", err) 125 } 126 127 bootstrapClient, err := clientset.NewForConfig(bootstrapClientConfig) 128 if err != nil { 129 return fmt.Errorf("unable to create certificates signing request client: %v", err) 130 } 131 132 store, err := certificate.NewFileStore("kubelet-client", certDir, certDir, "", "") 133 if err != nil { 134 return fmt.Errorf("unable to build bootstrap cert store") 135 } 136 137 var keyData []byte 138 if cert, err := store.Current(); err == nil { 139 if cert.PrivateKey != nil { 140 keyData, err = keyutil.MarshalPrivateKeyToPEM(cert.PrivateKey) 141 if err != nil { 142 keyData = nil 143 } 144 } 145 } 146 // Cache the private key in a separate file until CSR succeeds. This has to 147 // be a separate file because store.CurrentPath() points to a symlink 148 // managed by the store. 149 privKeyPath := filepath.Join(certDir, tmpPrivateKeyFile) 150 if !verifyKeyData(keyData) { 151 klog.V(2).InfoS("No valid private key and/or certificate found, reusing existing private key or creating a new one") 152 // Note: always call LoadOrGenerateKeyFile so that private key is 153 // reused on next startup if CSR request fails. 154 keyData, _, err = keyutil.LoadOrGenerateKeyFile(privKeyPath) 155 if err != nil { 156 return err 157 } 158 } 159 160 if err := waitForServer(ctx, *bootstrapClientConfig, 1*time.Minute); err != nil { 161 klog.InfoS("Error waiting for apiserver to come up", "err", err) 162 } 163 164 certData, err := requestNodeCertificate(ctx, bootstrapClient, keyData, nodeName) 165 if err != nil { 166 return err 167 } 168 if _, err := store.Update(certData, keyData); err != nil { 169 return err 170 } 171 if err := os.Remove(privKeyPath); err != nil && !os.IsNotExist(err) { 172 klog.V(2).InfoS("Failed cleaning up private key file", "path", privKeyPath, "err", err) 173 } 174 175 return writeKubeconfigFromBootstrapping(bootstrapClientConfig, kubeconfigPath, store.CurrentPath()) 176 } 177 178 func writeKubeconfigFromBootstrapping(bootstrapClientConfig *restclient.Config, kubeconfigPath, pemPath string) error { 179 // Get the CA data from the bootstrap client config. 180 caFile, caData := bootstrapClientConfig.CAFile, []byte{} 181 if len(caFile) == 0 { 182 caData = bootstrapClientConfig.CAData 183 } 184 185 // Build resulting kubeconfig. 186 kubeconfigData := clientcmdapi.Config{ 187 // Define a cluster stanza based on the bootstrap kubeconfig. 188 Clusters: map[string]*clientcmdapi.Cluster{"default-cluster": { 189 Server: bootstrapClientConfig.Host, 190 InsecureSkipTLSVerify: bootstrapClientConfig.Insecure, 191 CertificateAuthority: caFile, 192 CertificateAuthorityData: caData, 193 }}, 194 // Define auth based on the obtained client cert. 195 AuthInfos: map[string]*clientcmdapi.AuthInfo{"default-auth": { 196 ClientCertificate: pemPath, 197 ClientKey: pemPath, 198 }}, 199 // Define a context that connects the auth info and cluster, and set it as the default 200 Contexts: map[string]*clientcmdapi.Context{"default-context": { 201 Cluster: "default-cluster", 202 AuthInfo: "default-auth", 203 Namespace: "default", 204 }}, 205 CurrentContext: "default-context", 206 } 207 208 // Marshal to disk 209 return clientcmd.WriteToFile(kubeconfigData, kubeconfigPath) 210 } 211 212 func loadRESTClientConfig(kubeconfig string) (*restclient.Config, error) { 213 // Load structured kubeconfig data from the given path. 214 loader := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig} 215 loadedConfig, err := loader.Load() 216 if err != nil { 217 return nil, err 218 } 219 // Flatten the loaded data to a particular restclient.Config based on the current context. 220 return clientcmd.NewNonInteractiveClientConfig( 221 *loadedConfig, 222 loadedConfig.CurrentContext, 223 &clientcmd.ConfigOverrides{}, 224 loader, 225 ).ClientConfig() 226 } 227 228 // isClientConfigStillValid checks the provided kubeconfig to see if it has a valid 229 // client certificate. It returns true if the kubeconfig is valid, or an error if bootstrapping 230 // should stop immediately. 231 func isClientConfigStillValid(kubeconfigPath string) (bool, error) { 232 _, err := os.Stat(kubeconfigPath) 233 if os.IsNotExist(err) { 234 return false, nil 235 } 236 if err != nil { 237 return false, fmt.Errorf("error reading existing bootstrap kubeconfig %s: %v", kubeconfigPath, err) 238 } 239 bootstrapClientConfig, err := loadRESTClientConfig(kubeconfigPath) 240 if err != nil { 241 utilruntime.HandleError(fmt.Errorf("unable to read existing bootstrap client config from %s: %v", kubeconfigPath, err)) 242 return false, nil 243 } 244 transportConfig, err := bootstrapClientConfig.TransportConfig() 245 if err != nil { 246 utilruntime.HandleError(fmt.Errorf("unable to load transport configuration from existing bootstrap client config read from %s: %v", kubeconfigPath, err)) 247 return false, nil 248 } 249 // has side effect of populating transport config data fields 250 if _, err := transport.TLSConfigFor(transportConfig); err != nil { 251 utilruntime.HandleError(fmt.Errorf("unable to load TLS configuration from existing bootstrap client config read from %s: %v", kubeconfigPath, err)) 252 return false, nil 253 } 254 certs, err := certutil.ParseCertsPEM(transportConfig.TLS.CertData) 255 if err != nil { 256 utilruntime.HandleError(fmt.Errorf("unable to load TLS certificates from existing bootstrap client config read from %s: %v", kubeconfigPath, err)) 257 return false, nil 258 } 259 if len(certs) == 0 { 260 utilruntime.HandleError(fmt.Errorf("unable to read TLS certificates from existing bootstrap client config read from %s: %v", kubeconfigPath, err)) 261 return false, nil 262 } 263 now := time.Now() 264 for _, cert := range certs { 265 if now.After(cert.NotAfter) { 266 utilruntime.HandleError(fmt.Errorf("part of the existing bootstrap client certificate in %s is expired: %v", kubeconfigPath, cert.NotAfter)) 267 return false, nil 268 } 269 } 270 return true, nil 271 } 272 273 // verifyKeyData returns true if the provided data appears to be a valid private key. 274 func verifyKeyData(data []byte) bool { 275 if len(data) == 0 { 276 return false 277 } 278 _, err := keyutil.ParsePrivateKeyPEM(data) 279 return err == nil 280 } 281 282 func waitForServer(ctx context.Context, cfg restclient.Config, deadline time.Duration) error { 283 cfg.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 284 cfg.Timeout = 1 * time.Second 285 cli, err := restclient.UnversionedRESTClientFor(&cfg) 286 if err != nil { 287 return fmt.Errorf("couldn't create client: %v", err) 288 } 289 290 ctx, cancel := context.WithTimeout(ctx, deadline) 291 defer cancel() 292 293 var connected bool 294 wait.JitterUntil(func() { 295 if _, err := cli.Get().AbsPath("/healthz").Do(ctx).Raw(); err != nil { 296 klog.InfoS("Failed to connect to apiserver", "err", err) 297 return 298 } 299 cancel() 300 connected = true 301 }, 2*time.Second, 0.2, true, ctx.Done()) 302 303 if !connected { 304 return errors.New("timed out waiting to connect to apiserver") 305 } 306 return nil 307 } 308 309 // requestNodeCertificate will create a certificate signing request for a node 310 // (Organization and CommonName for the CSR will be set as expected for node 311 // certificates) and send it to API server, then it will watch the object's 312 // status, once approved by API server, it will return the API server's issued 313 // certificate (pem-encoded). If there is any errors, or the watch timeouts, it 314 // will return an error. This is intended for use on nodes (kubelet and 315 // kubeadm). 316 func requestNodeCertificate(ctx context.Context, client clientset.Interface, privateKeyData []byte, nodeName types.NodeName) (certData []byte, err error) { 317 subject := &pkix.Name{ 318 Organization: []string{"system:nodes"}, 319 CommonName: "system:node:" + string(nodeName), 320 } 321 322 privateKey, err := keyutil.ParsePrivateKeyPEM(privateKeyData) 323 if err != nil { 324 return nil, fmt.Errorf("invalid private key for certificate request: %v", err) 325 } 326 csrData, err := certutil.MakeCSR(privateKey, subject, nil, nil) 327 if err != nil { 328 return nil, fmt.Errorf("unable to generate certificate request: %v", err) 329 } 330 331 usages := []certificatesv1.KeyUsage{ 332 certificatesv1.UsageDigitalSignature, 333 certificatesv1.UsageClientAuth, 334 } 335 if _, ok := privateKey.(*rsa.PrivateKey); ok { 336 usages = append(usages, certificatesv1.UsageKeyEncipherment) 337 } 338 339 // The Signer interface contains the Public() method to get the public key. 340 signer, ok := privateKey.(crypto.Signer) 341 if !ok { 342 return nil, fmt.Errorf("private key does not implement crypto.Signer") 343 } 344 345 name, err := digestedName(signer.Public(), subject, usages) 346 if err != nil { 347 return nil, err 348 } 349 350 reqName, reqUID, err := csr.RequestCertificate(client, csrData, name, certificatesv1.KubeAPIServerClientKubeletSignerName, nil, usages, privateKey) 351 if err != nil { 352 return nil, err 353 } 354 355 ctx, cancel := context.WithTimeout(ctx, 3600*time.Second) 356 defer cancel() 357 358 klog.V(2).InfoS("Waiting for client certificate to be issued") 359 return csr.WaitForCertificate(ctx, client, reqName, reqUID) 360 } 361 362 // This digest should include all the relevant pieces of the CSR we care about. 363 // We can't directly hash the serialized CSR because of random padding that we 364 // regenerate every loop and we include usages which are not contained in the 365 // CSR. This needs to be kept up to date as we add new fields to the node 366 // certificates and with ensureCompatible. 367 func digestedName(publicKey interface{}, subject *pkix.Name, usages []certificatesv1.KeyUsage) (string, error) { 368 hash := sha512.New512_256() 369 370 // Here we make sure two different inputs can't write the same stream 371 // to the hash. This delimiter is not in the base64.URLEncoding 372 // alphabet so there is no way to have spill over collisions. Without 373 // it 'CN:foo,ORG:bar' hashes to the same value as 'CN:foob,ORG:ar' 374 const delimiter = '|' 375 encode := base64.RawURLEncoding.EncodeToString 376 377 write := func(data []byte) { 378 hash.Write([]byte(encode(data))) 379 hash.Write([]byte{delimiter}) 380 } 381 382 publicKeyData, err := x509.MarshalPKIXPublicKey(publicKey) 383 if err != nil { 384 return "", err 385 } 386 write(publicKeyData) 387 388 write([]byte(subject.CommonName)) 389 for _, v := range subject.Organization { 390 write([]byte(v)) 391 } 392 for _, v := range usages { 393 write([]byte(v)) 394 } 395 396 return fmt.Sprintf("node-csr-%s", encode(hash.Sum(nil))), nil 397 }