istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/bootstrap/istio_ca.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 bootstrap 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "fmt" 22 "os" 23 "path" 24 "strings" 25 "time" 26 27 "github.com/fsnotify/fsnotify" 28 "google.golang.org/grpc" 29 "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 32 "istio.io/api/security/v1beta1" 33 "istio.io/istio/pilot/pkg/features" 34 securityModel "istio.io/istio/pilot/pkg/security/model" 35 "istio.io/istio/pkg/config/constants" 36 "istio.io/istio/pkg/env" 37 "istio.io/istio/pkg/log" 38 "istio.io/istio/pkg/security" 39 "istio.io/istio/security/pkg/cmd" 40 "istio.io/istio/security/pkg/pki/ca" 41 "istio.io/istio/security/pkg/pki/ra" 42 caserver "istio.io/istio/security/pkg/server/ca" 43 "istio.io/istio/security/pkg/server/ca/authenticate" 44 "istio.io/istio/security/pkg/util" 45 ) 46 47 type caOptions struct { 48 ExternalCAType ra.CaExternalType 49 ExternalCASigner string 50 // domain to use in SPIFFE identity URLs 51 TrustDomain string 52 Namespace string 53 Authenticators []security.Authenticator 54 CertSignerDomain string 55 } 56 57 // Based on istio_ca main - removing creation of Secrets with private keys in all namespaces and install complexity. 58 // 59 // For backward compat, will preserve support for the "cacerts" Secret used for self-signed certificates. 60 // It is mounted in the same location, and if found will be used - creating the secret is sufficient, no need for 61 // extra options. 62 // 63 // In old installer, the LocalCertDir is hardcoded to /etc/cacerts and mounted from "cacerts" secret. 64 // 65 // Support for signing other root CA has been removed - too dangerous, no clear use case. 66 // 67 // Default config, for backward compat with Citadel: 68 // - if "cacerts" secret exists in istio-system, will be mounted. It may contain an optional "root-cert.pem", 69 // with additional roots and optional {ca-key, ca-cert, cert-chain}.pem user-provided root CA. 70 // - if user-provided root CA is not found, the Secret "istio-ca-secret" is used, with ca-cert.pem and ca-key.pem files. 71 // - if neither is found, istio-ca-secret will be created. 72 // 73 // - a config map "istio-security" with a "caTLSRootCert" file will be used for root cert, and created if needed. 74 // The config map was used by node agent - no longer possible to use in sds-agent, but we still save it for 75 // backward compat. Will be removed with the node-agent. sds-agent is calling NewCitadelClient directly, using 76 // K8S root. 77 78 var ( 79 // LocalCertDir replaces the "cert-chain", "signing-cert" and "signing-key" flags in citadel - Istio installer is 80 // requires a secret named "cacerts" with specific files inside. 81 LocalCertDir = env.Register("ROOT_CA_DIR", "./etc/cacerts", 82 "Location of a local or mounted CA root") 83 84 useRemoteCerts = env.Register("USE_REMOTE_CERTS", false, 85 "Whether to try to load CA certs from config Kubernetes cluster. Used for external Istiod.") 86 87 workloadCertTTL = env.Register("DEFAULT_WORKLOAD_CERT_TTL", 88 cmd.DefaultWorkloadCertTTL, 89 "The default TTL of issued workload certificates. Applied when the client sets a "+ 90 "non-positive TTL in the CSR.") 91 92 maxWorkloadCertTTL = env.Register("MAX_WORKLOAD_CERT_TTL", 93 cmd.DefaultMaxWorkloadCertTTL, 94 "The max TTL of issued workload certificates.") 95 96 SelfSignedCACertTTL = env.Register("CITADEL_SELF_SIGNED_CA_CERT_TTL", 97 cmd.DefaultSelfSignedCACertTTL, 98 "The TTL of self-signed CA root certificate.") 99 100 selfSignedRootCertCheckInterval = env.Register("CITADEL_SELF_SIGNED_ROOT_CERT_CHECK_INTERVAL", 101 cmd.DefaultSelfSignedRootCertCheckInterval, 102 "The interval that self-signed CA checks its root certificate "+ 103 "expiration time and rotates root certificate. Setting this interval "+ 104 "to zero or a negative value disables automated root cert check and "+ 105 "rotation. This interval is suggested to be larger than 10 minutes.") 106 107 selfSignedRootCertGracePeriodPercentile = env.Register("CITADEL_SELF_SIGNED_ROOT_CERT_GRACE_PERIOD_PERCENTILE", 108 cmd.DefaultRootCertGracePeriodPercentile, 109 "Grace period percentile for self-signed root cert.") 110 111 enableJitterForRootCertRotator = env.Register("CITADEL_ENABLE_JITTER_FOR_ROOT_CERT_ROTATOR", 112 true, 113 "If true, set up a jitter to start root cert rotator. "+ 114 "Jitter selects a backoff time in seconds to start root cert rotator, "+ 115 "and the back off time is below root cert check interval.") 116 117 k8sInCluster = env.Register("KUBERNETES_SERVICE_HOST", "", 118 "Kubernetes service host, set automatically when running in-cluster") 119 120 // This value can also be extracted from the mounted token 121 trustedIssuer = env.Register("TOKEN_ISSUER", "", 122 "OIDC token issuer. If set, will be used to check the tokens.") 123 124 audience = env.Register("AUDIENCE", "", 125 "Expected audience in the tokens. ") 126 127 caRSAKeySize = env.Register("CITADEL_SELF_SIGNED_CA_RSA_KEY_SIZE", 2048, 128 "Specify the RSA key size to use for self-signed Istio CA certificates.") 129 130 // TODO: Likely to be removed and added to mesh config 131 externalCaType = env.Register("EXTERNAL_CA", "", 132 "External CA Integration Type. Permitted value is ISTIOD_RA_KUBERNETES_API.").Get() 133 134 // TODO: Likely to be removed and added to mesh config 135 k8sSigner = env.Register("K8S_SIGNER", "", 136 "Kubernetes CA Signer type. Valid from Kubernetes 1.18").Get() 137 ) 138 139 // initCAServer create a CA Server. The CA API uses cert with the max workload cert TTL. 140 // 'hostlist' must be non-empty - but is not used since CA Server will start on existing 141 // grpc server. Adds client cert auth and kube (sds enabled) 142 func (s *Server) initCAServer(ca caserver.CertificateAuthority, opts *caOptions) { 143 caServer, startErr := caserver.New(ca, maxWorkloadCertTTL.Get(), opts.Authenticators, s.multiclusterController) 144 if startErr != nil { 145 log.Fatalf("failed to create istio ca server: %v", startErr) 146 } 147 s.caServer = caServer 148 } 149 150 // RunCA will start the cert signing GRPC service on an existing server. 151 // Protected by installer options: the CA will be started only if the JWT token in /var/run/secrets 152 // is mounted. If it is missing - for example old versions of K8S that don't support such tokens - 153 // we will not start the cert-signing server, since pods will have no way to authenticate. 154 func (s *Server) RunCA(grpc *grpc.Server) { 155 iss := trustedIssuer.Get() 156 aud := audience.Get() 157 158 token, err := os.ReadFile(securityModel.ThirdPartyJwtPath) 159 if err == nil { 160 tok, err := detectAuthEnv(string(token)) 161 if err != nil { 162 log.Warnf("Starting with invalid K8S JWT token: %v", err) 163 } else { 164 if iss == "" { 165 iss = tok.Iss 166 } 167 if len(tok.Aud) > 0 && len(aud) == 0 { 168 aud = tok.Aud[0] 169 } 170 } 171 } 172 173 // TODO: if not set, parse Istiod's own token (if present) and get the issuer. The same issuer is used 174 // for all tokens - no need to configure twice. The token may also include cluster info to auto-configure 175 // networking properties. 176 if iss != "" && // issuer set explicitly or extracted from our own JWT 177 k8sInCluster.Get() == "" { // not running in cluster - in cluster use direct call to apiserver 178 // Add a custom authenticator using standard JWT validation, if not running in K8S 179 // When running inside K8S - we can use the built-in validator, which also check pod removal (invalidation). 180 jwtRule := v1beta1.JWTRule{Issuer: iss, Audiences: []string{aud}} 181 oidcAuth, err := authenticate.NewJwtAuthenticator(&jwtRule) 182 if err == nil { 183 s.caServer.Authenticators = append(s.caServer.Authenticators, oidcAuth) 184 log.Info("Using out-of-cluster JWT authentication") 185 } else { 186 log.Info("K8S token doesn't support OIDC, using only in-cluster auth") 187 } 188 } 189 190 s.caServer.Register(grpc) 191 192 log.Info("Istiod CA has started") 193 } 194 195 // detectAuthEnv will use the JWT token that is mounted in istiod to set the default audience 196 // and trust domain for Istiod, if not explicitly defined. 197 // K8S will use the same kind of tokens for the pods, and the value in istiod's own token is 198 // simplest and safest way to have things match. 199 // 200 // Note that K8S is not required to use JWT tokens - we will fallback to the defaults 201 // or require explicit user option for K8S clusters using opaque tokens. 202 func detectAuthEnv(jwt string) (*authenticate.JwtPayload, error) { 203 jwtSplit := strings.Split(jwt, ".") 204 if len(jwtSplit) != 3 { 205 return nil, fmt.Errorf("invalid JWT parts: %s", jwt) 206 } 207 payload := jwtSplit[1] 208 209 payloadBytes, err := util.DecodeJwtPart(payload) 210 if err != nil { 211 return nil, fmt.Errorf("failed to decode jwt: %v", err.Error()) 212 } 213 214 structuredPayload := &authenticate.JwtPayload{} 215 err = json.Unmarshal(payloadBytes, &structuredPayload) 216 if err != nil { 217 return nil, fmt.Errorf("failed to unmarshal jwt: %v", err.Error()) 218 } 219 220 return structuredPayload, nil 221 } 222 223 // detectSigningCABundle determines in which format the signing ca files are created. 224 // kubernetes tls secrets mount files as tls.crt,tls.key,ca.crt 225 // istiod secret is ca-cert.pem ca-key.pem cert-chain.pem root-cert.pem 226 func detectSigningCABundle() (ca.SigningCAFileBundle, error) { 227 tlsSigningFile := path.Join(LocalCertDir.Get(), ca.TLSSecretCACertFile) 228 229 // looking for tls file format (tls.crt) 230 if _, err := os.Stat(tlsSigningFile); err == nil { 231 log.Info("Using kubernetes.io/tls secret type for signing ca files") 232 return ca.SigningCAFileBundle{ 233 RootCertFile: path.Join(LocalCertDir.Get(), ca.TLSSecretRootCertFile), 234 CertChainFiles: []string{ 235 tlsSigningFile, 236 path.Join(LocalCertDir.Get(), ca.TLSSecretRootCertFile), 237 }, 238 SigningCertFile: tlsSigningFile, 239 SigningKeyFile: path.Join(LocalCertDir.Get(), ca.TLSSecretCAPrivateKeyFile), 240 }, nil 241 } else if !os.IsNotExist(err) { 242 return ca.SigningCAFileBundle{}, err 243 } 244 245 log.Info("Using istiod file format for signing ca files") 246 // default ca file format 247 return ca.SigningCAFileBundle{ 248 RootCertFile: path.Join(LocalCertDir.Get(), ca.RootCertFile), 249 CertChainFiles: []string{path.Join(LocalCertDir.Get(), ca.CertChainFile)}, 250 SigningCertFile: path.Join(LocalCertDir.Get(), ca.CACertFile), 251 SigningKeyFile: path.Join(LocalCertDir.Get(), ca.CAPrivateKeyFile), 252 }, nil 253 } 254 255 // loadCACerts loads an existing `cacerts` Secret if the files aren't mounted locally. 256 // By default, a cacerts Secret would be mounted during pod startup due to the 257 // Istiod Deployment configuration. But with external Istiod, we want to be 258 // able to load cacerts from a remote cluster instead. 259 func (s *Server) loadCACerts(caOpts *caOptions, dir string) error { 260 if s.kubeClient == nil { 261 return nil 262 } 263 264 signingKeyFile := path.Join(dir, ca.CAPrivateKeyFile) 265 if _, err := os.Stat(signingKeyFile); err == nil { 266 return nil 267 } else if !os.IsNotExist(err) { 268 return fmt.Errorf("signing key file %s already exists", signingKeyFile) 269 } 270 271 secret, err := s.kubeClient.Kube().CoreV1().Secrets(caOpts.Namespace).Get( 272 context.TODO(), ca.CACertsSecret, metav1.GetOptions{}) 273 if err != nil { 274 if errors.IsNotFound(err) { 275 return nil 276 } 277 return err 278 } 279 280 log.Infof("cacerts Secret found in config cluster, saving contents to %s", dir) 281 if err := os.MkdirAll(dir, 0o700); err != nil { 282 return err 283 } 284 for key, data := range secret.Data { 285 filename := path.Join(dir, key) 286 if err := os.WriteFile(filename, data, 0o600); err != nil { 287 return err 288 } 289 } 290 return nil 291 } 292 293 // handleEvent handles the events on cacerts related files. 294 // If create/write(modified) event occurs, then it verifies that 295 // newly introduced cacerts are intermediate CA which is generated 296 // from cuurent root-cert.pem. Then it updates and keycertbundle 297 // and generates new dns certs. 298 func handleEvent(s *Server) { 299 log.Info("Update Istiod cacerts") 300 301 var newCABundle []byte 302 var err error 303 304 currentCABundle := s.CA.GetCAKeyCertBundle().GetRootCertPem() 305 306 fileBundle, err := detectSigningCABundle() 307 if err != nil { 308 log.Errorf("unable to determine signing file format %v", err) 309 return 310 } 311 newCABundle, err = os.ReadFile(fileBundle.RootCertFile) 312 if err != nil { 313 log.Errorf("failed reading root-cert.pem: %v", err) 314 return 315 } 316 317 // Only updating intermediate CA is supported now 318 if !bytes.Equal(currentCABundle, newCABundle) { 319 if !features.MultiRootMesh { 320 log.Warn("Multi root is disabled, updating new ROOT-CA not supported") 321 return 322 } 323 324 // in order to support root ca rotation, or we are removing the old ca, 325 // we need to make the new CA bundle contain both old and new CA certs 326 if bytes.Contains(currentCABundle, newCABundle) || 327 bytes.Contains(newCABundle, currentCABundle) { 328 log.Info("Updating new ROOT-CA") 329 } else { 330 log.Warn("Updating new ROOT-CA not supported") 331 return 332 } 333 } 334 335 err = s.CA.GetCAKeyCertBundle().UpdateVerifiedKeyCertBundleFromFile( 336 fileBundle.SigningCertFile, 337 fileBundle.SigningKeyFile, 338 fileBundle.CertChainFiles, 339 fileBundle.RootCertFile) 340 if err != nil { 341 log.Errorf("Failed to update new Plug-in CA certs: %v", err) 342 return 343 } 344 345 err = s.updateRootCertAndGenKeyCert() 346 if err != nil { 347 log.Errorf("Failed generating plugged-in istiod key cert: %v", err) 348 return 349 } 350 351 log.Info("Istiod has detected the newly added intermediate CA and updated its key and certs accordingly") 352 } 353 354 // handleCACertsFileWatch handles the events on cacerts files 355 func (s *Server) handleCACertsFileWatch() { 356 var timerC <-chan time.Time 357 for { 358 select { 359 case <-timerC: 360 timerC = nil 361 handleEvent(s) 362 363 case event, ok := <-s.cacertsWatcher.Events: 364 if !ok { 365 log.Debug("plugin cacerts watch stopped") 366 return 367 } 368 if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { 369 if timerC == nil { 370 timerC = time.After(100 * time.Millisecond) 371 } 372 } 373 374 case err := <-s.cacertsWatcher.Errors: 375 if err != nil { 376 log.Errorf("failed to catch events on cacerts file: %v", err) 377 return 378 } 379 380 case <-s.internalStop: 381 return 382 } 383 } 384 } 385 386 func (s *Server) addCACertsFileWatcher(dir string) error { 387 err := s.cacertsWatcher.Add(dir) 388 if err != nil { 389 log.Infof("failed to add cacerts file watcher for %s: %v", dir, err) 390 return err 391 } 392 393 log.Infof("Added cacerts files watcher at %v", dir) 394 395 return nil 396 } 397 398 // initCACertsWatcher initializes the cacerts (/etc/cacerts) directory. 399 // In particular it monitors 'ca-key.pem', 'ca-cert.pem', 'root-cert.pem' 400 // and 'cert-chain.pem'. 401 func (s *Server) initCACertsWatcher() { 402 var err error 403 404 s.cacertsWatcher, err = fsnotify.NewWatcher() 405 if err != nil { 406 log.Warnf("failed to add CAcerts watcher: %v", err) 407 return 408 } 409 410 err = s.addCACertsFileWatcher(LocalCertDir.Get()) 411 if err != nil { 412 log.Warnf("failed to add CAcerts file watcher: %v", err) 413 return 414 } 415 416 go s.handleCACertsFileWatch() 417 } 418 419 // createIstioCA initializes the Istio CA signing functionality. 420 // - for 'plugged in', uses ./etc/cacert directory, mounted from 'cacerts' secret in k8s. 421 // 422 // Inside, the key/cert are 'ca-key.pem' and 'ca-cert.pem'. The root cert signing the intermediate is root-cert.pem, 423 // which may contain multiple roots. A 'cert-chain.pem' file has the full cert chain. 424 func (s *Server) createIstioCA(opts *caOptions) (*ca.IstioCA, error) { 425 var caOpts *ca.IstioCAOptions 426 var detectedSigningCABundle bool 427 var istioGenerated bool 428 var err error 429 430 fileBundle, err := detectSigningCABundle() 431 if err != nil { 432 return nil, fmt.Errorf("unable to determine signing file format %v", err) 433 } 434 if _, err := os.Stat(fileBundle.SigningKeyFile); err == nil { 435 detectedSigningCABundle = true 436 if _, err := os.Stat(path.Join(LocalCertDir.Get(), ca.IstioGenerated)); err == nil { 437 istioGenerated = true 438 } 439 } 440 441 if !detectedSigningCABundle || (features.UseCacertsForSelfSignedCA && istioGenerated) { 442 if features.UseCacertsForSelfSignedCA && istioGenerated { 443 log.Infof("IstioGenerated %s secret found, use it as the CA certificate", ca.CACertsSecret) 444 445 // TODO(jaellio): Currently, when the USE_CACERTS_FOR_SELF_SIGNED_CA flag is true istiod 446 // handles loading and updating the "cacerts" secret with the "istio-generated" key the 447 // same way it handles the "istio-ca-secret" secret. Isitod utilizes a secret watch instead 448 // of file watch to check for secret updates. This may change in the future, and istiod 449 // will watch the file mount instead. 450 } 451 452 // Either the secret is not mounted because it is named `istio-ca-secret`, 453 // or it is `cacerts` secret mounted with "istio-generated" key set. 454 caOpts, err = s.createSelfSignedCACertificateOptions(&fileBundle, opts) 455 if err != nil { 456 return nil, err 457 } 458 caOpts.OnRootCertUpdate = s.updateRootCertAndGenKeyCert 459 } else { 460 // The secret is mounted and the "istio-generated" key is not used. 461 log.Info("Use local CA certificate") 462 463 caOpts, err = ca.NewPluggedCertIstioCAOptions(fileBundle, workloadCertTTL.Get(), maxWorkloadCertTTL.Get(), caRSAKeySize.Get()) 464 if err != nil { 465 return nil, fmt.Errorf("failed to create an istiod CA: %v", err) 466 } 467 468 s.initCACertsWatcher() 469 } 470 istioCA, err := ca.NewIstioCA(caOpts) 471 if err != nil { 472 return nil, fmt.Errorf("failed to create an istiod CA: %v", err) 473 } 474 475 // Start root cert rotator in a separate goroutine. 476 istioCA.Run(s.internalStop) 477 return istioCA, nil 478 } 479 480 func (s *Server) createSelfSignedCACertificateOptions(fileBundle *ca.SigningCAFileBundle, opts *caOptions) (*ca.IstioCAOptions, error) { 481 var caOpts *ca.IstioCAOptions 482 var err error 483 if s.kubeClient != nil { 484 log.Info("Use self-signed certificate as the CA certificate") 485 486 // Abort after 20 minutes. 487 ctx, cancel := context.WithTimeout(context.Background(), time.Minute*20) 488 defer cancel() 489 // rootCertFile will be added to "ca-cert.pem". 490 // readSigningCertOnly set to false - it doesn't seem to be used in Citadel, nor do we have a way 491 // to set it only for one job. 492 caOpts, err = ca.NewSelfSignedIstioCAOptions(ctx, 493 selfSignedRootCertGracePeriodPercentile.Get(), SelfSignedCACertTTL.Get(), 494 selfSignedRootCertCheckInterval.Get(), workloadCertTTL.Get(), 495 maxWorkloadCertTTL.Get(), opts.TrustDomain, features.UseCacertsForSelfSignedCA, true, 496 opts.Namespace, s.kubeClient.Kube().CoreV1(), fileBundle.RootCertFile, 497 enableJitterForRootCertRotator.Get(), caRSAKeySize.Get()) 498 } else { 499 log.Warnf( 500 "Use local self-signed CA certificate for testing. Will use in-memory root CA, no K8S access and no ca key file %s", 501 fileBundle.SigningKeyFile) 502 503 caOpts, err = ca.NewSelfSignedDebugIstioCAOptions(fileBundle.RootCertFile, SelfSignedCACertTTL.Get(), 504 workloadCertTTL.Get(), maxWorkloadCertTTL.Get(), opts.TrustDomain, caRSAKeySize.Get()) 505 } 506 if err != nil { 507 return nil, fmt.Errorf("failed to create a self-signed istiod CA: %v", err) 508 } 509 510 return caOpts, nil 511 } 512 513 // createIstioRA initializes the Istio RA signing functionality. 514 // the caOptions defines the external provider 515 // ca cert can come from three sources, order matters: 516 // 1. Define ca cert via kubernetes secret and mount the secret through `external-ca-cert` volume 517 // 2. Use kubernetes ca cert `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` if signer is 518 // 519 // kubernetes built-in `kubernetes.io/legacy-unknown" signer 520 // 521 // 3. Extract from the cert-chain signed by other CSR signer. 522 func (s *Server) createIstioRA(opts *caOptions) (ra.RegistrationAuthority, error) { 523 caCertFile := path.Join(ra.DefaultExtCACertDir, constants.CACertNamespaceConfigMapDataName) 524 certSignerDomain := opts.CertSignerDomain 525 _, err := os.Stat(caCertFile) 526 if err != nil { 527 if !os.IsNotExist(err) { 528 return nil, fmt.Errorf("failed to get file info: %v", err) 529 } 530 531 // File does not exist. 532 if certSignerDomain == "" { 533 log.Infof("CA cert file %q not found, using %q.", caCertFile, defaultCACertPath) 534 caCertFile = defaultCACertPath 535 } else { 536 log.Infof("CA cert file %q not found - ignoring.", caCertFile) 537 caCertFile = "" 538 } 539 } 540 541 if s.kubeClient == nil { 542 return nil, fmt.Errorf("kubeClient is nil") 543 } 544 raOpts := &ra.IstioRAOptions{ 545 ExternalCAType: opts.ExternalCAType, 546 DefaultCertTTL: workloadCertTTL.Get(), 547 MaxCertTTL: maxWorkloadCertTTL.Get(), 548 CaSigner: opts.ExternalCASigner, 549 CaCertFile: caCertFile, 550 VerifyAppendCA: true, 551 K8sClient: s.kubeClient.Kube(), 552 TrustDomain: opts.TrustDomain, 553 CertSignerDomain: opts.CertSignerDomain, 554 } 555 raServer, err := ra.NewIstioRA(raOpts) 556 if err != nil { 557 return nil, err 558 } 559 raServer.SetCACertificatesFromMeshConfig(s.environment.Mesh().CaCertificates) 560 s.environment.AddMeshHandler(func() { 561 meshConfig := s.environment.Mesh() 562 caCertificates := meshConfig.CaCertificates 563 s.RA.SetCACertificatesFromMeshConfig(caCertificates) 564 }) 565 return raServer, err 566 }