istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/nodeagent/cache/secretcache.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 cache is the in-memory secret store.
    16  package cache
    17  
    18  import (
    19  	"bytes"
    20  	"context"
    21  	"crypto/tls"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/fsnotify/fsnotify"
    30  
    31  	"istio.io/istio/pkg/backoff"
    32  	"istio.io/istio/pkg/file"
    33  	istiolog "istio.io/istio/pkg/log"
    34  	"istio.io/istio/pkg/queue"
    35  	"istio.io/istio/pkg/security"
    36  	"istio.io/istio/pkg/spiffe"
    37  	"istio.io/istio/pkg/util/sets"
    38  	"istio.io/istio/security/pkg/monitoring"
    39  	nodeagentutil "istio.io/istio/security/pkg/nodeagent/util"
    40  	pkiutil "istio.io/istio/security/pkg/pki/util"
    41  )
    42  
    43  var (
    44  	cacheLog = istiolog.RegisterScope("cache", "cache debugging")
    45  	// The total timeout for any credential retrieval process, default value of 10s is used.
    46  	totalTimeout = time.Second * 10
    47  )
    48  
    49  const (
    50  	// firstRetryBackOffDuration is the initial backoff time interval when hitting
    51  	// non-retryable error in CSR request or while there is an error in reading file mounts.
    52  	firstRetryBackOffDuration = 50 * time.Millisecond
    53  )
    54  
    55  // SecretManagerClient a SecretManager that signs CSRs using a provided security.Client. The primary
    56  // usage is to fetch the two specially named resources: `default`, which refers to the workload's
    57  // spiffe certificate, and ROOTCA, which contains just the root certificate for the workload
    58  // certificates. These are separated only due to the fact that Envoy has them separated.
    59  // Additionally, arbitrary certificates may be fetched from local files to support DestinationRule
    60  // and Gateway. Note that certificates stored externally will be sent from Istiod directly; the
    61  // in-agent SecretManagerClient has low privileges and cannot read Kubernetes Secrets or other
    62  // storage backends. Istiod is in charge of determining whether the agent (ie SecretManagerClient) or
    63  // Istiod will serve an SDS response, by selecting the appropriate cluster in the SDS configuration
    64  // it serves.
    65  //
    66  // SecretManagerClient supports two modes of retrieving certificate (potentially at the same time):
    67  //   - File based certificates. If certs are mounted under well-known path /etc/certs/{key,cert,root-cert.pem},
    68  //     requests for `default` and `ROOTCA` will automatically read from these files. Additionally,
    69  //     certificates from Gateway/DestinationRule can also be served. This is done by parsing resource
    70  //     names in accordance with security.SdsCertificateConfig (file-cert: and file-root:).
    71  //   - On demand CSRs. This is used only for the `default` certificate. When this resource is
    72  //     requested, a CSR will be sent to the configured caClient.
    73  //
    74  // Callers are expected to only call GenerateSecret when a new certificate is required. Generally,
    75  // this should be done a single time at startup, then repeatedly when the certificate is near
    76  // expiration. To help users handle certificate expiration, any certificates created by the caClient
    77  // will be monitored; when they are near expiration the secretHandler function is triggered,
    78  // prompting the client to call GenerateSecret again, if they still care about the certificate. For
    79  // files, this callback is instead triggered on any change to the file (triggering on expiration
    80  // would not be helpful, as all we can do is re-read the same file).
    81  type SecretManagerClient struct {
    82  	caClient security.Client
    83  
    84  	// configOptions includes all configurable params for the cache.
    85  	configOptions *security.Options
    86  
    87  	// callback function to invoke when detecting secret change.
    88  	secretHandler func(resourceName string)
    89  
    90  	// Cache of workload certificate and root certificate. File based certs are never cached, as
    91  	// lookup is cheap.
    92  	cache secretCache
    93  
    94  	// generateMutex ensures we do not send concurrent requests to generate a certificate
    95  	generateMutex sync.Mutex
    96  
    97  	// The paths for an existing certificate chain, key and root cert files. Istio agent will
    98  	// use them as the source of secrets if they exist.
    99  	existingCertificateFile security.SdsCertificateConfig
   100  
   101  	// certWatcher watches the certificates for changes and triggers a notification to proxy.
   102  	certWatcher *fsnotify.Watcher
   103  	// certs being watched with file watcher.
   104  	fileCerts map[FileCert]struct{}
   105  	certMutex sync.RWMutex
   106  
   107  	// outputMutex protects writes of certificates to disk
   108  	outputMutex sync.Mutex
   109  
   110  	// Dynamically configured Trust Bundle Mutex
   111  	configTrustBundleMutex sync.RWMutex
   112  	// Dynamically configured Trust Bundle
   113  	configTrustBundle []byte
   114  
   115  	// queue maintains all certificate rotation events that need to be triggered when they are about to expire
   116  	queue queue.Delayed
   117  	stop  chan struct{}
   118  
   119  	caRootPath string
   120  }
   121  
   122  type secretCache struct {
   123  	mu       sync.RWMutex
   124  	workload *security.SecretItem
   125  	certRoot []byte
   126  }
   127  
   128  // GetRoot returns cached root cert and cert expiration time. This method is thread safe.
   129  func (s *secretCache) GetRoot() (rootCert []byte) {
   130  	s.mu.RLock()
   131  	defer s.mu.RUnlock()
   132  	return s.certRoot
   133  }
   134  
   135  // SetRoot sets root cert into cache. This method is thread safe.
   136  func (s *secretCache) SetRoot(rootCert []byte) {
   137  	s.mu.Lock()
   138  	defer s.mu.Unlock()
   139  	s.certRoot = rootCert
   140  }
   141  
   142  func (s *secretCache) GetWorkload() *security.SecretItem {
   143  	s.mu.RLock()
   144  	defer s.mu.RUnlock()
   145  	if s.workload == nil {
   146  		return nil
   147  	}
   148  	return s.workload
   149  }
   150  
   151  func (s *secretCache) SetWorkload(value *security.SecretItem) {
   152  	s.mu.Lock()
   153  	defer s.mu.Unlock()
   154  	s.workload = value
   155  }
   156  
   157  var _ security.SecretManager = &SecretManagerClient{}
   158  
   159  // FileCert stores a reference to a certificate on disk
   160  type FileCert struct {
   161  	ResourceName string
   162  	Filename     string
   163  }
   164  
   165  // NewSecretManagerClient creates a new SecretManagerClient.
   166  func NewSecretManagerClient(caClient security.Client, options *security.Options) (*SecretManagerClient, error) {
   167  	watcher, err := fsnotify.NewWatcher()
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	ret := &SecretManagerClient{
   173  		queue:         queue.NewDelayed(queue.DelayQueueBuffer(0)),
   174  		caClient:      caClient,
   175  		configOptions: options,
   176  		existingCertificateFile: security.SdsCertificateConfig{
   177  			CertificatePath:   options.CertChainFilePath,
   178  			PrivateKeyPath:    options.KeyFilePath,
   179  			CaCertificatePath: options.RootCertFilePath,
   180  		},
   181  		certWatcher: watcher,
   182  		fileCerts:   make(map[FileCert]struct{}),
   183  		stop:        make(chan struct{}),
   184  		caRootPath:  options.CARootPath,
   185  	}
   186  
   187  	go ret.queue.Run(ret.stop)
   188  	go ret.handleFileWatch()
   189  	return ret, nil
   190  }
   191  
   192  func (sc *SecretManagerClient) Close() {
   193  	_ = sc.certWatcher.Close()
   194  	if sc.caClient != nil {
   195  		sc.caClient.Close()
   196  	}
   197  	close(sc.stop)
   198  }
   199  
   200  func (sc *SecretManagerClient) RegisterSecretHandler(h func(resourceName string)) {
   201  	sc.certMutex.Lock()
   202  	defer sc.certMutex.Unlock()
   203  	sc.secretHandler = h
   204  }
   205  
   206  func (sc *SecretManagerClient) OnSecretUpdate(resourceName string) {
   207  	sc.certMutex.RLock()
   208  	defer sc.certMutex.RUnlock()
   209  	if sc.secretHandler != nil {
   210  		sc.secretHandler(resourceName)
   211  	}
   212  }
   213  
   214  // getCachedSecret: retrieve cached Secret Item (workload-certificate/workload-root) from secretManager client
   215  func (sc *SecretManagerClient) getCachedSecret(resourceName string) (secret *security.SecretItem) {
   216  	var rootCertBundle []byte
   217  	var ns *security.SecretItem
   218  
   219  	if c := sc.cache.GetWorkload(); c != nil {
   220  		if resourceName == security.RootCertReqResourceName {
   221  			rootCertBundle = sc.mergeTrustAnchorBytes(c.RootCert)
   222  			ns = &security.SecretItem{
   223  				ResourceName: resourceName,
   224  				RootCert:     rootCertBundle,
   225  			}
   226  			cacheLog.WithLabels("ttl", time.Until(c.ExpireTime)).Info("returned workload trust anchor from cache")
   227  
   228  		} else {
   229  			ns = &security.SecretItem{
   230  				ResourceName:     resourceName,
   231  				CertificateChain: c.CertificateChain,
   232  				PrivateKey:       c.PrivateKey,
   233  				ExpireTime:       c.ExpireTime,
   234  				CreatedTime:      c.CreatedTime,
   235  			}
   236  			cacheLog.WithLabels("ttl", time.Until(c.ExpireTime)).Info("returned workload certificate from cache")
   237  		}
   238  
   239  		return ns
   240  	}
   241  	return nil
   242  }
   243  
   244  // GenerateSecret passes the cached secret to SDS.StreamSecrets and SDS.FetchSecret.
   245  func (sc *SecretManagerClient) GenerateSecret(resourceName string) (secret *security.SecretItem, err error) {
   246  	cacheLog.Debugf("generate secret %q", resourceName)
   247  	// Setup the call to store generated secret to disk
   248  	defer func() {
   249  		if secret == nil || err != nil {
   250  			return
   251  		}
   252  		// We need to hold a mutex here, otherwise if two threads are writing the same certificate,
   253  		// we may permanently end up with a mismatch key/cert pair. We still make end up temporarily
   254  		// with mismatched key/cert pair since we cannot atomically write multiple files. It may be
   255  		// possible by keeping the output in a directory with clever use of symlinks in the future,
   256  		// if needed.
   257  		sc.outputMutex.Lock()
   258  		defer sc.outputMutex.Unlock()
   259  		if resourceName == security.RootCertReqResourceName || resourceName == security.WorkloadKeyCertResourceName {
   260  			if err := nodeagentutil.OutputKeyCertToDir(sc.configOptions.OutputKeyCertToDir, secret.PrivateKey,
   261  				secret.CertificateChain, secret.RootCert); err != nil {
   262  				cacheLog.Errorf("error when output the resource: %v", err)
   263  			} else if sc.configOptions.OutputKeyCertToDir != "" {
   264  				resourceLog(resourceName).Debugf("output the resource to %v", sc.configOptions.OutputKeyCertToDir)
   265  			}
   266  		}
   267  	}()
   268  
   269  	// First try to generate secret from file.
   270  	if sdsFromFile, ns, err := sc.generateFileSecret(resourceName); sdsFromFile {
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  		return ns, nil
   275  	}
   276  
   277  	ns := sc.getCachedSecret(resourceName)
   278  	if ns != nil {
   279  		return ns, nil
   280  	}
   281  
   282  	t0 := time.Now()
   283  	sc.generateMutex.Lock()
   284  	defer sc.generateMutex.Unlock()
   285  
   286  	// Now that we got the lock, look at cache again before sending request to avoid overwhelming CA
   287  	ns = sc.getCachedSecret(resourceName)
   288  	if ns != nil {
   289  		return ns, nil
   290  	}
   291  
   292  	if ts := time.Since(t0); ts > time.Second {
   293  		cacheLog.Warnf("slow generate secret lock: %v", ts)
   294  	}
   295  
   296  	// send request to CA to get new workload certificate
   297  	ns, err = sc.generateNewSecret(resourceName)
   298  	if err != nil {
   299  		return nil, fmt.Errorf("failed to generate workload certificate: %v", err)
   300  	}
   301  
   302  	// Store the new secret in the secretCache and trigger the periodic rotation for workload certificate
   303  	sc.registerSecret(*ns)
   304  
   305  	if resourceName == security.RootCertReqResourceName {
   306  		ns.RootCert = sc.mergeTrustAnchorBytes(ns.RootCert)
   307  	} else {
   308  		// If periodic cert refresh resulted in discovery of a new root, trigger a ROOTCA request to refresh trust anchor
   309  		oldRoot := sc.cache.GetRoot()
   310  		if !bytes.Equal(oldRoot, ns.RootCert) {
   311  			cacheLog.Info("Root cert has changed, start rotating root cert")
   312  			// We store the oldRoot only for comparison and not for serving
   313  			sc.cache.SetRoot(ns.RootCert)
   314  			sc.OnSecretUpdate(security.RootCertReqResourceName)
   315  		}
   316  	}
   317  
   318  	return ns, nil
   319  }
   320  
   321  func (sc *SecretManagerClient) addFileWatcher(file string, resourceName string) {
   322  	// Try adding file watcher and if it fails start a retry loop.
   323  	if err := sc.tryAddFileWatcher(file, resourceName); err == nil {
   324  		return
   325  	}
   326  	// RetryWithContext file watcher as some times it might fail to add and we will miss change
   327  	// notifications on those files. For now, retry for ever till the watcher is added.
   328  	// TODO(ramaraochavali): Think about tieing these failures to liveness probe with a
   329  	// reasonable threshold (when the problem is not transient) and restart the pod.
   330  	go func() {
   331  		b := backoff.NewExponentialBackOff(backoff.DefaultOption())
   332  		_ = b.RetryWithContext(context.TODO(), func() error {
   333  			err := sc.tryAddFileWatcher(file, resourceName)
   334  			return err
   335  		})
   336  	}()
   337  }
   338  
   339  func (sc *SecretManagerClient) tryAddFileWatcher(file string, resourceName string) error {
   340  	// Check if this file is being already watched, if so ignore it. This check is needed here to
   341  	// avoid processing duplicate events for the same file.
   342  	sc.certMutex.Lock()
   343  	defer sc.certMutex.Unlock()
   344  	file, err := filepath.Abs(file)
   345  	if err != nil {
   346  		cacheLog.Errorf("%v: error finding absolute path of %s, retrying watches: %v", resourceName, file, err)
   347  		return err
   348  	}
   349  	key := FileCert{
   350  		ResourceName: resourceName,
   351  		Filename:     file,
   352  	}
   353  	if _, alreadyWatching := sc.fileCerts[key]; alreadyWatching {
   354  		cacheLog.Debugf("already watching file for %s", file)
   355  		// Already watching, no need to do anything
   356  		return nil
   357  	}
   358  	sc.fileCerts[key] = struct{}{}
   359  	// File is not being watched, start watching now and trigger key push.
   360  	cacheLog.Infof("adding watcher for file certificate %s", file)
   361  	if err := sc.certWatcher.Add(file); err != nil {
   362  		cacheLog.Errorf("%v: error adding watcher for file %v, retrying watches: %v", resourceName, file, err)
   363  		numFileWatcherFailures.Increment()
   364  		return err
   365  	}
   366  	return nil
   367  }
   368  
   369  // If there is existing root certificates under a well known path, return true.
   370  // Otherwise, return false.
   371  func (sc *SecretManagerClient) rootCertificateExist(filePath string) bool {
   372  	b, err := os.ReadFile(filePath)
   373  	if err != nil || len(b) == 0 {
   374  		return false
   375  	}
   376  	return true
   377  }
   378  
   379  // If there is an existing private key and certificate under a well known path, return true.
   380  // Otherwise, return false.
   381  func (sc *SecretManagerClient) keyCertificateExist(certPath, keyPath string) bool {
   382  	b, err := os.ReadFile(certPath)
   383  	if err != nil || len(b) == 0 {
   384  		return false
   385  	}
   386  	b, err = os.ReadFile(keyPath)
   387  	if err != nil || len(b) == 0 {
   388  		return false
   389  	}
   390  
   391  	return true
   392  }
   393  
   394  // Generate a root certificate item from the passed in rootCertPath
   395  func (sc *SecretManagerClient) generateRootCertFromExistingFile(rootCertPath, resourceName string, workload bool) (*security.SecretItem, error) {
   396  	var rootCert []byte
   397  	var err error
   398  	o := backoff.DefaultOption()
   399  	o.InitialInterval = sc.configOptions.FileDebounceDuration
   400  	b := backoff.NewExponentialBackOff(o)
   401  	certValid := func() error {
   402  		rootCert, err = os.ReadFile(rootCertPath)
   403  		if err != nil {
   404  			return err
   405  		}
   406  		_, _, err := pkiutil.ParsePemEncodedCertificateChain(rootCert)
   407  		if err != nil {
   408  			return err
   409  		}
   410  		return nil
   411  	}
   412  	ctx, cancel := context.WithTimeout(context.Background(), totalTimeout)
   413  	defer cancel()
   414  	if err := b.RetryWithContext(ctx, certValid); err != nil {
   415  		return nil, err
   416  	}
   417  
   418  	// Set the rootCert only if it is workload root cert.
   419  	if workload {
   420  		sc.cache.SetRoot(rootCert)
   421  	}
   422  	return &security.SecretItem{
   423  		ResourceName: resourceName,
   424  		RootCert:     rootCert,
   425  	}, nil
   426  }
   427  
   428  // Generate a key and certificate item from the existing key certificate files from the passed in file paths.
   429  func (sc *SecretManagerClient) generateKeyCertFromExistingFiles(certChainPath, keyPath, resourceName string) (*security.SecretItem, error) {
   430  	// There is a remote possibility that key is written and cert is not written yet.
   431  	// To handle that case, check if cert and key are valid if they are valid then only send to proxy.
   432  	o := backoff.DefaultOption()
   433  	o.InitialInterval = sc.configOptions.FileDebounceDuration
   434  	b := backoff.NewExponentialBackOff(o)
   435  	secretValid := func() error {
   436  		_, err := tls.LoadX509KeyPair(certChainPath, keyPath)
   437  		return err
   438  	}
   439  	ctx, cancel := context.WithTimeout(context.Background(), totalTimeout)
   440  	defer cancel()
   441  	if err := b.RetryWithContext(ctx, secretValid); err != nil {
   442  		return nil, err
   443  	}
   444  	return sc.keyCertSecretItem(certChainPath, keyPath, resourceName)
   445  }
   446  
   447  func (sc *SecretManagerClient) keyCertSecretItem(cert, key, resource string) (*security.SecretItem, error) {
   448  	certChain, err := sc.readFileWithTimeout(cert)
   449  	if err != nil {
   450  		return nil, err
   451  	}
   452  	keyPEM, err := sc.readFileWithTimeout(key)
   453  	if err != nil {
   454  		return nil, err
   455  	}
   456  
   457  	now := time.Now()
   458  	var certExpireTime time.Time
   459  	if certExpireTime, err = nodeagentutil.ParseCertAndGetExpiryTimestamp(certChain); err != nil {
   460  		cacheLog.Errorf("failed to extract expiration time in the certificate loaded from file: %v", err)
   461  		return nil, fmt.Errorf("failed to extract expiration time in the certificate loaded from file: %v", err)
   462  	}
   463  
   464  	return &security.SecretItem{
   465  		CertificateChain: certChain,
   466  		PrivateKey:       keyPEM,
   467  		ResourceName:     resource,
   468  		CreatedTime:      now,
   469  		ExpireTime:       certExpireTime,
   470  	}, nil
   471  }
   472  
   473  // readFileWithTimeout reads the given file with timeout. It returns error
   474  // if it is not able to read file after timeout.
   475  func (sc *SecretManagerClient) readFileWithTimeout(path string) ([]byte, error) {
   476  	retryBackoff := firstRetryBackOffDuration
   477  	timeout := time.After(totalTimeout)
   478  	for {
   479  		cert, err := os.ReadFile(path)
   480  		if err == nil {
   481  			return cert, nil
   482  		}
   483  		select {
   484  		case <-time.After(retryBackoff):
   485  			retryBackoff *= 2
   486  		case <-timeout:
   487  			return nil, err
   488  		case <-sc.stop:
   489  			return nil, err
   490  		}
   491  	}
   492  }
   493  
   494  func (sc *SecretManagerClient) generateFileSecret(resourceName string) (bool, *security.SecretItem, error) {
   495  	logPrefix := cacheLogPrefix(resourceName)
   496  
   497  	cf := sc.existingCertificateFile
   498  	// outputToCertificatePath handles a special case where we have configured to output certificates
   499  	// to the special /etc/certs directory. In this case, we need to ensure we do *not* read from
   500  	// these files, otherwise we would never rotate.
   501  	outputToCertificatePath, ferr := file.DirEquals(filepath.Dir(cf.CertificatePath), sc.configOptions.OutputKeyCertToDir)
   502  	if ferr != nil {
   503  		return false, nil, ferr
   504  	}
   505  	// When there are existing root certificates, or private key and certificate under
   506  	// a well known path, they are used in the SDS response.
   507  	sdsFromFile := false
   508  	var err error
   509  	var sitem *security.SecretItem
   510  
   511  	switch {
   512  	// Default root certificate.
   513  	case resourceName == security.RootCertReqResourceName && sc.rootCertificateExist(cf.CaCertificatePath) && !outputToCertificatePath:
   514  		sdsFromFile = true
   515  		if sitem, err = sc.generateRootCertFromExistingFile(cf.CaCertificatePath, resourceName, true); err == nil {
   516  			// If retrieving workload trustBundle, then merge other configured trustAnchors in ProxyConfig
   517  			sitem.RootCert = sc.mergeTrustAnchorBytes(sitem.RootCert)
   518  			sc.addFileWatcher(cf.CaCertificatePath, resourceName)
   519  		}
   520  	// Default workload certificate.
   521  	case resourceName == security.WorkloadKeyCertResourceName && sc.keyCertificateExist(cf.CertificatePath, cf.PrivateKeyPath) && !outputToCertificatePath:
   522  		sdsFromFile = true
   523  		if sitem, err = sc.generateKeyCertFromExistingFiles(cf.CertificatePath, cf.PrivateKeyPath, resourceName); err == nil {
   524  			// Adding cert is sufficient here as key can't change without changing the cert.
   525  			sc.addFileWatcher(cf.CertificatePath, resourceName)
   526  		}
   527  	case resourceName == security.FileRootSystemCACert:
   528  		sdsFromFile = true
   529  		if sc.caRootPath != "" {
   530  			if sitem, err = sc.generateRootCertFromExistingFile(sc.caRootPath, resourceName, false); err == nil {
   531  				sc.addFileWatcher(sc.caRootPath, resourceName)
   532  			}
   533  		} else {
   534  			sdsFromFile = false
   535  		}
   536  	default:
   537  		// Check if the resource name refers to a file mounted certificate.
   538  		// Currently used in destination rules and server certs (via metadata).
   539  		// Based on the resource name, we need to read the secret from a file encoded in the resource name.
   540  		cfg, ok := security.SdsCertificateConfigFromResourceName(resourceName)
   541  		sdsFromFile = ok
   542  		switch {
   543  		case ok && cfg.IsRootCertificate():
   544  			if sitem, err = sc.generateRootCertFromExistingFile(cfg.CaCertificatePath, resourceName, false); err == nil {
   545  				sc.addFileWatcher(cfg.CaCertificatePath, resourceName)
   546  			}
   547  		case ok && cfg.IsKeyCertificate():
   548  			if sitem, err = sc.generateKeyCertFromExistingFiles(cfg.CertificatePath, cfg.PrivateKeyPath, resourceName); err == nil {
   549  				// Adding cert is sufficient here as key can't change without changing the cert.
   550  				sc.addFileWatcher(cfg.CertificatePath, resourceName)
   551  			}
   552  		}
   553  	}
   554  
   555  	if sdsFromFile {
   556  		if err != nil {
   557  			cacheLog.Errorf("%s failed to generate secret for proxy from file: %v",
   558  				logPrefix, err)
   559  			numFileSecretFailures.Increment()
   560  			return sdsFromFile, nil, err
   561  		}
   562  		cacheLog.WithLabels("resource", resourceName).Info("read certificate from file")
   563  		// We do not register the secret. Unlike on-demand CSRs, there is nothing we can do if a file
   564  		// cert expires; there is no point sending an update when its near expiry. Instead, a
   565  		// separate file watcher will ensure if the file changes we trigger an update.
   566  		return sdsFromFile, sitem, nil
   567  	}
   568  	return sdsFromFile, nil, nil
   569  }
   570  
   571  func (sc *SecretManagerClient) generateNewSecret(resourceName string) (*security.SecretItem, error) {
   572  	trustBundlePEM := []string{}
   573  	var rootCertPEM []byte
   574  
   575  	if sc.caClient == nil {
   576  		return nil, fmt.Errorf("attempted to fetch secret, but ca client is nil")
   577  	}
   578  	t0 := time.Now()
   579  	logPrefix := cacheLogPrefix(resourceName)
   580  
   581  	csrHostName := &spiffe.Identity{
   582  		TrustDomain:    sc.configOptions.TrustDomain,
   583  		Namespace:      sc.configOptions.WorkloadNamespace,
   584  		ServiceAccount: sc.configOptions.ServiceAccount,
   585  	}
   586  
   587  	cacheLog.Debugf("constructed host name for CSR: %s", csrHostName.String())
   588  	options := pkiutil.CertOptions{
   589  		Host:       csrHostName.String(),
   590  		RSAKeySize: sc.configOptions.WorkloadRSAKeySize,
   591  		PKCS8Key:   sc.configOptions.Pkcs8Keys,
   592  		ECSigAlg:   pkiutil.SupportedECSignatureAlgorithms(sc.configOptions.ECCSigAlg),
   593  		ECCCurve:   pkiutil.SupportedEllipticCurves(sc.configOptions.ECCCurve),
   594  	}
   595  
   596  	// Generate the cert/key, send CSR to CA.
   597  	csrPEM, keyPEM, err := pkiutil.GenCSR(options)
   598  	if err != nil {
   599  		cacheLog.Errorf("%s failed to generate key and certificate for CSR: %v", logPrefix, err)
   600  		return nil, err
   601  	}
   602  
   603  	numOutgoingRequests.With(RequestType.Value(monitoring.CSR)).Increment()
   604  	timeBeforeCSR := time.Now()
   605  	certChainPEM, err := sc.caClient.CSRSign(csrPEM, int64(sc.configOptions.SecretTTL.Seconds()))
   606  	if err == nil {
   607  		trustBundlePEM, err = sc.caClient.GetRootCertBundle()
   608  	}
   609  	csrLatency := float64(time.Since(timeBeforeCSR).Nanoseconds()) / float64(time.Millisecond)
   610  	outgoingLatency.With(RequestType.Value(monitoring.CSR)).Record(csrLatency)
   611  	if err != nil {
   612  		numFailedOutgoingRequests.With(RequestType.Value(monitoring.CSR)).Increment()
   613  		cacheLog.Errorf("%s failed to sign: %v", logPrefix, err)
   614  		return nil, err
   615  	}
   616  
   617  	certChain := concatCerts(certChainPEM)
   618  
   619  	var expireTime time.Time
   620  	// Cert expire time by default is createTime + sc.configOptions.SecretTTL.
   621  	// Istiod respects SecretTTL that passed to it and use it decide TTL of cert it issued.
   622  	// Some customer CA may override TTL param that's passed to it.
   623  	if expireTime, err = nodeagentutil.ParseCertAndGetExpiryTimestamp(certChain); err != nil {
   624  		cacheLog.Errorf("%s failed to extract expire time from server certificate in CSR response %+v: %v",
   625  			logPrefix, certChainPEM, err)
   626  		return nil, fmt.Errorf("failed to extract expire time from server certificate in CSR response: %v", err)
   627  	}
   628  
   629  	cacheLog.WithLabels("latency", time.Since(t0), "ttl", time.Until(expireTime)).Info("generated new workload certificate")
   630  
   631  	if len(trustBundlePEM) > 0 {
   632  		rootCertPEM = concatCerts(trustBundlePEM)
   633  	} else {
   634  		// If CA Client has no explicit mechanism to retrieve CA root, infer it from the root of the certChain
   635  		rootCertPEM = []byte(certChainPEM[len(certChainPEM)-1])
   636  	}
   637  
   638  	return &security.SecretItem{
   639  		CertificateChain: certChain,
   640  		PrivateKey:       keyPEM,
   641  		ResourceName:     resourceName,
   642  		CreatedTime:      time.Now(),
   643  		ExpireTime:       expireTime,
   644  		RootCert:         rootCertPEM,
   645  	}, nil
   646  }
   647  
   648  var rotateTime = func(secret security.SecretItem, graceRatio float64) time.Duration {
   649  	secretLifeTime := secret.ExpireTime.Sub(secret.CreatedTime)
   650  	gracePeriod := time.Duration((graceRatio) * float64(secretLifeTime))
   651  	delay := time.Until(secret.ExpireTime.Add(-gracePeriod))
   652  	if delay < 0 {
   653  		delay = 0
   654  	}
   655  	return delay
   656  }
   657  
   658  func (sc *SecretManagerClient) registerSecret(item security.SecretItem) {
   659  	delay := rotateTime(item, sc.configOptions.SecretRotationGracePeriodRatio)
   660  	certExpirySeconds.ValueFrom(func() float64 { return time.Until(item.ExpireTime).Seconds() }, ResourceName.Value(item.ResourceName))
   661  	item.ResourceName = security.WorkloadKeyCertResourceName
   662  	// In case there are two calls to GenerateSecret at once, we don't want both to be concurrently registered
   663  	if sc.cache.GetWorkload() != nil {
   664  		resourceLog(item.ResourceName).Infof("skip scheduling certificate rotation, already scheduled")
   665  		return
   666  	}
   667  	sc.cache.SetWorkload(&item)
   668  	resourceLog(item.ResourceName).Debugf("scheduled certificate for rotation in %v", delay)
   669  	sc.queue.PushDelayed(func() error {
   670  		// In case `UpdateConfigTrustBundle` called, it will resign workload cert.
   671  		// Check if this is a stale scheduled rotating task.
   672  		if cached := sc.cache.GetWorkload(); cached != nil {
   673  			if cached.CreatedTime == item.CreatedTime {
   674  				resourceLog(item.ResourceName).Debugf("rotating certificate")
   675  				// Clear the cache so the next call generates a fresh certificate
   676  				sc.cache.SetWorkload(nil)
   677  				sc.OnSecretUpdate(item.ResourceName)
   678  			}
   679  		}
   680  		return nil
   681  	}, delay)
   682  }
   683  
   684  func (sc *SecretManagerClient) handleFileWatch() {
   685  	for {
   686  		select {
   687  		case event, ok := <-sc.certWatcher.Events:
   688  			// Channel is closed.
   689  			if !ok {
   690  				return
   691  			}
   692  			// We only care about updates that change the file content
   693  			if !(isWrite(event) || isRemove(event) || isCreate(event)) {
   694  				continue
   695  			}
   696  			sc.certMutex.RLock()
   697  			resources := make(map[FileCert]struct{})
   698  			for k, v := range sc.fileCerts {
   699  				resources[k] = v
   700  			}
   701  			sc.certMutex.RUnlock()
   702  			cacheLog.Infof("event for file certificate %s : %s, pushing to proxy", event.Name, event.Op.String())
   703  			// If it is remove event - cleanup from file certs so that if it is added again, we can watch.
   704  			// The cleanup should happen first before triggering callbacks, as the callbacks are async and
   705  			// we may get generate call before cleanup is done and we will end up not watching the file.
   706  			if isRemove(event) {
   707  				sc.certMutex.Lock()
   708  				for fc := range sc.fileCerts {
   709  					if fc.Filename == event.Name {
   710  						cacheLog.Debugf("removing file %s from file certs", event.Name)
   711  						delete(sc.fileCerts, fc)
   712  						break
   713  					}
   714  				}
   715  				sc.certMutex.Unlock()
   716  			}
   717  			// Trigger callbacks for all resources referencing this file. This is practically always
   718  			// a single resource.
   719  			for k := range resources {
   720  				if k.Filename == event.Name {
   721  					sc.OnSecretUpdate(k.ResourceName)
   722  				}
   723  			}
   724  		case err, ok := <-sc.certWatcher.Errors:
   725  			// Channel is closed.
   726  			if !ok {
   727  				return
   728  			}
   729  			numFileWatcherFailures.Increment()
   730  			cacheLog.Errorf("certificate watch error: %v", err)
   731  		}
   732  	}
   733  }
   734  
   735  func isWrite(event fsnotify.Event) bool {
   736  	return event.Has(fsnotify.Write)
   737  }
   738  
   739  func isCreate(event fsnotify.Event) bool {
   740  	return event.Has(fsnotify.Create)
   741  }
   742  
   743  func isRemove(event fsnotify.Event) bool {
   744  	return event.Has(fsnotify.Remove)
   745  }
   746  
   747  // concatCerts concatenates PEM certificates, making sure each one starts on a new line
   748  func concatCerts(certsPEM []string) []byte {
   749  	if len(certsPEM) == 0 {
   750  		return []byte{}
   751  	}
   752  	var certChain bytes.Buffer
   753  	for i, c := range certsPEM {
   754  		certChain.WriteString(c)
   755  		if i < len(certsPEM)-1 && !strings.HasSuffix(c, "\n") {
   756  			certChain.WriteString("\n")
   757  		}
   758  	}
   759  	return certChain.Bytes()
   760  }
   761  
   762  // UpdateConfigTrustBundle : Update the Configured Trust Bundle in the secret Manager client
   763  func (sc *SecretManagerClient) UpdateConfigTrustBundle(trustBundle []byte) error {
   764  	sc.configTrustBundleMutex.Lock()
   765  	if bytes.Equal(sc.configTrustBundle, trustBundle) {
   766  		cacheLog.Debugf("skip for same trust bundle")
   767  		sc.configTrustBundleMutex.Unlock()
   768  		return nil
   769  	}
   770  	sc.configTrustBundle = trustBundle
   771  	sc.configTrustBundleMutex.Unlock()
   772  	cacheLog.Debugf("update new trust bundle")
   773  	sc.OnSecretUpdate(security.RootCertReqResourceName)
   774  	sc.cache.SetWorkload(nil)
   775  	sc.OnSecretUpdate(security.WorkloadKeyCertResourceName)
   776  	return nil
   777  }
   778  
   779  // mergeTrustAnchorBytes: Merge cert bytes with the cached TrustAnchors.
   780  func (sc *SecretManagerClient) mergeTrustAnchorBytes(caCerts []byte) []byte {
   781  	return sc.mergeConfigTrustBundle(pkiutil.PemCertBytestoString(caCerts))
   782  }
   783  
   784  // mergeConfigTrustBundle: merge rootCerts trustAnchors provided in args with proxyConfig trustAnchors
   785  // ensure dedup and sorting before returning trustAnchors
   786  func (sc *SecretManagerClient) mergeConfigTrustBundle(rootCerts []string) []byte {
   787  	sc.configTrustBundleMutex.RLock()
   788  	existingCerts := pkiutil.PemCertBytestoString(sc.configTrustBundle)
   789  	sc.configTrustBundleMutex.RUnlock()
   790  	anchors := sets.New[string]()
   791  	for _, cert := range existingCerts {
   792  		anchors.Insert(cert)
   793  	}
   794  	for _, cert := range rootCerts {
   795  		anchors.Insert(cert)
   796  	}
   797  	anchorBytes := []byte{}
   798  	for _, cert := range sets.SortedList(anchors) {
   799  		anchorBytes = pkiutil.AppendCertByte(anchorBytes, []byte(cert))
   800  	}
   801  	return anchorBytes
   802  }