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 }