k8s.io/kubernetes@v1.29.3/pkg/kubelet/certificate/transport.go (about)

     1  /*
     2  Copyright 2017 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 certificate
    18  
    19  import (
    20  	"crypto/tls"
    21  	"fmt"
    22  	"net"
    23  	"net/http"
    24  	"os"
    25  	"sync"
    26  	"sync/atomic"
    27  	"time"
    28  
    29  	"k8s.io/klog/v2"
    30  
    31  	utilnet "k8s.io/apimachinery/pkg/util/net"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	restclient "k8s.io/client-go/rest"
    34  	"k8s.io/client-go/util/certificate"
    35  	"k8s.io/client-go/util/connrotation"
    36  )
    37  
    38  // UpdateTransport instruments a restconfig with a transport that dynamically uses
    39  // certificates provided by the manager for TLS client auth.
    40  //
    41  // The config must not already provide an explicit transport.
    42  //
    43  // The returned function allows forcefully closing all active connections.
    44  //
    45  // The returned transport periodically checks the manager to determine if the
    46  // certificate has changed. If it has, the transport shuts down all existing client
    47  // connections, forcing the client to re-handshake with the server and use the
    48  // new certificate.
    49  //
    50  // The exitAfter duration, if set, will terminate the current process if a certificate
    51  // is not available from the store (because it has been deleted on disk or is corrupt)
    52  // or if the certificate has expired and the server is responsive. This allows the
    53  // process parent or the bootstrap credentials an opportunity to retrieve a new initial
    54  // certificate.
    55  //
    56  // stopCh should be used to indicate when the transport is unused and doesn't need
    57  // to continue checking the manager.
    58  func UpdateTransport(stopCh <-chan struct{}, clientConfig *restclient.Config, clientCertificateManager certificate.Manager, exitAfter time.Duration) (func(), error) {
    59  	return updateTransport(stopCh, 10*time.Second, clientConfig, clientCertificateManager, exitAfter)
    60  }
    61  
    62  // updateTransport is an internal method that exposes how often this method checks that the
    63  // client cert has changed.
    64  func updateTransport(stopCh <-chan struct{}, period time.Duration, clientConfig *restclient.Config, clientCertificateManager certificate.Manager, exitAfter time.Duration) (func(), error) {
    65  	if clientConfig.Transport != nil || clientConfig.Dial != nil {
    66  		return nil, fmt.Errorf("there is already a transport or dialer configured")
    67  	}
    68  
    69  	d := connrotation.NewDialer((&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext)
    70  
    71  	if clientCertificateManager != nil {
    72  		if err := addCertRotation(stopCh, period, clientConfig, clientCertificateManager, exitAfter, d); err != nil {
    73  			return nil, err
    74  		}
    75  	} else {
    76  		clientConfig.Dial = d.DialContext
    77  	}
    78  
    79  	return d.CloseAll, nil
    80  }
    81  
    82  func addCertRotation(stopCh <-chan struct{}, period time.Duration, clientConfig *restclient.Config, clientCertificateManager certificate.Manager, exitAfter time.Duration, d *connrotation.Dialer) error {
    83  	tlsConfig, err := restclient.TLSConfigFor(clientConfig)
    84  	if err != nil {
    85  		return fmt.Errorf("unable to configure TLS for the rest client: %v", err)
    86  	}
    87  	if tlsConfig == nil {
    88  		tlsConfig = &tls.Config{}
    89  	}
    90  
    91  	tlsConfig.Certificates = nil
    92  	tlsConfig.GetClientCertificate = func(requestInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) {
    93  		cert := clientCertificateManager.Current()
    94  		if cert == nil {
    95  			return &tls.Certificate{Certificate: nil}, nil
    96  		}
    97  		return cert, nil
    98  	}
    99  
   100  	lastCertAvailable := time.Now()
   101  	lastCert := clientCertificateManager.Current()
   102  
   103  	var hasCert atomic.Bool
   104  	hasCert.Store(lastCert != nil)
   105  
   106  	checkLock := &sync.Mutex{}
   107  	checkNewCertificateAndRotate := func() {
   108  		// don't run concurrently
   109  		checkLock.Lock()
   110  		defer checkLock.Unlock()
   111  
   112  		curr := clientCertificateManager.Current()
   113  
   114  		if exitAfter > 0 {
   115  			now := time.Now()
   116  			if curr == nil {
   117  				// the certificate has been deleted from disk or is otherwise corrupt
   118  				if now.After(lastCertAvailable.Add(exitAfter)) {
   119  					if clientCertificateManager.ServerHealthy() {
   120  						klog.ErrorS(nil, "No valid client certificate is found and the server is responsive, exiting.", "lastCertificateAvailabilityTime", lastCertAvailable, "shutdownThreshold", exitAfter)
   121  						os.Exit(1)
   122  					} else {
   123  						klog.ErrorS(nil, "No valid client certificate is found but the server is not responsive. A restart may be necessary to retrieve new initial credentials.", "lastCertificateAvailabilityTime", lastCertAvailable, "shutdownThreshold", exitAfter)
   124  					}
   125  				}
   126  			} else {
   127  				// the certificate is expired
   128  				if now.After(curr.Leaf.NotAfter) {
   129  					if clientCertificateManager.ServerHealthy() {
   130  						klog.ErrorS(nil, "The currently active client certificate has expired and the server is responsive, exiting.")
   131  						os.Exit(1)
   132  					} else {
   133  						klog.ErrorS(nil, "The currently active client certificate has expired, but the server is not responsive. A restart may be necessary to retrieve new initial credentials.")
   134  					}
   135  				}
   136  				lastCertAvailable = now
   137  			}
   138  		}
   139  
   140  		if curr == nil || lastCert == curr {
   141  			// Cert hasn't been rotated.
   142  			return
   143  		}
   144  		lastCert = curr
   145  		hasCert.Store(lastCert != nil)
   146  
   147  		klog.InfoS("Certificate rotation detected, shutting down client connections to start using new credentials")
   148  		// The cert has been rotated. Close all existing connections to force the client
   149  		// to reperform its TLS handshake with new cert.
   150  		//
   151  		// See: https://github.com/kubernetes-incubator/bootkube/pull/663#issuecomment-318506493
   152  		d.CloseAll()
   153  	}
   154  
   155  	// start long-term check
   156  	go wait.Until(checkNewCertificateAndRotate, period, stopCh)
   157  
   158  	if !hasCert.Load() {
   159  		// start a faster check until we get the initial certificate
   160  		go wait.PollUntil(time.Second, func() (bool, error) {
   161  			checkNewCertificateAndRotate()
   162  			return hasCert.Load(), nil
   163  		}, stopCh)
   164  	}
   165  
   166  	clientConfig.Transport = utilnet.SetTransportDefaults(&http.Transport{
   167  		Proxy:               http.ProxyFromEnvironment,
   168  		TLSHandshakeTimeout: 10 * time.Second,
   169  		TLSClientConfig:     tlsConfig,
   170  		MaxIdleConnsPerHost: 25,
   171  		DialContext:         d.DialContext,
   172  	})
   173  
   174  	// Zero out all existing TLS options since our new transport enforces them.
   175  	clientConfig.CertData = nil
   176  	clientConfig.KeyData = nil
   177  	clientConfig.CertFile = ""
   178  	clientConfig.KeyFile = ""
   179  	clientConfig.CAData = nil
   180  	clientConfig.CAFile = ""
   181  	clientConfig.Insecure = false
   182  	clientConfig.NextProtos = nil
   183  
   184  	return nil
   185  }