istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/sds.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 xds 16 17 import ( 18 "crypto/x509" 19 "encoding/pem" 20 "fmt" 21 "strconv" 22 "strings" 23 "time" 24 25 xxhashv2 "github.com/cespare/xxhash/v2" 26 cryptomb "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/private_key_providers/cryptomb/v3alpha" 27 qat "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/private_key_providers/qat/v3alpha" 28 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 29 envoytls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" 30 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 31 "google.golang.org/protobuf/types/known/anypb" 32 "google.golang.org/protobuf/types/known/durationpb" 33 34 mesh "istio.io/api/mesh/v1alpha1" 35 credscontroller "istio.io/istio/pilot/pkg/credentials" 36 "istio.io/istio/pilot/pkg/features" 37 "istio.io/istio/pilot/pkg/model" 38 "istio.io/istio/pilot/pkg/model/credentials" 39 securitymodel "istio.io/istio/pilot/pkg/security/model" 40 "istio.io/istio/pilot/pkg/util/protoconv" 41 "istio.io/istio/pkg/cluster" 42 "istio.io/istio/pkg/config/schema/kind" 43 "istio.io/istio/pkg/util/sets" 44 ) 45 46 // SecretResource wraps the authnmodel type with cache functions implemented 47 type SecretResource struct { 48 credentials.SecretResource 49 pkpConfHash string 50 } 51 52 var _ model.XdsCacheEntry = SecretResource{} 53 54 func (sr SecretResource) Type() string { 55 return model.SDSType 56 } 57 58 func (sr SecretResource) Key() any { 59 return sr.SecretResource.Key() + "/" + sr.pkpConfHash 60 } 61 62 func (sr SecretResource) DependentConfigs() []model.ConfigHash { 63 configs := []model.ConfigHash{} 64 for _, config := range relatedConfigs(model.ConfigKey{Kind: kind.Secret, Name: sr.Name, Namespace: sr.Namespace}) { 65 configs = append(configs, config.HashCode()) 66 } 67 return configs 68 } 69 70 func (sr SecretResource) Cacheable() bool { 71 return true 72 } 73 74 func sdsNeedsPush(updates model.XdsUpdates) bool { 75 if len(updates) == 0 { 76 return true 77 } 78 for update := range updates { 79 switch update.Kind { 80 case kind.Secret: 81 return true 82 case kind.ReferenceGrant: 83 return true 84 } 85 } 86 return false 87 } 88 89 // parseResources parses a list of resource names to SecretResource types, for a given proxy. 90 // Invalid resource names are ignored 91 func (s *SecretGen) parseResources(names []string, proxy *model.Proxy) []SecretResource { 92 res := make([]SecretResource, 0, len(names)) 93 pkpConf := (*mesh.ProxyConfig)(proxy.Metadata.ProxyConfig).GetPrivateKeyProvider() 94 pkpConfHashStr := "" 95 if pkpConf != nil { 96 pkpConfHashStr = strconv.FormatUint(xxhashv2.Sum64String(pkpConf.String()), 10) 97 } 98 for _, resource := range names { 99 sr, err := credentials.ParseResourceName(resource, proxy.VerifiedIdentity.Namespace, proxy.Metadata.ClusterID, s.configCluster) 100 if err != nil { 101 pilotSDSCertificateErrors.Increment() 102 log.Warnf("error parsing resource name: %v", err) 103 continue 104 } 105 res = append(res, SecretResource{sr, pkpConfHashStr}) 106 } 107 return res 108 } 109 110 func (s *SecretGen) Generate(proxy *model.Proxy, w *model.WatchedResource, req *model.PushRequest) (model.Resources, model.XdsLogDetails, error) { 111 if proxy.VerifiedIdentity == nil { 112 log.Warnf("proxy %s is not authorized to receive credscontroller. Ensure you are connecting over TLS port and are authenticated.", proxy.ID) 113 return nil, model.DefaultXdsLogDetails, nil 114 } 115 if req == nil || !sdsNeedsPush(req.ConfigsUpdated) { 116 return nil, model.DefaultXdsLogDetails, nil 117 } 118 var updatedSecrets sets.Set[model.ConfigKey] 119 if !req.Full { 120 updatedSecrets = model.ConfigsOfKind(req.ConfigsUpdated, kind.Secret) 121 } 122 123 proxyClusterSecrets, err := s.secrets.ForCluster(proxy.Metadata.ClusterID) 124 if err != nil { 125 log.Warnf("proxy %s is from an unknown cluster, cannot retrieve certificates: %v", proxy.ID, err) 126 pilotSDSCertificateErrors.Increment() 127 return nil, model.DefaultXdsLogDetails, nil 128 } 129 configClusterSecrets, err := s.secrets.ForCluster(s.configCluster) 130 if err != nil { 131 log.Warnf("config cluster %s not found, cannot retrieve certificates: %v", s.configCluster, err) 132 pilotSDSCertificateErrors.Increment() 133 return nil, model.DefaultXdsLogDetails, nil 134 } 135 136 // Filter down to resources we can access. We do not return an error if they attempt to access a Secret 137 // they cannot; instead we just exclude it. This ensures that a single bad reference does not break the whole 138 // SDS flow. The pilotSDSCertificateErrors metric and logs handle visibility into invalid references. 139 resources := filterAuthorizedResources(s.parseResources(w.ResourceNames, proxy), proxy, proxyClusterSecrets) 140 141 var results model.Resources 142 cached, regenerated := 0, 0 143 for _, sr := range resources { 144 if updatedSecrets != nil { 145 if !containsAny(updatedSecrets, relatedConfigs(model.ConfigKey{Kind: kind.Secret, Name: sr.Name, Namespace: sr.Namespace})) { 146 // This is an incremental update, filter out secrets that are not updated. 147 continue 148 } 149 } 150 151 cachedItem := s.cache.Get(sr) 152 if cachedItem != nil && !features.EnableUnsafeAssertions { 153 // If it is in the Cache, add it and continue 154 // We skip cache if assertions are enabled, so that the cache will assert our eviction logic is correct 155 results = append(results, cachedItem) 156 cached++ 157 continue 158 } 159 regenerated++ 160 res := s.generate(sr, configClusterSecrets, proxyClusterSecrets, proxy) 161 if res != nil { 162 s.cache.Add(sr, req, res) 163 results = append(results, res) 164 } 165 } 166 return results, model.XdsLogDetails{ 167 Incremental: updatedSecrets != nil, 168 AdditionalInfo: fmt.Sprintf("cached:%v/%v", cached, cached+regenerated), 169 }, nil 170 } 171 172 func (s *SecretGen) generate(sr SecretResource, configClusterSecrets, proxyClusterSecrets credscontroller.Controller, proxy *model.Proxy) *discovery.Resource { 173 // Fetch the appropriate cluster's secret, based on the credential type 174 var secretController credscontroller.Controller 175 switch sr.ResourceType { 176 case credentials.KubernetesGatewaySecretType: 177 secretController = configClusterSecrets 178 default: 179 secretController = proxyClusterSecrets 180 } 181 182 isCAOnlySecret := strings.HasSuffix(sr.Name, securitymodel.SdsCaSuffix) 183 if isCAOnlySecret { 184 caCertInfo, err := secretController.GetCaCert(sr.Name, sr.Namespace) 185 if err != nil { 186 pilotSDSCertificateErrors.Increment() 187 log.Warnf("failed to fetch ca certificate for %s: %v", sr.ResourceName, err) 188 return nil 189 } 190 if err := ValidateCertificate(caCertInfo.Cert); err != nil { 191 recordInvalidCertificate(sr.ResourceName, err) 192 } 193 res := toEnvoyCaSecret(sr.ResourceName, caCertInfo) 194 return res 195 } 196 certInfo, err := secretController.GetCertInfo(sr.Name, sr.Namespace) 197 if err != nil { 198 pilotSDSCertificateErrors.Increment() 199 log.Warnf("failed to fetch key and certificate for %s: %v", sr.ResourceName, err) 200 return nil 201 } 202 if err := ValidateCertificate(certInfo.Cert); err != nil { 203 recordInvalidCertificate(sr.ResourceName, err) 204 } 205 res := toEnvoyTLSSecret(sr.ResourceName, certInfo, proxy, s.meshConfig) 206 return res 207 } 208 209 func ValidateCertificate(data []byte) error { 210 block, _ := pem.Decode(data) 211 if block == nil { 212 return fmt.Errorf("pem decode failed") 213 } 214 certs, err := x509.ParseCertificates(block.Bytes) 215 if err != nil { 216 return err 217 } 218 now := time.Now() 219 for _, cert := range certs { 220 // check if the certificate has expired 221 if now.After(cert.NotAfter) || now.Before(cert.NotBefore) { 222 return fmt.Errorf("certificate is expired or not yet valid") 223 } 224 } 225 return nil 226 } 227 228 func recordInvalidCertificate(name string, err error) { 229 pilotSDSCertificateErrors.Increment() 230 log.Warnf("invalid certificates: %q: %v", name, err) 231 } 232 233 // filterAuthorizedResources takes a list of SecretResource and filters out resources that proxy cannot access 234 func filterAuthorizedResources(resources []SecretResource, proxy *model.Proxy, secrets credscontroller.Controller) []SecretResource { 235 var authzResult *bool 236 var authzError error 237 // isAuthorized is a small wrapper around credscontroller.Authorize so we only call it once instead of each time in the loop 238 isAuthorized := func() bool { 239 if authzResult != nil { 240 return *authzResult 241 } 242 res := false 243 if err := secrets.Authorize(proxy.VerifiedIdentity.ServiceAccount, proxy.VerifiedIdentity.Namespace); err == nil { 244 res = true 245 } else { 246 authzError = err 247 } 248 authzResult = &res 249 return res 250 } 251 252 // There are 4 cases of secret reference 253 // Verified cross namespace (by ReferencePolicy). No Authz needed. 254 // Verified same namespace (implicit). No Authz needed. 255 // Unverified cross namespace. Never allowed. 256 // Unverified same namespace. Allowed if authorized. 257 allowedResources := make([]SecretResource, 0, len(resources)) 258 deniedResources := make([]string, 0) 259 for _, r := range resources { 260 sameNamespace := r.Namespace == proxy.VerifiedIdentity.Namespace 261 verified := proxy.MergedGateway != nil && proxy.MergedGateway.VerifiedCertificateReferences.Contains(r.ResourceName) 262 switch r.ResourceType { 263 case credentials.KubernetesGatewaySecretType: 264 // For KubernetesGateway, we only allow VerifiedCertificateReferences. 265 // This means a Secret in the same namespace as the Gateway (which also must be in the same namespace 266 // as the proxy), or a ReferencePolicy allowing the reference. 267 if verified { 268 allowedResources = append(allowedResources, r) 269 } else { 270 deniedResources = append(deniedResources, r.Name) 271 } 272 case credentials.KubernetesSecretType: 273 // For Kubernetes, we require the secret to be in the same namespace as the proxy and for it to be 274 // authorized for access. 275 if sameNamespace && isAuthorized() { 276 allowedResources = append(allowedResources, r) 277 } else { 278 deniedResources = append(deniedResources, r.Name) 279 } 280 default: 281 // Should never happen 282 log.Warnf("unknown credential type %q", r.Type) 283 pilotSDSCertificateErrors.Increment() 284 } 285 } 286 287 // If we filtered any out, report an error. We aggregate errors in one place here, rather than in the loop, 288 // to avoid excessive logs. 289 if len(deniedResources) > 0 { 290 errMessage := authzError 291 if errMessage == nil { 292 errMessage = fmt.Errorf("cross namespace secret reference requires ReferencePolicy") 293 } 294 log.Warnf("proxy %s attempted to access unauthorized certificates %s: %v", proxy.ID, atMostNJoin(deniedResources, 3), errMessage) 295 pilotSDSCertificateErrors.Increment() 296 } 297 298 return allowedResources 299 } 300 301 func toEnvoyCaSecret(name string, certInfo *credscontroller.CertInfo) *discovery.Resource { 302 validationContext := &envoytls.CertificateValidationContext{ 303 TrustedCa: &core.DataSource{ 304 Specifier: &core.DataSource_InlineBytes{ 305 InlineBytes: certInfo.Cert, 306 }, 307 }, 308 } 309 if certInfo.CRL != nil { 310 validationContext.Crl = &core.DataSource{ 311 Specifier: &core.DataSource_InlineBytes{ 312 InlineBytes: certInfo.CRL, 313 }, 314 } 315 } 316 res := protoconv.MessageToAny(&envoytls.Secret{ 317 Name: name, 318 Type: &envoytls.Secret_ValidationContext{ 319 ValidationContext: validationContext, 320 }, 321 }) 322 return &discovery.Resource{ 323 Name: name, 324 Resource: res, 325 } 326 } 327 328 func toEnvoyTLSSecret(name string, certInfo *credscontroller.CertInfo, proxy *model.Proxy, meshConfig *mesh.MeshConfig) *discovery.Resource { 329 var res *anypb.Any 330 pkpConf := proxy.Metadata.ProxyConfigOrDefault(meshConfig.GetDefaultConfig()).GetPrivateKeyProvider() 331 switch pkpConf.GetProvider().(type) { 332 case *mesh.PrivateKeyProvider_Cryptomb: 333 crypto := pkpConf.GetCryptomb() 334 msg := protoconv.MessageToAny(&cryptomb.CryptoMbPrivateKeyMethodConfig{ 335 PollDelay: durationpb.New(time.Duration(crypto.GetPollDelay().Nanos)), 336 PrivateKey: &core.DataSource{ 337 Specifier: &core.DataSource_InlineBytes{ 338 InlineBytes: certInfo.Key, 339 }, 340 }, 341 }) 342 res = protoconv.MessageToAny(&envoytls.Secret{ 343 Name: name, 344 Type: &envoytls.Secret_TlsCertificate{ 345 TlsCertificate: &envoytls.TlsCertificate{ 346 CertificateChain: &core.DataSource{ 347 Specifier: &core.DataSource_InlineBytes{ 348 InlineBytes: certInfo.Cert, 349 }, 350 }, 351 PrivateKeyProvider: &envoytls.PrivateKeyProvider{ 352 ProviderName: "cryptomb", 353 ConfigType: &envoytls.PrivateKeyProvider_TypedConfig{ 354 TypedConfig: msg, 355 }, 356 Fallback: crypto.GetFallback().GetValue(), 357 }, 358 }, 359 }, 360 }) 361 case *mesh.PrivateKeyProvider_Qat: 362 qatConf := pkpConf.GetQat() 363 msg := protoconv.MessageToAny(&qat.QatPrivateKeyMethodConfig{ 364 PollDelay: durationpb.New(time.Duration(qatConf.GetPollDelay().Nanos)), 365 PrivateKey: &core.DataSource{ 366 Specifier: &core.DataSource_InlineBytes{ 367 InlineBytes: certInfo.Key, 368 }, 369 }, 370 }) 371 res = protoconv.MessageToAny(&envoytls.Secret{ 372 Name: name, 373 Type: &envoytls.Secret_TlsCertificate{ 374 TlsCertificate: &envoytls.TlsCertificate{ 375 CertificateChain: &core.DataSource{ 376 Specifier: &core.DataSource_InlineBytes{ 377 InlineBytes: certInfo.Cert, 378 }, 379 }, 380 PrivateKeyProvider: &envoytls.PrivateKeyProvider{ 381 ProviderName: "qat", 382 ConfigType: &envoytls.PrivateKeyProvider_TypedConfig{ 383 TypedConfig: msg, 384 }, 385 Fallback: qatConf.GetFallback().GetValue(), 386 }, 387 }, 388 }, 389 }) 390 default: 391 tlsCertificate := &envoytls.TlsCertificate{ 392 CertificateChain: &core.DataSource{ 393 Specifier: &core.DataSource_InlineBytes{ 394 InlineBytes: certInfo.Cert, 395 }, 396 }, 397 PrivateKey: &core.DataSource{ 398 Specifier: &core.DataSource_InlineBytes{ 399 InlineBytes: certInfo.Key, 400 }, 401 }, 402 } 403 if certInfo.Staple != nil { 404 tlsCertificate.OcspStaple = &core.DataSource{ 405 Specifier: &core.DataSource_InlineBytes{ 406 InlineBytes: certInfo.Staple, 407 }, 408 } 409 } 410 res = protoconv.MessageToAny(&envoytls.Secret{ 411 Name: name, 412 Type: &envoytls.Secret_TlsCertificate{ 413 TlsCertificate: tlsCertificate, 414 }, 415 }) 416 } 417 return &discovery.Resource{ 418 Name: name, 419 Resource: res, 420 } 421 } 422 423 func containsAny(mp sets.Set[model.ConfigKey], keys []model.ConfigKey) bool { 424 for _, k := range keys { 425 if _, f := mp[k]; f { 426 return true 427 } 428 } 429 return false 430 } 431 432 // relatedConfigs maps a single resource to a list of relevant resources. This is used for cache invalidation 433 // and push skipping. This is because an secret potentially has a dependency on the same secret with or without 434 // the -cacert suffix. By including this dependency we ensure we do not miss any updates. 435 // This is important for cases where we have a compound secret. In this case, the `foo` secret may update, 436 // but we need to push both the `foo` and `foo-cacert` resource name, or they will fall out of sync. 437 func relatedConfigs(k model.ConfigKey) []model.ConfigKey { 438 related := []model.ConfigKey{k} 439 // For secret without -cacert suffix, add the suffix 440 if !strings.HasSuffix(k.Name, securitymodel.SdsCaSuffix) { 441 k.Name += securitymodel.SdsCaSuffix 442 related = append(related, k) 443 } else { 444 // For secret with -cacert suffix, remove the suffix 445 k.Name = strings.TrimSuffix(k.Name, securitymodel.SdsCaSuffix) 446 related = append(related, k) 447 } 448 return related 449 } 450 451 type SecretGen struct { 452 secrets credscontroller.MulticlusterController 453 // Cache for XDS resources 454 cache model.XdsCache 455 configCluster cluster.ID 456 meshConfig *mesh.MeshConfig 457 } 458 459 var _ model.XdsResourceGenerator = &SecretGen{} 460 461 func NewSecretGen(sc credscontroller.MulticlusterController, cache model.XdsCache, configCluster cluster.ID, 462 meshConfig *mesh.MeshConfig, 463 ) *SecretGen { 464 // TODO: Currently we only have a single credentials controller (Kubernetes). In the future, we will need a mapping 465 // of resource type to secret controller (ie kubernetes:// -> KubernetesController, vault:// -> VaultController) 466 return &SecretGen{ 467 secrets: sc, 468 cache: cache, 469 configCluster: configCluster, 470 meshConfig: meshConfig, 471 } 472 }