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 }