istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/filebased_tls_origination/egress_gateway_origination_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 filebasedtlsorigination
    19  
    20  import (
    21  	"fmt"
    22  	"net/http"
    23  	"testing"
    24  	"time"
    25  
    26  	admin "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
    27  
    28  	"istio.io/istio/pkg/http/headers"
    29  	"istio.io/istio/pkg/test"
    30  	echoClient "istio.io/istio/pkg/test/echo"
    31  	"istio.io/istio/pkg/test/framework"
    32  	"istio.io/istio/pkg/test/framework/components/echo"
    33  	"istio.io/istio/pkg/test/framework/components/echo/check"
    34  	"istio.io/istio/pkg/test/framework/components/istio"
    35  	"istio.io/istio/pkg/test/framework/components/namespace"
    36  	"istio.io/istio/pkg/test/framework/resource"
    37  	"istio.io/istio/pkg/test/util/retry"
    38  	"istio.io/istio/pkg/test/util/structpath"
    39  )
    40  
    41  // TestEgressGatewayTls brings up an cluster and will ensure that the TLS origination at
    42  // egress gateway allows secure communication between the egress gateway and external workload.
    43  // This test brings up an egress gateway to originate TLS connection. The test will ensure that requests
    44  // are routed securely through the egress gateway and that the TLS origination happens at the gateway.
    45  func TestEgressGatewayTls(t *testing.T) {
    46  	framework.NewTest(t).
    47  		Run(func(t framework.TestContext) {
    48  			// Apply Egress Gateway for service namespace to originate external traffic
    49  
    50  			createGateway(t, t, appNS, serviceNS, inst.Settings().EgressGatewayServiceNamespace,
    51  				inst.Settings().EgressGatewayServiceName, inst.Settings().EgressGatewayIstioLabel)
    52  
    53  			if err := WaitUntilNotCallable(internalClient[0], externalService[0]); err != nil {
    54  				t.Fatalf("failed to apply sidecar, %v", err)
    55  			}
    56  
    57  			// Set up Host Name
    58  			host := "external-service." + serviceNS.Name() + ".svc.cluster.local"
    59  
    60  			testCases := map[string]struct {
    61  				destinationRuleMode string
    62  				code                int
    63  				gateway             bool //  If gateway is true, request is expected to pass through the egress gateway
    64  				fakeRootCert        bool // If Fake root cert is to be used to verify server's presented certificate
    65  			}{
    66  				// Mutual Connection is originated by our DR but server side drops the connection to
    67  				// only use Simple TLS as it doesn't verify client side cert
    68  				// TODO: mechanism to enforce mutual TLS(client cert) validation by the server
    69  				// 1. Mutual TLS origination from egress gateway to https endpoint:
    70  				//    internalClient ) ---HTTP request (Host: some-external-site.com----> Hits listener 0.0.0.0_80 ->
    71  				//      VS Routing (add Egress Header) --> Egress Gateway(originates mTLS with client certs)
    72  				//      --> externalServer(443 with only Simple TLS used and client cert is not verified)
    73  				"Mutual TLS origination from egress gateway to https endpoint": {
    74  					destinationRuleMode: "MUTUAL",
    75  					code:                http.StatusOK,
    76  					gateway:             true,
    77  					fakeRootCert:        false,
    78  				},
    79  				// 2. Simple TLS case:
    80  				//    internalClient ) ---HTTP request (Host: some-external-site.com----> Hits listener 0.0.0.0_80 ->
    81  				//      VS Routing (add Egress Header) --> Egress Gateway(originates TLS)
    82  				//      --> externalServer(443 with TLS enforced)
    83  
    84  				"SIMPLE TLS origination from egress gateway to https endpoint": {
    85  					destinationRuleMode: "SIMPLE",
    86  					code:                http.StatusOK,
    87  					gateway:             true,
    88  					fakeRootCert:        false,
    89  				},
    90  				// 3. No TLS case:
    91  				//    internalClient ) ---HTTP request (Host: some-external-site.com----> Hits listener 0.0.0.0_80 ->
    92  				//      VS Routing (add Egress Header) --> Egress Gateway(does not originate TLS)
    93  				//      --> externalServer(443 with TLS enforced) request fails as gateway tries plain text only
    94  				"No TLS origination from egress gateway to https endpoint": {
    95  					destinationRuleMode: "DISABLE",
    96  					code:                http.StatusBadRequest,
    97  					gateway:             false, // 400 response will not contain header
    98  				},
    99  				// 5. SIMPLE TLS origination with "fake" root cert::
   100  				//   internalClient ) ---HTTP request (Host: some-external-site.com----> Hits listener 0.0.0.0_80 ->
   101  				//     VS Routing (add Egress Header) --> Egress Gateway(originates simple TLS)
   102  				//     --> externalServer(443 with TLS enforced)
   103  				//    request fails as the server cert can't be validated using the fake root cert used during origination
   104  				"SIMPLE TLS origination from egress gateway to https endpoint with fake root cert": {
   105  					destinationRuleMode: "SIMPLE",
   106  					code:                http.StatusServiceUnavailable,
   107  					gateway:             false, // 503 response will not contain header
   108  					fakeRootCert:        true,
   109  				},
   110  			}
   111  
   112  			for name, tc := range testCases {
   113  				t.NewSubTest(name).
   114  					Run(func(t framework.TestContext) {
   115  						createDestinationRule(t, serviceNS, tc.destinationRuleMode, tc.fakeRootCert)
   116  
   117  						opts := echo.CallOptions{
   118  							To:    externalService[0],
   119  							Count: 1,
   120  							Port: echo.Port{
   121  								Name: "http",
   122  							},
   123  							HTTP: echo.HTTP{
   124  								Headers: headers.New().WithHost(host).Build(),
   125  							},
   126  							Retry: echo.Retry{
   127  								Options: []retry.Option{retry.Delay(1 * time.Second), retry.Timeout(2 * time.Minute)},
   128  							},
   129  							Check: check.And(
   130  								check.NoError(),
   131  								check.Status(tc.code),
   132  								check.Each(func(r echoClient.Response) error {
   133  									if _, f := r.RequestHeaders["Handled-By-Egress-Gateway"]; tc.gateway && !f {
   134  										return fmt.Errorf("expected to be handled by gateway. response: %s", r)
   135  									}
   136  									return nil
   137  								})),
   138  						}
   139  
   140  						internalClient[0].CallOrFail(t, opts)
   141  					})
   142  			}
   143  		})
   144  }
   145  
   146  const (
   147  	// Destination Rule configs
   148  	DestinationRuleConfigSimple = `
   149  apiVersion: networking.istio.io/v1alpha3
   150  kind: DestinationRule
   151  metadata:
   152    name: originate-tls-for-server-filebased-simple
   153  spec:
   154    host: "external-service.{{.AppNamespace}}.svc.cluster.local"
   155    trafficPolicy:
   156      portLevelSettings:
   157        - port:
   158            number: 443
   159          tls:
   160            mode: {{.Mode}}
   161            caCertificates: {{.RootCertPath}}
   162            sni: external-service.{{.AppNamespace}}.svc.cluster.local
   163  
   164  `
   165  	// Destination Rule configs
   166  	DestinationRuleConfigDisabledOrIstioMutual = `
   167  apiVersion: networking.istio.io/v1alpha3
   168  kind: DestinationRule
   169  metadata:
   170    name: originate-tls-for-server-filebased-disabled
   171  spec:
   172    host: "external-service.{{.AppNamespace}}.svc.cluster.local"
   173    trafficPolicy:
   174      portLevelSettings:
   175        - port:
   176            number: 443
   177          tls:
   178            mode: {{.Mode}}
   179            sni: external-service.{{.AppNamespace}}.svc.cluster.local
   180  
   181  `
   182  	DestinationRuleConfigMutual = `
   183  apiVersion: networking.istio.io/v1alpha3
   184  kind: DestinationRule
   185  metadata:
   186    name: originate-tls-for-server-filebased-mutual
   187  spec:
   188    host: "external-service.{{.AppNamespace}}.svc.cluster.local"
   189    trafficPolicy:
   190      portLevelSettings:
   191        - port:
   192            number: 443
   193          tls:
   194            mode: {{.Mode}}
   195            clientCertificate: /etc/certs/custom/cert-chain.pem
   196            privateKey: /etc/certs/custom/key.pem
   197            caCertificates: {{.RootCertPath}}
   198            sni: external-service.{{.AppNamespace}}.svc.cluster.local
   199  `
   200  )
   201  
   202  func createDestinationRule(t framework.TestContext, serviceNamespace namespace.Instance,
   203  	destinationRuleMode string, fakeRootCert bool,
   204  ) {
   205  	var destinationRuleToParse string
   206  	var rootCertPathToUse string
   207  	if destinationRuleMode == "MUTUAL" {
   208  		destinationRuleToParse = DestinationRuleConfigMutual
   209  	} else if destinationRuleMode == "SIMPLE" {
   210  		destinationRuleToParse = DestinationRuleConfigSimple
   211  	} else {
   212  		destinationRuleToParse = DestinationRuleConfigDisabledOrIstioMutual
   213  	}
   214  	if fakeRootCert {
   215  		rootCertPathToUse = "/etc/certs/custom/fake-root-cert.pem"
   216  	} else {
   217  		rootCertPathToUse = "/etc/certs/custom/root-cert.pem"
   218  	}
   219  	istioCfg := istio.DefaultConfigOrFail(t, t)
   220  	systemNamespace := namespace.ClaimOrFail(t, t, istioCfg.SystemNamespace)
   221  	args := map[string]string{
   222  		"AppNamespace": serviceNamespace.Name(),
   223  		"Mode":         destinationRuleMode, "RootCertPath": rootCertPathToUse,
   224  	}
   225  	t.ConfigIstio().Eval(systemNamespace.Name(), args, destinationRuleToParse).ApplyOrFail(t)
   226  }
   227  
   228  const (
   229  	Gateway = `
   230  apiVersion: networking.istio.io/v1alpha3
   231  kind: Gateway
   232  metadata:
   233    name: istio-egressgateway-filebased
   234  spec:
   235    selector:
   236      istio: {{.EgressLabel}}
   237    servers:
   238      - port:
   239          number: 443
   240          name: https-filebased
   241          protocol: HTTPS
   242        hosts:
   243          - external-service.{{.ServerNamespace}}.svc.cluster.local
   244        tls:
   245          mode: ISTIO_MUTUAL
   246  ---
   247  apiVersion: networking.istio.io/v1alpha3
   248  kind: DestinationRule
   249  metadata:
   250    name: egressgateway-for-server-filebased
   251  spec:
   252    host: {{.EgressService}}.{{.EgressNamespace}}.svc.cluster.local
   253    subsets:
   254    - name: server
   255      trafficPolicy:
   256        portLevelSettings:
   257        - port:
   258            number: 443
   259          tls:
   260            mode: ISTIO_MUTUAL
   261            sni: external-service.{{.ServerNamespace}}.svc.cluster.local
   262  `
   263  	VirtualService = `
   264  apiVersion: networking.istio.io/v1alpha3
   265  kind: VirtualService
   266  metadata:
   267    name: route-via-egressgateway-filebased
   268  spec:
   269    hosts:
   270      - external-service.{{.ServerNamespace}}.svc.cluster.local
   271    gateways:
   272      - istio-egressgateway-filebased
   273      - mesh
   274    http:
   275      - match:
   276          - gateways:
   277              - mesh # from sidecars, route to egress gateway service
   278            port: 80
   279        route:
   280          - destination:
   281              host: {{.EgressService}}.{{.EgressNamespace}}.svc.cluster.local
   282              subset: server
   283              port:
   284                number: 443
   285            weight: 100
   286      - match:
   287          - gateways:
   288              - istio-egressgateway-filebased
   289            port: 443
   290        route:
   291          - destination:
   292              host: external-service.{{.ServerNamespace}}.svc.cluster.local
   293              port:
   294                number: 443
   295            weight: 100
   296        headers:
   297          request:
   298            add:
   299              handled-by-egress-gateway: "true"
   300  `
   301  )
   302  
   303  func createGateway(t test.Failer, ctx resource.Context, appsNamespace namespace.Instance,
   304  	serviceNamespace namespace.Instance, egressNs string, egressSvc string, egressLabel string,
   305  ) {
   306  	ctx.ConfigIstio().
   307  		Eval(appsNamespace.Name(), map[string]string{
   308  			"ServerNamespace": serviceNamespace.Name(),
   309  			"EgressNamespace": egressNs, "EgressLabel": egressLabel, "EgressService": egressSvc,
   310  		}, Gateway).
   311  		Eval(appsNamespace.Name(), map[string]string{
   312  			"ServerNamespace": serviceNamespace.Name(),
   313  			"EgressNamespace": egressNs, "EgressService": egressSvc,
   314  		}, VirtualService).
   315  		ApplyOrFail(t)
   316  }
   317  
   318  func clusterName(target echo.Instance, port echo.Port) string {
   319  	cfg := target.Config()
   320  	return fmt.Sprintf("outbound|%d||%s.%s.svc.%s", port.ServicePort, cfg.Service, cfg.Namespace.Name(), cfg.Domain)
   321  }
   322  
   323  // Wait for the server to NOT be callable by the client. This allows us to simulate external traffic.
   324  // This essentially just waits for the Sidecar to be applied, without sleeping.
   325  func WaitUntilNotCallable(c echo.Instance, dest echo.Instance) error {
   326  	accept := func(cfg *admin.ConfigDump) (bool, error) {
   327  		validator := structpath.ForProto(cfg)
   328  		for _, port := range dest.Config().Ports {
   329  			clusterName := clusterName(dest, port)
   330  			// Ensure that we have an outbound configuration for the target port.
   331  			err := validator.NotExists("{.configs[*].dynamicActiveClusters[?(@.cluster.Name == '%s')]}", clusterName).Check()
   332  			if err != nil {
   333  				return false, err
   334  			}
   335  		}
   336  
   337  		return true, nil
   338  	}
   339  
   340  	workloads, _ := c.Workloads()
   341  	// Wait for the outbound config to be received by each workload from Pilot.
   342  	for _, w := range workloads {
   343  		if w.Sidecar() != nil {
   344  			if err := w.Sidecar().WaitForConfig(accept, retry.Timeout(time.Second*10)); err != nil {
   345  				return err
   346  			}
   347  		}
   348  	}
   349  
   350  	return nil
   351  }