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 }