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  }