k8s.io/client-go@v0.31.1/transport/cert_rotation.go (about)

     1  /*
     2  Copyright 2020 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 transport
    18  
    19  import (
    20  	"bytes"
    21  	"crypto/tls"
    22  	"fmt"
    23  	"reflect"
    24  	"sync"
    25  	"time"
    26  
    27  	utilnet "k8s.io/apimachinery/pkg/util/net"
    28  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  	"k8s.io/client-go/util/connrotation"
    31  	"k8s.io/client-go/util/workqueue"
    32  	"k8s.io/klog/v2"
    33  )
    34  
    35  const workItemKey = "key"
    36  
    37  // CertCallbackRefreshDuration is exposed so that integration tests can crank up the reload speed.
    38  var CertCallbackRefreshDuration = 5 * time.Minute
    39  
    40  type reloadFunc func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
    41  
    42  type dynamicClientCert struct {
    43  	clientCert *tls.Certificate
    44  	certMtx    sync.RWMutex
    45  
    46  	reload     reloadFunc
    47  	connDialer *connrotation.Dialer
    48  
    49  	// queue only ever has one item, but it has nice error handling backoff/retry semantics
    50  	queue workqueue.TypedRateLimitingInterface[string]
    51  }
    52  
    53  func certRotatingDialer(reload reloadFunc, dial utilnet.DialFunc) *dynamicClientCert {
    54  	d := &dynamicClientCert{
    55  		reload:     reload,
    56  		connDialer: connrotation.NewDialer(connrotation.DialFunc(dial)),
    57  		queue: workqueue.NewTypedRateLimitingQueueWithConfig(
    58  			workqueue.DefaultTypedControllerRateLimiter[string](),
    59  			workqueue.TypedRateLimitingQueueConfig[string]{Name: "DynamicClientCertificate"},
    60  		),
    61  	}
    62  
    63  	return d
    64  }
    65  
    66  // loadClientCert calls the callback and rotates connections if needed
    67  func (c *dynamicClientCert) loadClientCert() (*tls.Certificate, error) {
    68  	cert, err := c.reload(nil)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	// check to see if we have a change. If the values are the same, do nothing.
    74  	c.certMtx.RLock()
    75  	haveCert := c.clientCert != nil
    76  	if certsEqual(c.clientCert, cert) {
    77  		c.certMtx.RUnlock()
    78  		return c.clientCert, nil
    79  	}
    80  	c.certMtx.RUnlock()
    81  
    82  	c.certMtx.Lock()
    83  	c.clientCert = cert
    84  	c.certMtx.Unlock()
    85  
    86  	// The first certificate requested is not a rotation that is worth closing connections for
    87  	if !haveCert {
    88  		return cert, nil
    89  	}
    90  
    91  	klog.V(1).Infof("certificate rotation detected, shutting down client connections to start using new credentials")
    92  	c.connDialer.CloseAll()
    93  
    94  	return cert, nil
    95  }
    96  
    97  // certsEqual compares tls Certificates, ignoring the Leaf which may get filled in dynamically
    98  func certsEqual(left, right *tls.Certificate) bool {
    99  	if left == nil || right == nil {
   100  		return left == right
   101  	}
   102  
   103  	if !byteMatrixEqual(left.Certificate, right.Certificate) {
   104  		return false
   105  	}
   106  
   107  	if !reflect.DeepEqual(left.PrivateKey, right.PrivateKey) {
   108  		return false
   109  	}
   110  
   111  	if !byteMatrixEqual(left.SignedCertificateTimestamps, right.SignedCertificateTimestamps) {
   112  		return false
   113  	}
   114  
   115  	if !bytes.Equal(left.OCSPStaple, right.OCSPStaple) {
   116  		return false
   117  	}
   118  
   119  	return true
   120  }
   121  
   122  func byteMatrixEqual(left, right [][]byte) bool {
   123  	if len(left) != len(right) {
   124  		return false
   125  	}
   126  
   127  	for i := range left {
   128  		if !bytes.Equal(left[i], right[i]) {
   129  			return false
   130  		}
   131  	}
   132  	return true
   133  }
   134  
   135  // run starts the controller and blocks until stopCh is closed.
   136  func (c *dynamicClientCert) Run(stopCh <-chan struct{}) {
   137  	defer utilruntime.HandleCrash()
   138  	defer c.queue.ShutDown()
   139  
   140  	klog.V(3).Infof("Starting client certificate rotation controller")
   141  	defer klog.V(3).Infof("Shutting down client certificate rotation controller")
   142  
   143  	go wait.Until(c.runWorker, time.Second, stopCh)
   144  
   145  	go wait.PollImmediateUntil(CertCallbackRefreshDuration, func() (bool, error) {
   146  		c.queue.Add(workItemKey)
   147  		return false, nil
   148  	}, stopCh)
   149  
   150  	<-stopCh
   151  }
   152  
   153  func (c *dynamicClientCert) runWorker() {
   154  	for c.processNextWorkItem() {
   155  	}
   156  }
   157  
   158  func (c *dynamicClientCert) processNextWorkItem() bool {
   159  	dsKey, quit := c.queue.Get()
   160  	if quit {
   161  		return false
   162  	}
   163  	defer c.queue.Done(dsKey)
   164  
   165  	_, err := c.loadClientCert()
   166  	if err == nil {
   167  		c.queue.Forget(dsKey)
   168  		return true
   169  	}
   170  
   171  	utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
   172  	c.queue.AddRateLimited(dsKey)
   173  
   174  	return true
   175  }
   176  
   177  func (c *dynamicClientCert) GetClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
   178  	return c.loadClientCert()
   179  }