istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/reachability_test.go (about) 1 //go:build integ 2 // +build integ 3 4 // Copyright Istio Authors 5 // 6 // Licensed under the Apache License, Version 2.0 (the "License"); 7 // you may not use this file except in compliance with the License. 8 // You may obtain a copy of the License at 9 // 10 // http://www.apache.org/licenses/LICENSE-2.0 11 // 12 // Unless required by applicable law or agreed to in writing, software 13 // distributed under the License is distributed on an "AS IS" BASIS, 14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 // See the License for the specific language governing permissions and 16 // limitations under the License. 17 18 package security 19 20 import ( 21 "testing" 22 23 "istio.io/api/annotation" 24 "istio.io/istio/pilot/pkg/model" 25 "istio.io/istio/pkg/test/echo/common/scheme" 26 "istio.io/istio/pkg/test/framework" 27 "istio.io/istio/pkg/test/framework/components/cluster" 28 "istio.io/istio/pkg/test/framework/components/echo" 29 "istio.io/istio/pkg/test/framework/components/echo/check" 30 "istio.io/istio/pkg/test/framework/components/echo/common/ports" 31 "istio.io/istio/pkg/test/framework/components/echo/config" 32 "istio.io/istio/pkg/test/framework/components/echo/config/param" 33 "istio.io/istio/pkg/test/framework/components/echo/deployment" 34 "istio.io/istio/pkg/test/framework/components/echo/echotest" 35 "istio.io/istio/pkg/test/framework/components/echo/match" 36 "istio.io/istio/pkg/test/framework/components/istio" 37 "istio.io/istio/pkg/test/framework/resource" 38 ) 39 40 const ( 41 migrationServiceName = "migration" 42 migrationVersionIstio = "vistio" 43 migrationVersionNonIstio = "vlegacy" 44 migrationPathIstio = "/" + migrationVersionIstio 45 migrationPathNonIstio = "/" + migrationVersionNonIstio 46 mtlsModeParam = "MTLSMode" 47 mtlsModeOverrideParam = "MTLSModeOverride" 48 tlsModeParam = "TLSMode" 49 cMinIstioVersion = "1.15.0" 50 // cMinIstioVersionDS = "1.16.0" 51 ) 52 53 func TestReachability(t *testing.T) { 54 framework.NewTest(t). 55 Run(func(t framework.TestContext) { 56 systemNS := istio.ClaimSystemNamespaceOrFail(t, t) 57 58 integIstioVersion := cMinIstioVersion 59 var migrationApp echo.Instances 60 // if dual stack is enabled, a dual stack echo config should be added 61 if !t.Settings().EnableDualStack { 62 // Create a custom echo deployment in NS1 with subsets that allows us to test the 63 // migration of a workload to istio (from no sidecar to sidecar). 64 migrationApp = deployment.New(t). 65 WithClusters(t.Clusters()...).WithConfig(echo.Config{ 66 Namespace: echo1NS, 67 Service: migrationServiceName, 68 ServiceAccount: true, 69 Ports: ports.All(), 70 Subsets: []echo.SubsetConfig{ 71 { 72 // Istio deployment, with sidecar. 73 Version: migrationVersionIstio, 74 Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, 75 }, 76 { 77 // Legacy (non-Istio) deployment subset, does not have sidecar injected. 78 Version: migrationVersionNonIstio, 79 Annotations: map[string]string{annotation.SidecarInject.Name: "false"}, 80 }, 81 }, 82 }).BuildOrFail(t) 83 } else { 84 // TODO: remove the MinIstioVersion setting for dual stack integration test for next line 85 // integIstioVersion = cMinIstioVersionDS 86 // Create a custom echo deployment in NS1 with subsets that allows us to test the 87 // migration of a workload to istio (from no sidecar to sidecar). 88 migrationApp = deployment.New(t). 89 WithClusters(t.Clusters()...).WithConfig(echo.Config{ 90 Namespace: echo1NS, 91 Service: migrationServiceName, 92 ServiceAccount: true, 93 Ports: ports.All(), 94 Subsets: []echo.SubsetConfig{ 95 { 96 // Istio deployment, with sidecar. 97 Version: migrationVersionIstio, 98 Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, 99 }, 100 { 101 // Legacy (non-Istio) deployment subset, does not have sidecar injected. 102 Version: migrationVersionNonIstio, 103 Annotations: map[string]string{annotation.SidecarInject.Name: "false"}, 104 }, 105 }, 106 IPFamilies: "IPv4, IPv6", 107 IPFamilyPolicy: "RequireDualStack", 108 }).BuildOrFail(t) 109 } 110 111 // Add the migration app to the full list of services. 112 allServices := apps.Ns1.All.Append(migrationApp.Services()) 113 114 // Create matchers for the migration app. 115 migration := match.ServiceName(migrationApp.NamespacedName()) 116 notMigration := match.Not(migration) 117 118 // Call options to be used for tests using the migration app. 119 migrationOpts := []echo.CallOptions{ 120 { 121 Port: echo.Port{ 122 Name: ports.HTTP.Name, 123 }, 124 HTTP: echo.HTTP{ 125 Path: migrationPathIstio, 126 }, 127 }, 128 { 129 Port: echo.Port{ 130 Name: ports.HTTP.Name, 131 }, 132 HTTP: echo.HTTP{ 133 Path: migrationPathNonIstio, 134 }, 135 }, 136 } 137 138 cases := []struct { 139 name string 140 configs config.Sources 141 fromMatch match.Matcher 142 toMatch match.Matcher 143 callOpts []echo.CallOptions 144 expectMTLS condition 145 expectCrossCluster condition 146 expectCrossNetwork condition 147 expectSuccess condition 148 // minIstioVersion allows conditionally skipping based on required version 149 minIstioVersion string 150 }{ 151 { 152 name: "global mtls strict", 153 configs: config.Sources{ 154 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 155 config.File("testdata/reachability/global-dr.yaml.tmpl"), 156 }.WithParams(param.Params{ 157 mtlsModeParam: model.MTLSStrict.String(), 158 tlsModeParam: "ISTIO_MUTUAL", 159 param.Namespace.String(): systemNS, 160 }), 161 fromMatch: notMigration, 162 toMatch: notMigration, 163 expectMTLS: notNaked, 164 expectCrossCluster: notFromNaked, 165 expectCrossNetwork: notNaked, 166 expectSuccess: notNaked, 167 minIstioVersion: integIstioVersion, 168 }, 169 { 170 name: "global mtls permissive", 171 configs: config.Sources{ 172 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 173 config.File("testdata/reachability/global-dr.yaml.tmpl"), 174 }.WithParams(param.Params{ 175 mtlsModeParam: model.MTLSPermissive.String(), 176 tlsModeParam: "ISTIO_MUTUAL", 177 param.Namespace.String(): systemNS, 178 }), 179 fromMatch: notMigration, 180 toMatch: notMigration, 181 expectMTLS: notNaked, 182 expectCrossCluster: notFromNaked, 183 expectCrossNetwork: notNaked, 184 expectSuccess: notToNaked, 185 minIstioVersion: integIstioVersion, 186 }, 187 { 188 name: "global mtls disabled", 189 configs: config.Sources{ 190 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 191 config.File("testdata/reachability/global-dr.yaml.tmpl"), 192 }.WithParams(param.Params{ 193 mtlsModeParam: model.MTLSDisable.String(), 194 tlsModeParam: "DISABLE", 195 param.Namespace.String(): systemNS, 196 }), 197 fromMatch: notMigration, 198 toMatch: notMigration, 199 expectMTLS: never, 200 expectCrossCluster: notFromNaked, 201 expectCrossNetwork: never, 202 expectSuccess: always, 203 minIstioVersion: integIstioVersion, 204 }, 205 { 206 name: "global plaintext to mtls permissive", 207 configs: config.Sources{ 208 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 209 config.File("testdata/reachability/global-dr.yaml.tmpl"), 210 }.WithParams(param.Params{ 211 mtlsModeParam: model.MTLSPermissive.String(), 212 tlsModeParam: "DISABLE", 213 param.Namespace.String(): systemNS, 214 }), 215 fromMatch: notMigration, 216 toMatch: notMigration, 217 expectMTLS: never, 218 expectCrossCluster: notFromNaked, 219 expectCrossNetwork: never, 220 expectSuccess: always, 221 minIstioVersion: integIstioVersion, 222 }, 223 { 224 name: "global automtls strict", 225 configs: config.Sources{ 226 // No DR is added for this test. enableAutoMtls is expected on by default. 227 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 228 }.WithParams(param.Params{ 229 mtlsModeParam: model.MTLSStrict.String(), 230 param.Namespace.String(): systemNS, 231 }), 232 fromMatch: notMigration, 233 toMatch: notMigration, 234 expectMTLS: notNaked, 235 expectCrossCluster: notFromNaked, 236 expectCrossNetwork: notNaked, 237 expectSuccess: notFromNaked, 238 }, 239 { 240 name: "global automtls disable", 241 configs: config.Sources{ 242 // No DR is added for this test. enableAutoMtls is expected on by default. 243 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 244 }.WithParams(param.Params{ 245 mtlsModeParam: model.MTLSDisable.String(), 246 param.Namespace.String(): systemNS, 247 }), 248 fromMatch: notMigration, 249 toMatch: notMigration, 250 expectMTLS: never, 251 expectCrossCluster: notFromNaked, 252 expectCrossNetwork: never, 253 expectSuccess: always, 254 }, 255 { 256 name: "global automtls passthrough", 257 configs: config.Sources{ 258 config.File("testdata/reachability/automtls-passthrough.yaml.tmpl"), 259 }.WithNamespace(systemNS), 260 fromMatch: notMigration, 261 // VM passthrough doesn't work. We will send traffic to the ClusterIP of 262 // the VM service, which will have 0 Endpoints. If we generated 263 // EndpointSlice's for VMs this might work. 264 toMatch: match.And(match.NotVM, notMigration), 265 expectMTLS: notNaked, 266 // Since we are doing pass-through, all requests will stay in the same cluster, 267 // as we are bypassing Istio load balancing. 268 // TODO(https://github.com/istio/istio/issues/39700): Why does headless behave differently? 269 expectCrossCluster: and(notFromNaked, or(toHeadless, toStatefulSet)), 270 expectCrossNetwork: never, 271 expectSuccess: always, 272 minIstioVersion: integIstioVersion, 273 }, 274 { 275 name: "global no peer authn", 276 configs: config.Sources{ 277 config.File("testdata/reachability/global-dr.yaml.tmpl"), 278 }.WithParams(param.Params{ 279 tlsModeParam: "ISTIO_MUTUAL", 280 param.Namespace.String(): systemNS, 281 }), 282 fromMatch: notMigration, 283 toMatch: notMigration, 284 expectMTLS: notNaked, 285 expectCrossCluster: notFromNaked, 286 expectCrossNetwork: notNaked, 287 expectSuccess: notToNaked, 288 minIstioVersion: integIstioVersion, 289 }, 290 { 291 name: "mtls strict", 292 configs: config.Sources{ 293 config.File("testdata/reachability/workload-peer-authn.yaml.tmpl"), 294 config.File("testdata/reachability/workload-dr.yaml.tmpl"), 295 }.WithParams(param.Params{ 296 mtlsModeParam: model.MTLSStrict.String(), 297 tlsModeParam: "ISTIO_MUTUAL", 298 }), 299 fromMatch: notMigration, 300 toMatch: notMigration, 301 expectMTLS: notNaked, 302 expectCrossCluster: notFromNaked, 303 expectCrossNetwork: notNaked, 304 expectSuccess: notNaked, 305 }, 306 { 307 name: "mtls permissive", 308 configs: config.Sources{ 309 config.File("testdata/reachability/workload-peer-authn.yaml.tmpl"), 310 config.File("testdata/reachability/workload-dr.yaml.tmpl"), 311 }.WithParams(param.Params{ 312 mtlsModeParam: model.MTLSPermissive.String(), 313 tlsModeParam: "ISTIO_MUTUAL", 314 }), 315 fromMatch: notMigration, 316 toMatch: notMigration, 317 expectMTLS: notNaked, 318 expectCrossCluster: notFromNaked, 319 expectCrossNetwork: notNaked, 320 expectSuccess: notToNaked, 321 }, 322 { 323 name: "mtls disabled", 324 configs: config.Sources{ 325 config.File("testdata/reachability/workload-peer-authn.yaml.tmpl"), 326 config.File("testdata/reachability/workload-dr.yaml.tmpl"), 327 }.WithParams(param.Params{ 328 mtlsModeParam: model.MTLSDisable.String(), 329 tlsModeParam: "DISABLE", 330 }), 331 fromMatch: notMigration, 332 toMatch: notMigration, 333 expectMTLS: never, 334 expectCrossCluster: notFromNaked, 335 expectCrossNetwork: never, 336 expectSuccess: always, 337 }, 338 { 339 name: "mtls port override", 340 configs: config.Sources{ 341 config.File("testdata/reachability/workload-peer-authn-port-override.yaml.tmpl"), 342 }.WithParams(param.Params{ 343 mtlsModeParam: model.MTLSStrict.String(), 344 mtlsModeOverrideParam: model.MTLSDisable.String(), 345 }), 346 fromMatch: notMigration, 347 // TODO(https://github.com/istio/istio/issues/39439): 348 toMatch: match.And(match.NotHeadless, notMigration), 349 expectMTLS: never, 350 expectCrossCluster: notFromNaked, 351 expectCrossNetwork: never, 352 expectSuccess: always, 353 }, 354 355 // --------start of auto mtls partial test cases --------------- 356 // The follow three consecutive test together ensures the auto mtls works as intended 357 // for sidecar migration scenario. 358 { 359 name: "migration no tls", 360 configs: config.Sources{ 361 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 362 config.File("testdata/reachability/migration.yaml.tmpl"), 363 }.WithParams(param.Params{ 364 mtlsModeParam: model.MTLSStrict.String(), 365 tlsModeParam: "", // No TLS settings will be included. 366 param.Namespace.String(): apps.Ns1.Namespace, 367 }), 368 fromMatch: match.And(match.NotNaked, notMigration), 369 toMatch: migration, 370 callOpts: migrationOpts, 371 expectMTLS: toMigrationIstioSubset, 372 expectCrossCluster: notFromNaked, 373 expectCrossNetwork: toMigrationIstioSubset, 374 expectSuccess: always, 375 }, 376 { 377 name: "migration tls disabled", 378 configs: config.Sources{ 379 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 380 config.File("testdata/reachability/migration.yaml.tmpl"), 381 }.WithParams(param.Params{ 382 mtlsModeParam: model.MTLSStrict.String(), 383 tlsModeParam: "DISABLE", 384 param.Namespace.String(): apps.Ns1.Namespace, 385 }), 386 fromMatch: match.And(match.NotNaked, notMigration), 387 toMatch: migration, 388 callOpts: migrationOpts, 389 expectMTLS: never, 390 expectCrossCluster: notFromNaked, 391 expectCrossNetwork: never, 392 // Only the request to legacy one succeeds as we disable mtls explicitly. 393 expectSuccess: toMigrationNonIstioSubset, 394 }, 395 { 396 name: "migration tls mutual", 397 configs: config.Sources{ 398 config.File("testdata/reachability/global-peer-authn.yaml.tmpl"), 399 config.File("testdata/reachability/migration.yaml.tmpl"), 400 }.WithParams(param.Params{ 401 mtlsModeParam: model.MTLSStrict.String(), 402 tlsModeParam: "ISTIO_MUTUAL", 403 param.Namespace.String(): apps.Ns1.Namespace, 404 }), 405 fromMatch: match.And(match.NotNaked, notMigration), 406 toMatch: migration, 407 callOpts: migrationOpts, 408 expectMTLS: toMigrationIstioSubset, 409 expectCrossCluster: notFromNaked, 410 expectCrossNetwork: toMigrationIstioSubset, 411 // Only the request to vistio one succeeds as we enable mtls explicitly. 412 expectSuccess: toMigrationIstioSubset, 413 }, 414 } 415 416 for _, c := range cases { 417 c := c 418 419 t.NewSubTest(c.name).Run(func(t framework.TestContext) { 420 if c.minIstioVersion != "" { 421 skipMV := !t.Settings().Revisions.AtLeast(resource.IstioVersion(c.minIstioVersion)) 422 if skipMV { 423 t.SkipNow() 424 } 425 } 426 // Apply the configs. 427 config.New(t). 428 Source(c.configs...). 429 BuildAll(nil, allServices). 430 Apply() 431 // Run the test against a number of ports. 432 allOpts := append([]echo.CallOptions{}, c.callOpts...) 433 if len(allOpts) == 0 { 434 allOpts = []echo.CallOptions{ 435 { 436 Port: echo.Port{ 437 Name: ports.HTTP.Name, 438 }, 439 }, 440 { 441 Port: echo.Port{ 442 Name: ports.HTTP.Name, 443 }, 444 Scheme: scheme.WebSocket, 445 }, 446 { 447 Port: echo.Port{ 448 Name: ports.HTTP2.Name, 449 }, 450 }, 451 { 452 Port: echo.Port{ 453 Name: ports.HTTPS.Name, 454 }, 455 }, 456 { 457 Port: echo.Port{ 458 Name: ports.TCP.Name, 459 }, 460 }, 461 { 462 Port: echo.Port{ 463 Name: ports.GRPC.Name, 464 }, 465 }, 466 } 467 } 468 469 // Iterate over all protocols outside, rather than inside, the destination match 470 // This is to workaround a known bug (https://github.com/istio/istio/issues/38982) causing 471 // connection resets when sending traffic to multiple ports at once 472 for _, opts := range allOpts { 473 opts := opts 474 475 schemeStr := string(opts.Scheme) 476 if len(schemeStr) == 0 { 477 schemeStr = opts.Port.Name 478 } 479 t.NewSubTestf("%s%s", schemeStr, opts.HTTP.Path).Run(func(t framework.TestContext) { 480 // Run the test cases. 481 echotest.New(t, allServices.Instances()). 482 FromMatch(match.And(c.fromMatch, match.NotProxylessGRPC)). 483 ToMatch(match.And(c.toMatch, match.NotProxylessGRPC)). 484 WithDefaultFilters(1, 1). 485 ConditionallyTo(echotest.NoSelfCalls). 486 Run(func(t framework.TestContext, from echo.Instance, to echo.Target) { 487 opts := opts.DeepCopy() 488 opts.To = to 489 490 if c.expectSuccess(from, opts) { 491 opts.Check = check.OK() 492 493 // Check HTTP headers to confirm expected use of mTLS in the request. 494 if c.expectMTLS(from, opts) { 495 opts.Check = check.And(opts.Check, check.MTLSForHTTP()) 496 } else { 497 opts.Check = check.And(opts.Check, check.PlaintextForHTTP()) 498 } 499 500 // Check that the correct clusters/networks were reached. 501 if c.expectCrossNetwork(from, opts) { 502 if !check.IsDNSCaptureEnabled(t) && opts.To.Config().Headless { 503 opts.Check = check.And(opts.Check, check.ReachedSourceCluster(t.Clusters())) 504 } else { 505 opts.Check = check.And(opts.Check, check.ReachedTargetClusters(t)) 506 } 507 } else if c.expectCrossCluster(from, opts) { 508 // Expect to stay in the same network as the source pod. 509 expectedClusters := to.Clusters().ForNetworks(from.Config().Cluster.NetworkName()) 510 if !check.IsDNSCaptureEnabled(t) && opts.To.Config().Headless { 511 opts.Check = check.And(opts.Check, check.ReachedSourceCluster(t.Clusters())) 512 } else { 513 opts.Check = check.And(opts.Check, check.ReachedClusters(t.Clusters(), expectedClusters)) 514 } 515 } else { 516 // Expect to stay in the same cluster as the source pod. 517 expectedClusters := cluster.Clusters{from.Config().Cluster} 518 if !check.IsDNSCaptureEnabled(t) && opts.To.Config().Headless { 519 opts.Check = check.And(opts.Check, check.ReachedSourceCluster(t.Clusters())) 520 } else { 521 opts.Check = check.And(opts.Check, check.ReachedClusters(t.Clusters(), expectedClusters)) 522 } 523 } 524 } else { 525 opts.Check = check.NotOK() 526 } 527 from.CallOrFail(t, opts) 528 }) 529 }) 530 } 531 }) 532 } 533 }) 534 } 535 536 type condition func(from echo.Instance, opts echo.CallOptions) bool 537 538 func not(c condition) condition { 539 return func(from echo.Instance, opts echo.CallOptions) bool { 540 return !c(from, opts) 541 } 542 } 543 544 func and(conds ...condition) condition { 545 return func(from echo.Instance, opts echo.CallOptions) bool { 546 for _, c := range conds { 547 if !c(from, opts) { 548 return false 549 } 550 } 551 return true 552 } 553 } 554 555 func or(conds ...condition) condition { 556 return func(from echo.Instance, opts echo.CallOptions) bool { 557 for _, c := range conds { 558 if c(from, opts) { 559 return true 560 } 561 } 562 return false 563 } 564 } 565 566 var fromNaked condition = func(from echo.Instance, _ echo.CallOptions) bool { 567 return from.Config().IsNaked() 568 } 569 570 var toNaked condition = func(_ echo.Instance, opts echo.CallOptions) bool { 571 return opts.To.Config().IsNaked() 572 } 573 574 var toHeadless condition = func(_ echo.Instance, opts echo.CallOptions) bool { 575 return opts.To.Config().IsHeadless() 576 } 577 578 var toStatefulSet condition = func(_ echo.Instance, opts echo.CallOptions) bool { 579 return opts.To.Config().IsStatefulSet() 580 } 581 582 var toMigrationIstioSubset condition = func(_ echo.Instance, opts echo.CallOptions) bool { 583 return opts.HTTP.Path == migrationPathIstio 584 } 585 586 var toMigrationNonIstioSubset condition = func(_ echo.Instance, opts echo.CallOptions) bool { 587 return opts.HTTP.Path == migrationPathNonIstio 588 } 589 590 var anyNaked = or(fromNaked, toNaked) 591 592 var notNaked = not(anyNaked) 593 594 var notFromNaked = not(fromNaked) 595 596 var notToNaked = not(toNaked) 597 598 var always condition = func(echo.Instance, echo.CallOptions) bool { 599 return true 600 } 601 602 var never condition = func(echo.Instance, echo.CallOptions) bool { 603 return false 604 }