istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/sds_test.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_test
    16  
    17  import (
    18  	"errors"
    19  	"os"
    20  	"path/filepath"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"google.golang.org/protobuf/types/known/durationpb"
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/client-go/kubernetes/fake"
    30  	k8stesting "k8s.io/client-go/testing"
    31  
    32  	meshconfig "istio.io/api/mesh/v1alpha1"
    33  	credentials "istio.io/istio/pilot/pkg/credentials/kube"
    34  	"istio.io/istio/pilot/pkg/model"
    35  	v3 "istio.io/istio/pilot/pkg/xds/v3"
    36  	"istio.io/istio/pilot/test/xds"
    37  	"istio.io/istio/pilot/test/xdstest"
    38  	"istio.io/istio/pkg/config/constants"
    39  	"istio.io/istio/pkg/config/schema/kind"
    40  	"istio.io/istio/pkg/kube"
    41  	"istio.io/istio/pkg/spiffe"
    42  	"istio.io/istio/pkg/test/env"
    43  	"istio.io/istio/pkg/util/sets"
    44  	xdsserver "istio.io/istio/pkg/xds"
    45  )
    46  
    47  func makeSecret(name string, data map[string]string) *corev1.Secret {
    48  	bdata := map[string][]byte{}
    49  	for k, v := range data {
    50  		bdata[k] = []byte(v)
    51  	}
    52  	return &corev1.Secret{
    53  		ObjectMeta: metav1.ObjectMeta{
    54  			Name:      name,
    55  			Namespace: "istio-system",
    56  		},
    57  		Data: bdata,
    58  	}
    59  }
    60  
    61  var (
    62  	certDir     = filepath.Join(env.IstioSrc, "./tests/testdata/certs")
    63  	genericCert = makeSecret("generic", map[string]string{
    64  		credentials.GenericScrtCert: readFile(filepath.Join(certDir, "default/cert-chain.pem")),
    65  		credentials.GenericScrtKey:  readFile(filepath.Join(certDir, "default/key.pem")),
    66  	})
    67  	genericMtlsCert = makeSecret("generic-mtls", map[string]string{
    68  		credentials.GenericScrtCert:   readFile(filepath.Join(certDir, "dns/cert-chain.pem")),
    69  		credentials.GenericScrtKey:    readFile(filepath.Join(certDir, "dns/key.pem")),
    70  		credentials.GenericScrtCaCert: readFile(filepath.Join(certDir, "dns/root-cert.pem")),
    71  	})
    72  	genericMtlsCertCrl = makeSecret("generic-mtls-crl", map[string]string{
    73  		credentials.GenericScrtCert:   readFile(filepath.Join(certDir, "dns/cert-chain.pem")),
    74  		credentials.GenericScrtKey:    readFile(filepath.Join(certDir, "dns/key.pem")),
    75  		credentials.GenericScrtCaCert: readFile(filepath.Join(certDir, "dns/root-cert.pem")),
    76  		credentials.GenericScrtCRL:    readFile(filepath.Join(certDir, "dns/cert-chain.pem")),
    77  	})
    78  	genericMtlsCertSplit = makeSecret("generic-mtls-split", map[string]string{
    79  		credentials.GenericScrtCert: readFile(filepath.Join(certDir, "mountedcerts-client/cert-chain.pem")),
    80  		credentials.GenericScrtKey:  readFile(filepath.Join(certDir, "mountedcerts-client/key.pem")),
    81  	})
    82  	genericMtlsCertSplitCa = makeSecret("generic-mtls-split-cacert", map[string]string{
    83  		credentials.GenericScrtCaCert: readFile(filepath.Join(certDir, "mountedcerts-client/root-cert.pem")),
    84  	})
    85  )
    86  
    87  func readFile(name string) string {
    88  	cacert, _ := os.ReadFile(name)
    89  	return string(cacert)
    90  }
    91  
    92  func TestGenerateSDS(t *testing.T) {
    93  	type Expected struct {
    94  		Key    string
    95  		Cert   string
    96  		CaCert string
    97  		CaCrl  string
    98  	}
    99  	allResources := []string{
   100  		"kubernetes://generic", "kubernetes://generic-mtls", "kubernetes://generic-mtls-cacert",
   101  		"kubernetes://generic-mtls-split", "kubernetes://generic-mtls-split-cacert", "kubernetes://generic-mtls-crl",
   102  		"kubernetes://generic-mtls-crl-cacert",
   103  	}
   104  	cases := []struct {
   105  		name                 string
   106  		proxy                *model.Proxy
   107  		resources            []string
   108  		request              *model.PushRequest
   109  		expect               map[string]Expected
   110  		accessReviewResponse func(action k8stesting.Action) (bool, runtime.Object, error)
   111  	}{
   112  		{
   113  			name:      "simple",
   114  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   115  			resources: []string{"kubernetes://generic"},
   116  			request:   &model.PushRequest{Full: true},
   117  			expect: map[string]Expected{
   118  				"kubernetes://generic": {
   119  					Key:  string(genericCert.Data[credentials.GenericScrtKey]),
   120  					Cert: string(genericCert.Data[credentials.GenericScrtCert]),
   121  				},
   122  			},
   123  		},
   124  		{
   125  			name:      "sidecar",
   126  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}},
   127  			resources: []string{"kubernetes://generic"},
   128  			request:   &model.PushRequest{Full: true},
   129  			expect: map[string]Expected{
   130  				"kubernetes://generic": {
   131  					Key:  string(genericCert.Data[credentials.GenericScrtKey]),
   132  					Cert: string(genericCert.Data[credentials.GenericScrtCert]),
   133  				},
   134  			},
   135  		},
   136  		{
   137  			name:      "unauthenticated",
   138  			proxy:     &model.Proxy{Type: model.Router},
   139  			resources: []string{"kubernetes://generic"},
   140  			request:   &model.PushRequest{Full: true},
   141  			expect:    map[string]Expected{},
   142  		},
   143  		{
   144  			name:      "multiple",
   145  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   146  			resources: allResources,
   147  			request:   &model.PushRequest{Full: true},
   148  			expect: map[string]Expected{
   149  				"kubernetes://generic": {
   150  					Key:  string(genericCert.Data[credentials.GenericScrtKey]),
   151  					Cert: string(genericCert.Data[credentials.GenericScrtCert]),
   152  				},
   153  				"kubernetes://generic-mtls": {
   154  					Key:  string(genericMtlsCert.Data[credentials.GenericScrtKey]),
   155  					Cert: string(genericMtlsCert.Data[credentials.GenericScrtCert]),
   156  				},
   157  				"kubernetes://generic-mtls-cacert": {
   158  					CaCert: string(genericMtlsCert.Data[credentials.GenericScrtCaCert]),
   159  				},
   160  				"kubernetes://generic-mtls-split": {
   161  					Key:  string(genericMtlsCertSplit.Data[credentials.GenericScrtKey]),
   162  					Cert: string(genericMtlsCertSplit.Data[credentials.GenericScrtCert]),
   163  				},
   164  				"kubernetes://generic-mtls-split-cacert": {
   165  					CaCert: string(genericMtlsCertSplitCa.Data[credentials.GenericScrtCaCert]),
   166  				},
   167  				"kubernetes://generic-mtls-crl": {
   168  					Key:  string(genericMtlsCertCrl.Data[credentials.GenericScrtKey]),
   169  					Cert: string(genericMtlsCertCrl.Data[credentials.GenericScrtCert]),
   170  				},
   171  				"kubernetes://generic-mtls-crl-cacert": {
   172  					CaCert: string(genericMtlsCertCrl.Data[credentials.GenericScrtCaCert]),
   173  					CaCrl:  string(genericMtlsCertCrl.Data[credentials.GenericScrtCRL]),
   174  				},
   175  			},
   176  		},
   177  		{
   178  			name:      "full push with updates",
   179  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   180  			resources: []string{"kubernetes://generic", "kubernetes://generic-mtls", "kubernetes://generic-mtls-cacert"},
   181  			request: &model.PushRequest{Full: true, ConfigsUpdated: sets.New(model.ConfigKey{
   182  				Kind:      kind.Secret,
   183  				Name:      "generic-mtls",
   184  				Namespace: "istio-system",
   185  			})},
   186  			expect: map[string]Expected{
   187  				"kubernetes://generic": {
   188  					Key:  string(genericCert.Data[credentials.GenericScrtKey]),
   189  					Cert: string(genericCert.Data[credentials.GenericScrtCert]),
   190  				},
   191  				"kubernetes://generic-mtls": {
   192  					Key:  string(genericMtlsCert.Data[credentials.GenericScrtKey]),
   193  					Cert: string(genericMtlsCert.Data[credentials.GenericScrtCert]),
   194  				},
   195  				"kubernetes://generic-mtls-cacert": {
   196  					CaCert: string(genericMtlsCert.Data[credentials.GenericScrtCaCert]),
   197  				},
   198  			},
   199  		},
   200  		{
   201  			name:      "incremental push with updates",
   202  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   203  			resources: allResources,
   204  			request:   &model.PushRequest{Full: false, ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.Secret, Name: "generic", Namespace: "istio-system"})},
   205  			expect: map[string]Expected{
   206  				"kubernetes://generic": {
   207  					Key:  string(genericCert.Data[credentials.GenericScrtKey]),
   208  					Cert: string(genericCert.Data[credentials.GenericScrtCert]),
   209  				},
   210  			},
   211  		},
   212  		{
   213  			name:      "incremental push with updates - mtls",
   214  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   215  			resources: allResources,
   216  			request: &model.PushRequest{
   217  				Full:           false,
   218  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.Secret, Name: "generic-mtls", Namespace: "istio-system"}),
   219  			},
   220  			expect: map[string]Expected{
   221  				"kubernetes://generic-mtls": {
   222  					Key:  string(genericMtlsCert.Data[credentials.GenericScrtKey]),
   223  					Cert: string(genericMtlsCert.Data[credentials.GenericScrtCert]),
   224  				},
   225  				"kubernetes://generic-mtls-cacert": {
   226  					CaCert: string(genericMtlsCert.Data[credentials.GenericScrtCaCert]),
   227  				},
   228  			},
   229  		},
   230  		{
   231  			name:      "incremental push with updates - mtls with crl",
   232  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   233  			resources: allResources,
   234  			request: &model.PushRequest{
   235  				Full:           false,
   236  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.Secret, Name: "generic-mtls-crl", Namespace: "istio-system"}),
   237  			},
   238  			expect: map[string]Expected{
   239  				"kubernetes://generic-mtls-crl": {
   240  					Key:  string(genericMtlsCertCrl.Data[credentials.GenericScrtKey]),
   241  					Cert: string(genericMtlsCertCrl.Data[credentials.GenericScrtCert]),
   242  				},
   243  				"kubernetes://generic-mtls-crl-cacert": {
   244  					CaCert: string(genericMtlsCertCrl.Data[credentials.GenericScrtCaCert]),
   245  					CaCrl:  string(genericMtlsCertCrl.Data[credentials.GenericScrtCRL]),
   246  				},
   247  			},
   248  		},
   249  		{
   250  			name:      "incremental push with updates - mtls split",
   251  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   252  			resources: allResources,
   253  			request: &model.PushRequest{
   254  				Full:           false,
   255  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.Secret, Name: "generic-mtls-split", Namespace: "istio-system"}),
   256  			},
   257  			expect: map[string]Expected{
   258  				"kubernetes://generic-mtls-split": {
   259  					Key:  string(genericMtlsCertSplit.Data[credentials.GenericScrtKey]),
   260  					Cert: string(genericMtlsCertSplit.Data[credentials.GenericScrtCert]),
   261  				},
   262  				"kubernetes://generic-mtls-split-cacert": {
   263  					CaCert: string(genericMtlsCertSplitCa.Data[credentials.GenericScrtCaCert]),
   264  				},
   265  			},
   266  		},
   267  		{
   268  			name:      "incremental push with updates - mtls split ca update",
   269  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   270  			resources: allResources,
   271  			request: &model.PushRequest{
   272  				Full:           false,
   273  				ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.Secret, Name: "generic-mtls-split-cacert", Namespace: "istio-system"}),
   274  			},
   275  			expect: map[string]Expected{
   276  				"kubernetes://generic-mtls-split": {
   277  					Key:  string(genericMtlsCertSplit.Data[credentials.GenericScrtKey]),
   278  					Cert: string(genericMtlsCertSplit.Data[credentials.GenericScrtCert]),
   279  				},
   280  				"kubernetes://generic-mtls-split-cacert": {
   281  					CaCert: string(genericMtlsCertSplitCa.Data[credentials.GenericScrtCaCert]),
   282  				},
   283  			},
   284  		},
   285  		{
   286  			// If an unknown resource is request, we return all the ones we do know about
   287  			name:      "unknown",
   288  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   289  			resources: []string{"kubernetes://generic", "foo://invalid", "kubernetes://not-found", "default", "builtin://"},
   290  			request:   &model.PushRequest{Full: true},
   291  			expect: map[string]Expected{
   292  				"kubernetes://generic": {
   293  					Key:  string(genericCert.Data[credentials.GenericScrtKey]),
   294  					Cert: string(genericCert.Data[credentials.GenericScrtCert]),
   295  				},
   296  			},
   297  		},
   298  		{
   299  			// proxy without authorization
   300  			name:      "unauthorized",
   301  			proxy:     &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"}, Type: model.Router},
   302  			resources: []string{"kubernetes://generic"},
   303  			request:   &model.PushRequest{Full: true},
   304  			// Should get a response, but it will be empty
   305  			expect: map[string]Expected{},
   306  			accessReviewResponse: func(action k8stesting.Action) (bool, runtime.Object, error) {
   307  				return true, nil, errors.New("not authorized")
   308  			},
   309  		},
   310  	}
   311  	for _, tt := range cases {
   312  		t.Run(tt.name, func(t *testing.T) {
   313  			if tt.proxy.Metadata == nil {
   314  				tt.proxy.Metadata = &model.NodeMetadata{}
   315  			}
   316  			tt.proxy.Metadata.ClusterID = constants.DefaultClusterName
   317  			s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   318  				KubernetesObjects: []runtime.Object{genericCert, genericMtlsCert, genericMtlsCertCrl, genericMtlsCertSplit, genericMtlsCertSplitCa},
   319  			})
   320  			cc := s.KubeClient().Kube().(*fake.Clientset)
   321  
   322  			cc.Fake.Lock()
   323  			if tt.accessReviewResponse != nil {
   324  				cc.Fake.PrependReactor("create", "subjectaccessreviews", tt.accessReviewResponse)
   325  			} else {
   326  				xds.DisableAuthorizationForSecret(cc)
   327  			}
   328  			cc.Fake.Unlock()
   329  
   330  			gen := s.Discovery.Generators[v3.SecretType]
   331  			tt.request.Start = time.Now()
   332  			secrets, _, _ := gen.Generate(s.SetupProxy(tt.proxy), &model.WatchedResource{ResourceNames: tt.resources}, tt.request)
   333  			raw := xdstest.ExtractTLSSecrets(t, xdsserver.ResourcesToAny(secrets))
   334  
   335  			got := map[string]Expected{}
   336  			for _, scrt := range raw {
   337  				got[scrt.Name] = Expected{
   338  					Key:    string(scrt.GetTlsCertificate().GetPrivateKey().GetInlineBytes()),
   339  					Cert:   string(scrt.GetTlsCertificate().GetCertificateChain().GetInlineBytes()),
   340  					CaCert: string(scrt.GetValidationContext().GetTrustedCa().GetInlineBytes()),
   341  					CaCrl:  string(scrt.GetValidationContext().GetCrl().GetInlineBytes()),
   342  				}
   343  			}
   344  			if diff := cmp.Diff(got, tt.expect); diff != "" {
   345  				t.Fatal(diff)
   346  			}
   347  		})
   348  	}
   349  }
   350  
   351  // TestCaching ensures we don't have cross-proxy cache generation issues. This is split from TestGenerate
   352  // since it is order dependent.
   353  // Regression test for https://github.com/istio/istio/issues/33368
   354  func TestCaching(t *testing.T) {
   355  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   356  		KubernetesObjects: []runtime.Object{genericCert},
   357  		KubeClientModifier: func(c kube.Client) {
   358  			cc := c.Kube().(*fake.Clientset)
   359  			xds.DisableAuthorizationForSecret(cc)
   360  		},
   361  	})
   362  	gen := s.Discovery.Generators[v3.SecretType]
   363  
   364  	fullPush := &model.PushRequest{Full: true, Start: time.Now()}
   365  	istiosystem := &model.Proxy{
   366  		Metadata:         &model.NodeMetadata{ClusterID: constants.DefaultClusterName},
   367  		VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"},
   368  		Type:             model.Router,
   369  		ConfigNamespace:  "istio-system",
   370  	}
   371  	otherNamespace := &model.Proxy{
   372  		Metadata:         &model.NodeMetadata{ClusterID: constants.DefaultClusterName},
   373  		VerifiedIdentity: &spiffe.Identity{Namespace: "other-namespace"},
   374  		Type:             model.Router,
   375  		ConfigNamespace:  "other-namespace",
   376  	}
   377  
   378  	secrets, _, _ := gen.Generate(s.SetupProxy(istiosystem), &model.WatchedResource{ResourceNames: []string{"kubernetes://generic"}}, fullPush)
   379  	raw := xdstest.ExtractTLSSecrets(t, xdsserver.ResourcesToAny(secrets))
   380  	if len(raw) != 1 {
   381  		t.Fatalf("failed to get expected secrets for authorized proxy: %v", raw)
   382  	}
   383  
   384  	// We should not get secret returned, even though we are asking for the same one
   385  	secrets, _, _ = gen.Generate(s.SetupProxy(otherNamespace), &model.WatchedResource{ResourceNames: []string{"kubernetes://generic"}}, fullPush)
   386  	raw = xdstest.ExtractTLSSecrets(t, xdsserver.ResourcesToAny(secrets))
   387  	if len(raw) != 0 {
   388  		t.Fatalf("failed to get expected secrets for unauthorized proxy: %v", raw)
   389  	}
   390  }
   391  
   392  func TestPrivateKeyProviderProxyConfig(t *testing.T) {
   393  	pkpProxy := &model.Proxy{
   394  		VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"},
   395  		Type:             model.Router,
   396  		Metadata: &model.NodeMetadata{
   397  			ClusterID: constants.DefaultClusterName,
   398  			ProxyConfig: &model.NodeMetaProxyConfig{
   399  				PrivateKeyProvider: &meshconfig.PrivateKeyProvider{
   400  					Provider: &meshconfig.PrivateKeyProvider_Cryptomb{
   401  						Cryptomb: &meshconfig.PrivateKeyProvider_CryptoMb{
   402  							PollDelay: &durationpb.Duration{
   403  								Seconds: 0,
   404  								Nanos:   10000,
   405  							},
   406  						},
   407  					},
   408  				},
   409  			},
   410  		},
   411  	}
   412  	rawProxy := &model.Proxy{
   413  		VerifiedIdentity: &spiffe.Identity{Namespace: "istio-system"},
   414  		Type:             model.Router,
   415  		Metadata:         &model.NodeMetadata{ClusterID: constants.DefaultClusterName},
   416  	}
   417  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   418  		KubernetesObjects: []runtime.Object{genericCert},
   419  		KubeClientModifier: func(c kube.Client) {
   420  			cc := c.Kube().(*fake.Clientset)
   421  			xds.DisableAuthorizationForSecret(cc)
   422  		},
   423  	})
   424  	gen := s.Discovery.Generators[v3.SecretType]
   425  	fullPush := &model.PushRequest{Full: true, Start: time.Now()}
   426  	secrets, _, _ := gen.Generate(s.SetupProxy(rawProxy), &model.WatchedResource{ResourceNames: []string{"kubernetes://generic"}}, fullPush)
   427  	raw := xdstest.ExtractTLSSecrets(t, xdsserver.ResourcesToAny(secrets))
   428  	for _, scrt := range raw {
   429  		if scrt.GetTlsCertificate().GetPrivateKeyProvider() != nil {
   430  			t.Fatalf("expect no private key provider in secret")
   431  		}
   432  	}
   433  
   434  	// add private key provider in proxy-config
   435  	secrets, _, _ = gen.Generate(s.SetupProxy(pkpProxy), &model.WatchedResource{ResourceNames: []string{"kubernetes://generic"}}, fullPush)
   436  	raw = xdstest.ExtractTLSSecrets(t, xdsserver.ResourcesToAny(secrets))
   437  	for _, scrt := range raw {
   438  		privateKeyProvider := scrt.GetTlsCertificate().GetPrivateKeyProvider()
   439  		if privateKeyProvider == nil {
   440  			t.Fatalf("expect private key provider in secret")
   441  		}
   442  		if privateKeyProvider.GetFallback() {
   443  			t.Fatalf("expect fallback for private key provider in secret as false")
   444  		}
   445  	}
   446  
   447  	// erase private key provider in proxy-config
   448  	secrets, _, _ = gen.Generate(s.SetupProxy(rawProxy), &model.WatchedResource{ResourceNames: []string{"kubernetes://generic"}}, fullPush)
   449  	raw = xdstest.ExtractTLSSecrets(t, xdsserver.ResourcesToAny(secrets))
   450  	for _, scrt := range raw {
   451  		if scrt.GetTlsCertificate().GetPrivateKeyProvider() != nil {
   452  			t.Fatalf("expect no private key provider in secret")
   453  		}
   454  	}
   455  }