istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/pki/ca/selfsignedcarootcertrotator.go (about)

     1  // Copyright Istio 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 ca
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"math/rand"
    21  	"time"
    22  
    23  	v1 "k8s.io/api/core/v1"
    24  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    25  
    26  	"istio.io/istio/pkg/log"
    27  	"istio.io/istio/security/pkg/k8s/controller"
    28  	"istio.io/istio/security/pkg/pki/util"
    29  	certutil "istio.io/istio/security/pkg/util"
    30  )
    31  
    32  var rootCertRotatorLog = log.RegisterScope("rootcertrotator", "Self-signed CA root cert rotator log")
    33  
    34  type SelfSignedCARootCertRotatorConfig struct {
    35  	certInspector      certutil.CertUtil
    36  	caStorageNamespace string
    37  	org                string
    38  	rootCertFile       string
    39  	secretName         string
    40  	client             corev1.CoreV1Interface
    41  	CheckInterval      time.Duration
    42  	caCertTTL          time.Duration
    43  	retryInterval      time.Duration
    44  	retryMax           time.Duration
    45  	dualUse            bool
    46  	enableJitter       bool
    47  }
    48  
    49  // SelfSignedCARootCertRotator automatically checks self-signed signing root
    50  // certificate and rotates root certificate if it is going to expire.
    51  type SelfSignedCARootCertRotator struct {
    52  	caSecretController *controller.CaSecretController
    53  	config             *SelfSignedCARootCertRotatorConfig
    54  	backOffTime        time.Duration
    55  	ca                 *IstioCA
    56  	onRootCertUpdate   func() error
    57  }
    58  
    59  // NewSelfSignedCARootCertRotator returns a new root cert rotator instance that
    60  // rotates self-signed root cert periodically.
    61  // nolint: gosec
    62  // Not security sensitive code
    63  func NewSelfSignedCARootCertRotator(config *SelfSignedCARootCertRotatorConfig,
    64  	ca *IstioCA,
    65  	onRootCertUpdate func() error,
    66  ) *SelfSignedCARootCertRotator {
    67  	rotator := &SelfSignedCARootCertRotator{
    68  		caSecretController: controller.NewCaSecretController(config.client),
    69  		config:             config,
    70  		ca:                 ca,
    71  		onRootCertUpdate:   onRootCertUpdate,
    72  	}
    73  	if config.enableJitter {
    74  		// Select a back off time in seconds, which is in the range of [0, rotator.config.CheckInterval).
    75  		randSource := rand.NewSource(time.Now().UnixNano())
    76  		randBackOff := rand.New(randSource)
    77  		backOffSeconds := int(time.Duration(randBackOff.Int63n(int64(rotator.config.CheckInterval))).Seconds())
    78  		rotator.backOffTime = time.Duration(backOffSeconds) * time.Second
    79  		rootCertRotatorLog.Infof("Set up back off time %s to start rotator.", rotator.backOffTime.String())
    80  	} else {
    81  		rotator.backOffTime = time.Duration(0)
    82  	}
    83  	return rotator
    84  }
    85  
    86  // Run refreshes root certs and updates config map accordingly.
    87  func (rotator *SelfSignedCARootCertRotator) Run(stopCh chan struct{}) {
    88  	if rotator.config.enableJitter {
    89  		rootCertRotatorLog.Infof("Jitter is enabled, wait %s before "+
    90  			"starting root cert rotator.", rotator.backOffTime.String())
    91  		select {
    92  		case <-time.After(rotator.backOffTime):
    93  			rootCertRotatorLog.Infof("Jitter complete, start rotator.")
    94  		case <-stopCh:
    95  			rootCertRotatorLog.Info("Received stop signal, so stop the root cert rotator.")
    96  			return
    97  		}
    98  	}
    99  	ticker := time.NewTicker(rotator.config.CheckInterval)
   100  	for {
   101  		select {
   102  		case <-ticker.C:
   103  			rootCertRotatorLog.Info("Check and rotate root cert.")
   104  			rotator.checkAndRotateRootCert()
   105  		case _, ok := <-stopCh:
   106  			if !ok {
   107  				rootCertRotatorLog.Info("Received stop signal, so stop the root cert rotator.")
   108  				if ticker != nil {
   109  					ticker.Stop()
   110  				}
   111  				return
   112  			}
   113  		}
   114  	}
   115  }
   116  
   117  // checkAndRotateRootCert decides whether root cert should be refreshed, and rotates
   118  // root cert for self-signed Citadel.
   119  func (rotator *SelfSignedCARootCertRotator) checkAndRotateRootCert() {
   120  	caSecret, scrtErr := rotator.caSecretController.LoadCASecretWithRetry(rotator.config.secretName,
   121  		rotator.config.caStorageNamespace, rotator.config.retryInterval, rotator.config.retryMax)
   122  
   123  	if scrtErr != nil {
   124  		rootCertRotatorLog.Errorf("Fail to load CA secret %s:%s (error: %s), skip cert rotation job",
   125  			rotator.config.caStorageNamespace, rotator.config.secretName, scrtErr.Error())
   126  	} else {
   127  		rotator.checkAndRotateRootCertForSigningCertCitadel(caSecret)
   128  	}
   129  }
   130  
   131  // checkAndRotateRootCertForSigningCertCitadel checks root cert secret and rotates
   132  // root cert if the current one is about to expire. The rotation uses existing
   133  // root private key to generate a new root cert, and updates root cert secret.
   134  func (rotator *SelfSignedCARootCertRotator) checkAndRotateRootCertForSigningCertCitadel(
   135  	caSecret *v1.Secret,
   136  ) {
   137  	if caSecret == nil {
   138  		rootCertRotatorLog.Errorf("root cert secret %s is nil, skip cert rotation job",
   139  			rotator.config.secretName)
   140  		return
   141  	}
   142  	// Check root certificate expiration time in CA secret
   143  	waitTime, err := rotator.config.certInspector.GetWaitTime(caSecret.Data[CACertFile], time.Now())
   144  	if err == nil && waitTime > 0 {
   145  		rootCertRotatorLog.Info("Root cert is not about to expire, skipping root cert rotation.")
   146  		caCertInMem, _, _, _ := rotator.ca.GetCAKeyCertBundle().GetAllPem()
   147  		// If CA certificate is different from the CA certificate in local key
   148  		// cert bundle, it implies that other Citadels have updated istio-ca-secret or cacerts.
   149  		// Reload root certificate into key cert bundle.
   150  		if !bytes.Equal(caCertInMem, caSecret.Data[CACertFile]) {
   151  			rootCertRotatorLog.Warnf("CA cert in KeyCertBundle does not match CA cert in "+
   152  				"%s. Start to reload root cert into KeyCertBundle", rotator.config.secretName)
   153  			rootCerts, err := util.AppendRootCerts(caSecret.Data[CACertFile], rotator.config.rootCertFile)
   154  			if err != nil {
   155  				rootCertRotatorLog.Errorf("failed to append root certificates from file: %s", err.Error())
   156  				return
   157  			}
   158  
   159  			if err := rotator.ca.GetCAKeyCertBundle().VerifyAndSetAll(caSecret.Data[CACertFile],
   160  				caSecret.Data[CAPrivateKeyFile], nil, rootCerts); err != nil {
   161  				rootCertRotatorLog.Errorf("failed to reload root cert into KeyCertBundle (%v)", err)
   162  			} else {
   163  				rootCertRotatorLog.Info("Successfully reloaded root cert into KeyCertBundle.")
   164  			}
   165  			if rotator.onRootCertUpdate != nil {
   166  				_ = rotator.onRootCertUpdate()
   167  			}
   168  		}
   169  		return
   170  	}
   171  
   172  	rootCertRotatorLog.Infof("Refresh root certificate, root cert is about to expire: %s", err.Error())
   173  
   174  	oldCertOptions, err := util.GetCertOptionsFromExistingCert(caSecret.Data[CACertFile])
   175  	if err != nil {
   176  		rootCertRotatorLog.Warnf("Failed to generate cert options from existing root certificate (%v), "+
   177  			"new root certificate may not match old root certificate", err)
   178  	}
   179  	options := util.CertOptions{
   180  		TTL:           rotator.config.caCertTTL,
   181  		SignerPrivPem: caSecret.Data[CAPrivateKeyFile],
   182  		Org:           rotator.config.org,
   183  		IsCA:          true,
   184  		IsSelfSigned:  true,
   185  		RSAKeySize:    rotator.ca.caRSAKeySize,
   186  		IsDualUse:     rotator.config.dualUse,
   187  	}
   188  	// options should be consistent with the one used in NewSelfSignedIstioCAOptions().
   189  	// This is to make sure when rotate the root cert, we don't make unnecessary changes
   190  	// to the certificate or add extra fields to the certificate.
   191  	options = util.MergeCertOptions(options, oldCertOptions)
   192  	pemCert, pemKey, ckErr := util.GenRootCertFromExistingKey(options)
   193  	if ckErr != nil {
   194  		rootCertRotatorLog.Errorf("unable to generate CA cert and key for self-signed CA: %s", ckErr.Error())
   195  		return
   196  	}
   197  
   198  	pemRootCerts, err := util.AppendRootCerts(pemCert, rotator.config.rootCertFile)
   199  	if err != nil {
   200  		rootCertRotatorLog.Errorf("failed to append root certificates: %s", err.Error())
   201  		return
   202  	}
   203  
   204  	oldCaCert := caSecret.Data[CACertFile]
   205  	oldCaPrivateKey := caSecret.Data[CAPrivateKeyFile]
   206  	oldRootCerts := rotator.ca.GetCAKeyCertBundle().GetRootCertPem()
   207  	if rollback, err := rotator.updateRootCertificate(caSecret, true, pemCert, pemKey, pemRootCerts); err != nil {
   208  		if !rollback {
   209  			rootCertRotatorLog.Errorf("Failed to roll forward root certificate (error: %s). "+
   210  				"Abort new root certificate", err.Error())
   211  			return
   212  		}
   213  		// caSecret is out-of-date. Need to load the latest istio-ca-secret to roll back root certificate.
   214  		_, err = rotator.updateRootCertificate(nil, false, oldCaCert, oldCaPrivateKey, oldRootCerts)
   215  		if err != nil {
   216  			rootCertRotatorLog.Errorf("Failed to roll backward root certificate (error: %s).", err.Error())
   217  		}
   218  		return
   219  	}
   220  	rootCertRotatorLog.Info("Root certificate rotation is completed successfully.")
   221  }
   222  
   223  // updateRootCertificate updates root certificate in istio-ca-secret, keycertbundle and configmap. It takes a scrt
   224  // object, cert, and key, and a flag rollForward indicating whether this update is to roll forward root certificate or
   225  // to roll backward.
   226  // updateRootCertificate returns error when any step is failed, and a flag indicating whether a rollback is required.
   227  // Only when rollForward is true and failure happens, the returned rollback flag is true.
   228  func (rotator *SelfSignedCARootCertRotator) updateRootCertificate(caSecret *v1.Secret, rollForward bool, cert, key, rootCert []byte) (bool, error) {
   229  	var err error
   230  	if caSecret == nil {
   231  		caSecret, err = rotator.caSecretController.LoadCASecretWithRetry(rotator.config.secretName,
   232  			rotator.config.caStorageNamespace, rotator.config.retryInterval, rotator.config.retryMax)
   233  		if err != nil {
   234  			return false, fmt.Errorf("failed to load CA secret %s:%s (error: %s)", rotator.config.caStorageNamespace, rotator.config.secretName,
   235  				err.Error())
   236  		}
   237  	}
   238  	caSecret.Data[CACertFile] = cert
   239  	caSecret.Data[CAPrivateKeyFile] = key
   240  	if err = rotator.caSecretController.UpdateCASecretWithRetry(caSecret, rotator.config.retryInterval, rotator.config.retryMax); err != nil {
   241  		return false, fmt.Errorf("failed to update CA secret (error: %s)", err.Error())
   242  	}
   243  	rootCertRotatorLog.Infof("Root certificate is written into CA secret: %v", string(cert))
   244  	if err := rotator.ca.GetCAKeyCertBundle().VerifyAndSetAll(cert, key, nil, rootCert); err != nil {
   245  		if rollForward {
   246  			// Rolling forward root certificate fails at keycertbundle update, notify caller to rollback.
   247  			return true, fmt.Errorf("failed to update CA KeyCertBundle (error: %s)", err.Error())
   248  		}
   249  		return false, fmt.Errorf("failed to update CA KeyCertBundle (error: %s)", err.Error())
   250  	}
   251  	rootCertRotatorLog.Infof("Root certificate is updated in CA KeyCertBundle: %v", string(cert))
   252  	if rotator.onRootCertUpdate != nil {
   253  		_ = rotator.onRootCertUpdate()
   254  	}
   255  	return false, nil
   256  }