istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/bootstrap/certcontroller.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  	"crypto/tls"
    20  	"crypto/x509"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"strings"
    25  	"time"
    26  
    27  	"istio.io/istio/pilot/pkg/features"
    28  	tb "istio.io/istio/pilot/pkg/trustbundle"
    29  	"istio.io/istio/pkg/config/constants"
    30  	"istio.io/istio/pkg/log"
    31  	"istio.io/istio/pkg/security"
    32  	"istio.io/istio/pkg/sleep"
    33  	"istio.io/istio/security/pkg/k8s/chiron"
    34  	"istio.io/istio/security/pkg/pki/ca"
    35  	certutil "istio.io/istio/security/pkg/util"
    36  )
    37  
    38  const (
    39  	// defaultCertGracePeriodRatio is the default length of certificate rotation grace period,
    40  	// configured as the ratio of the certificate TTL.
    41  	defaultCertGracePeriodRatio = 0.5
    42  
    43  	// the interval polling root cert and resign istiod cert when it changes.
    44  	rootCertPollingInterval = 60 * time.Second
    45  
    46  	// Default CA certificate path
    47  	// Currently, custom CA path is not supported; no API to get custom CA cert yet.
    48  	defaultCACertPath = "./var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
    49  )
    50  
    51  // initDNSCerts will create the certificates to be used by Istiod GRPC server and webhooks.
    52  // If the certificate creation fails - for example no support in K8S - returns an error.
    53  // Will use the mesh.yaml DiscoveryAddress to find the default expected address of the control plane,
    54  // with an environment variable allowing override.
    55  func (s *Server) initDNSCerts() error {
    56  	var certChain, keyPEM, caBundle []byte
    57  	var err error
    58  	pilotCertProviderName := features.PilotCertProvider
    59  	if strings.HasPrefix(pilotCertProviderName, constants.CertProviderKubernetesSignerPrefix) && s.RA != nil {
    60  		signerName := strings.TrimPrefix(pilotCertProviderName, constants.CertProviderKubernetesSignerPrefix)
    61  		log.Infof("Generating K8S-signed cert for %v using signer %v", s.dnsNames, signerName)
    62  		certChain, keyPEM, _, err = chiron.GenKeyCertK8sCA(s.kubeClient.Kube(),
    63  			strings.Join(s.dnsNames, ","), "", signerName, true, SelfSignedCACertTTL.Get())
    64  		if err != nil {
    65  			return fmt.Errorf("failed generating key and cert by kubernetes: %v", err)
    66  		}
    67  		caBundle, err = s.RA.GetRootCertFromMeshConfig(signerName)
    68  		if err != nil {
    69  			return err
    70  		}
    71  		// MeshConfig:Add callback for mesh config update
    72  		s.environment.AddMeshHandler(func() {
    73  			newCaBundle, _ := s.RA.GetRootCertFromMeshConfig(signerName)
    74  			if newCaBundle != nil && !bytes.Equal(newCaBundle, s.istiodCertBundleWatcher.GetKeyCertBundle().CABundle) {
    75  				newCertChain, newKeyPEM, _, err := chiron.GenKeyCertK8sCA(s.kubeClient.Kube(),
    76  					strings.Join(s.dnsNames, ","), "", signerName, true, SelfSignedCACertTTL.Get())
    77  				if err != nil {
    78  					log.Fatalf("failed regenerating key and cert for istiod by kubernetes: %v", err)
    79  				}
    80  				s.istiodCertBundleWatcher.SetAndNotify(newKeyPEM, newCertChain, newCaBundle)
    81  			}
    82  		})
    83  
    84  		s.addStartFunc("istiod server certificate rotation", func(stop <-chan struct{}) error {
    85  			go func() {
    86  				// Track TTL of DNS cert and renew cert in accordance to grace period.
    87  				s.RotateDNSCertForK8sCA(stop, "", signerName, true, SelfSignedCACertTTL.Get())
    88  			}()
    89  			return nil
    90  		})
    91  	} else if pilotCertProviderName == constants.CertProviderKubernetes {
    92  		log.Infof("Generating K8S-signed cert for %v", s.dnsNames)
    93  		certChain, keyPEM, _, err = chiron.GenKeyCertK8sCA(s.kubeClient.Kube(),
    94  			strings.Join(s.dnsNames, ","), defaultCACertPath, "", true, SelfSignedCACertTTL.Get())
    95  		if err != nil {
    96  			return fmt.Errorf("failed generating key and cert by kubernetes: %v", err)
    97  		}
    98  		caBundle, err = os.ReadFile(defaultCACertPath)
    99  		if err != nil {
   100  			return fmt.Errorf("failed reading %s: %v", defaultCACertPath, err)
   101  		}
   102  
   103  		s.addStartFunc("istiod server certificate rotation", func(stop <-chan struct{}) error {
   104  			go func() {
   105  				// Track TTL of DNS cert and renew cert in accordance to grace period.
   106  				s.RotateDNSCertForK8sCA(stop, defaultCACertPath, "", true, SelfSignedCACertTTL.Get())
   107  			}()
   108  			return nil
   109  		})
   110  	} else if pilotCertProviderName == constants.CertProviderIstiod {
   111  		certChain, keyPEM, err = s.CA.GenKeyCert(s.dnsNames, SelfSignedCACertTTL.Get(), false)
   112  		if err != nil {
   113  			return fmt.Errorf("failed generating istiod key cert %v", err)
   114  		}
   115  		log.Infof("Generating istiod-signed cert for %v:\n %s", s.dnsNames, certChain)
   116  
   117  		fileBundle, err := detectSigningCABundle()
   118  		if err != nil {
   119  			return fmt.Errorf("unable to determine signing file format %v", err)
   120  		}
   121  
   122  		istioGenerated, detectedSigningCABundle := false, false
   123  		if _, err := os.Stat(fileBundle.SigningKeyFile); err == nil {
   124  			detectedSigningCABundle = true
   125  			if _, err := os.Stat(path.Join(LocalCertDir.Get(), ca.IstioGenerated)); err == nil {
   126  				istioGenerated = true
   127  			}
   128  		}
   129  		// check if signing key file exists the cert dir and if the istio-generated file
   130  		// exists (only if USE_CACERTS_FOR_SELF_SIGNED_CA is enabled)
   131  		if !detectedSigningCABundle || (features.UseCacertsForSelfSignedCA && istioGenerated) {
   132  			log.Infof("Use istio-generated cacerts at %v or istio-ca-secret", fileBundle.SigningKeyFile)
   133  
   134  			caBundle = s.CA.GetCAKeyCertBundle().GetRootCertPem()
   135  			s.addStartFunc("istiod server certificate rotation", func(stop <-chan struct{}) error {
   136  				go func() {
   137  					// regenerate istiod key cert when root cert changes.
   138  					s.watchRootCertAndGenKeyCert(stop)
   139  				}()
   140  				return nil
   141  			})
   142  		} else {
   143  			log.Infof("DNS certs use plugged-in cert at %v", fileBundle.SigningKeyFile)
   144  
   145  			caBundle, err = os.ReadFile(fileBundle.RootCertFile)
   146  			if err != nil {
   147  				return fmt.Errorf("failed reading %s: %v", fileBundle.RootCertFile, err)
   148  			}
   149  		}
   150  	} else {
   151  		customCACertPath := security.DefaultRootCertFilePath
   152  		log.Infof("User specified cert provider: %v, mounted in a well known location %v",
   153  			features.PilotCertProvider, customCACertPath)
   154  		caBundle, err = os.ReadFile(customCACertPath)
   155  		if err != nil {
   156  			return fmt.Errorf("failed reading %s: %v", customCACertPath, err)
   157  		}
   158  	}
   159  	s.istiodCertBundleWatcher.SetAndNotify(keyPEM, certChain, caBundle)
   160  	return nil
   161  }
   162  
   163  // TODO(hzxuzonghu): support async notification instead of polling the CA root cert.
   164  func (s *Server) watchRootCertAndGenKeyCert(stop <-chan struct{}) {
   165  	caBundle := s.CA.GetCAKeyCertBundle().GetRootCertPem()
   166  	for {
   167  		if !sleep.Until(stop, rootCertPollingInterval) {
   168  			return
   169  		}
   170  		newRootCert := s.CA.GetCAKeyCertBundle().GetRootCertPem()
   171  		if !bytes.Equal(caBundle, newRootCert) {
   172  			caBundle = newRootCert
   173  			certChain, keyPEM, err := s.CA.GenKeyCert(s.dnsNames, SelfSignedCACertTTL.Get(), false)
   174  			if err != nil {
   175  				log.Errorf("failed generating istiod key cert %v", err)
   176  			} else {
   177  				s.istiodCertBundleWatcher.SetAndNotify(keyPEM, certChain, caBundle)
   178  				log.Infof("regenerated istiod dns cert: %s", certChain)
   179  			}
   180  		}
   181  	}
   182  }
   183  
   184  func (s *Server) RotateDNSCertForK8sCA(stop <-chan struct{},
   185  	defaultCACertPath string,
   186  	signerName string,
   187  	approveCsr bool,
   188  	requestedLifetime time.Duration,
   189  ) {
   190  	certUtil := certutil.NewCertUtil(int(defaultCertGracePeriodRatio * 100))
   191  	for {
   192  		waitTime, _ := certUtil.GetWaitTime(s.istiodCertBundleWatcher.GetKeyCertBundle().CertPem, time.Now())
   193  		if !sleep.Until(stop, waitTime) {
   194  			return
   195  		}
   196  		certChain, keyPEM, _, err := chiron.GenKeyCertK8sCA(s.kubeClient.Kube(),
   197  			strings.Join(s.dnsNames, ","), defaultCACertPath, signerName, approveCsr, requestedLifetime)
   198  		if err != nil {
   199  			log.Errorf("failed regenerating key and cert for istiod by kubernetes: %v", err)
   200  			continue
   201  		}
   202  		s.istiodCertBundleWatcher.SetAndNotify(keyPEM, certChain, s.istiodCertBundleWatcher.GetCABundle())
   203  	}
   204  }
   205  
   206  // updateRootCertAndGenKeyCert when CA certs is updated, it generates new dns certs and notifies keycertbundle about the changes
   207  func (s *Server) updateRootCertAndGenKeyCert() error {
   208  	log.Infof("update root cert and generate new dns certs")
   209  	caBundle := s.CA.GetCAKeyCertBundle().GetRootCertPem()
   210  	certChain, keyPEM, err := s.CA.GenKeyCert(s.dnsNames, SelfSignedCACertTTL.Get(), false)
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	if features.MultiRootMesh {
   216  		// Trigger trust anchor update, this will send PCDS to all sidecars.
   217  		log.Infof("Update trust anchor with new root cert")
   218  		err = s.workloadTrustBundle.UpdateTrustAnchor(&tb.TrustAnchorUpdate{
   219  			TrustAnchorConfig: tb.TrustAnchorConfig{Certs: []string{string(caBundle)}},
   220  			Source:            tb.SourceIstioCA,
   221  		})
   222  		if err != nil {
   223  			log.Errorf("failed to update trust anchor from source Istio CA, err: %v", err)
   224  			return err
   225  		}
   226  	}
   227  
   228  	s.istiodCertBundleWatcher.SetAndNotify(keyPEM, certChain, caBundle)
   229  	return nil
   230  }
   231  
   232  // initCertificateWatches sets up watches for the plugin dns certs.
   233  func (s *Server) initCertificateWatches(tlsOptions TLSOptions) error {
   234  	if err := s.istiodCertBundleWatcher.SetFromFilesAndNotify(tlsOptions.KeyFile, tlsOptions.CertFile, tlsOptions.CaCertFile); err != nil {
   235  		return fmt.Errorf("set keyCertBundle failed: %v", err)
   236  	}
   237  	// TODO: Setup watcher for root and restart server if it changes.
   238  	for _, file := range []string{tlsOptions.CertFile, tlsOptions.KeyFile} {
   239  		log.Infof("adding watcher for certificate %s", file)
   240  		if err := s.fileWatcher.Add(file); err != nil {
   241  			return fmt.Errorf("could not watch %v: %v", file, err)
   242  		}
   243  	}
   244  	s.addStartFunc("certificate rotation", func(stop <-chan struct{}) error {
   245  		go func() {
   246  			var keyCertTimerC <-chan time.Time
   247  			for {
   248  				select {
   249  				case <-keyCertTimerC:
   250  					keyCertTimerC = nil
   251  					if err := s.istiodCertBundleWatcher.SetFromFilesAndNotify(tlsOptions.KeyFile, tlsOptions.CertFile, tlsOptions.CaCertFile); err != nil {
   252  						log.Errorf("Setting keyCertBundle failed: %v", err)
   253  					}
   254  				case <-s.fileWatcher.Events(tlsOptions.CertFile):
   255  					if keyCertTimerC == nil {
   256  						keyCertTimerC = time.After(watchDebounceDelay)
   257  					}
   258  				case <-s.fileWatcher.Events(tlsOptions.KeyFile):
   259  					if keyCertTimerC == nil {
   260  						keyCertTimerC = time.After(watchDebounceDelay)
   261  					}
   262  				case err := <-s.fileWatcher.Errors(tlsOptions.CertFile):
   263  					log.Errorf("error watching %v: %v", tlsOptions.CertFile, err)
   264  				case err := <-s.fileWatcher.Errors(tlsOptions.KeyFile):
   265  					log.Errorf("error watching %v: %v", tlsOptions.KeyFile, err)
   266  				case <-stop:
   267  					return
   268  				}
   269  			}
   270  		}()
   271  		return nil
   272  	})
   273  	return nil
   274  }
   275  
   276  func (s *Server) reloadIstiodCert(watchCh <-chan struct{}, stopCh <-chan struct{}) {
   277  	for {
   278  		select {
   279  		case <-stopCh:
   280  			return
   281  		case <-watchCh:
   282  			if err := s.loadIstiodCert(); err != nil {
   283  				log.Errorf("reload istiod cert failed: %v", err)
   284  			}
   285  		}
   286  	}
   287  }
   288  
   289  // loadIstiodCert load IstiodCert received from watchCh once
   290  func (s *Server) loadIstiodCert() error {
   291  	keyCertBundle := s.istiodCertBundleWatcher.GetKeyCertBundle()
   292  	keyPair, err := tls.X509KeyPair(keyCertBundle.CertPem, keyCertBundle.KeyPem)
   293  	if err != nil {
   294  		return fmt.Errorf("istiod loading x509 key pairs failed: %v", err)
   295  	}
   296  	for _, c := range keyPair.Certificate {
   297  		x509Cert, err := x509.ParseCertificates(c)
   298  		if err != nil {
   299  			// This can rarely happen, just in case.
   300  			return fmt.Errorf("x509 cert - ParseCertificates() error: %v", err)
   301  		}
   302  		for _, c := range x509Cert {
   303  			log.Infof("x509 cert - Issuer: %q, Subject: %q, SN: %x, NotBefore: %q, NotAfter: %q",
   304  				c.Issuer, c.Subject, c.SerialNumber,
   305  				c.NotBefore.Format(time.RFC3339), c.NotAfter.Format(time.RFC3339))
   306  		}
   307  	}
   308  
   309  	log.Info("Istiod certificates are reloaded")
   310  	s.certMu.Lock()
   311  	s.istiodCert = &keyPair
   312  	s.certMu.Unlock()
   313  	return nil
   314  }