istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/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 security
    19  
    20  import (
    21  	"fmt"
    22  	"net/http"
    23  	"path"
    24  	"strings"
    25  	"testing"
    26  
    27  	"istio.io/istio/pkg/http/headers"
    28  	"istio.io/istio/pkg/test"
    29  	echoClient "istio.io/istio/pkg/test/echo"
    30  	"istio.io/istio/pkg/test/env"
    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/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/components/namespace"
    38  	"istio.io/istio/pkg/test/framework/resource"
    39  	"istio.io/istio/pkg/test/util/file"
    40  	ingressutil "istio.io/istio/tests/integration/security/sds_ingress/util"
    41  )
    42  
    43  // TestSimpleTlsOrigination test SIMPLE TLS mode with TLS origination happening at Gateway proxy
    44  // It uses CredentialName set in DestinationRule API to fetch secrets from k8s API server
    45  func TestSimpleTlsOrigination(t *testing.T) {
    46  	// nolint: staticcheck
    47  	framework.NewTest(t).
    48  		RequiresSingleNetwork(). // https://github.com/istio/istio/issues/37134
    49  		Run(func(t framework.TestContext) {
    50  			var (
    51  				credName        = "tls-credential-cacert"
    52  				fakeCredName    = "fake-tls-credential-cacert"
    53  				credNameMissing = "tls-credential-not-created-cacert"
    54  			)
    55  
    56  			credentialA := ingressutil.IngressCredential{
    57  				CaCert: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
    58  			}
    59  			CredentialB := ingressutil.IngressCredential{
    60  				CaCert: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/fake-root-cert.pem")),
    61  			}
    62  			// Add kubernetes secret to provision key/cert for gateway.
    63  			ingressutil.CreateIngressKubeSecret(t, credName, ingressutil.TLS, credentialA, false)
    64  
    65  			// Add kubernetes secret to provision key/cert for gateway.
    66  			ingressutil.CreateIngressKubeSecret(t, fakeCredName, ingressutil.TLS, CredentialB, false)
    67  
    68  			// Set up Host Namespace
    69  			host := apps.External.All.Config().ClusterLocalFQDN()
    70  
    71  			testCases := []struct {
    72  				name            string
    73  				statusCode      int
    74  				credentialToUse string
    75  				useGateway      bool
    76  			}{
    77  				// Use CA certificate stored as k8s secret with the same issuing CA as server's CA.
    78  				// This root certificate can validate the server cert presented by the echoboot server instance.
    79  				{
    80  					name:            "simple",
    81  					statusCode:      http.StatusOK,
    82  					credentialToUse: strings.TrimSuffix(credName, "-cacert"),
    83  					useGateway:      true,
    84  				},
    85  				// Use CA certificate stored as k8s secret with different issuing CA as server's CA.
    86  				// This root certificate cannot validate the server cert presented by the echoboot server instance.
    87  				{
    88  					name:            "fake root",
    89  					statusCode:      http.StatusServiceUnavailable,
    90  					credentialToUse: strings.TrimSuffix(fakeCredName, "-cacert"),
    91  					useGateway:      false,
    92  				},
    93  
    94  				// Set up an UpstreamCluster with a CredentialName when secret doesn't even exist in istio-system ns.
    95  				// Secret fetching error at Gateway, results in a 503 response.
    96  				{
    97  					name:            "missing secret",
    98  					statusCode:      http.StatusServiceUnavailable,
    99  					credentialToUse: credNameMissing,
   100  					useGateway:      false,
   101  				},
   102  			}
   103  
   104  			newTLSGateway(t, t, apps.Ns1.Namespace, apps.External.All, i.Settings().EgressGatewayServiceNamespace,
   105  				i.Settings().EgressGatewayServiceName, i.Settings().EgressGatewayIstioLabel)
   106  
   107  			for _, tc := range testCases {
   108  				t.NewSubTest(tc.name).Run(func(t framework.TestContext) {
   109  					newTLSGatewayDestinationRule(t, apps.External.All, "SIMPLE", tc.credentialToUse)
   110  					newTLSGatewayTest(t).
   111  						Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
   112  							callOpt := newTLSGatewayCallOpts(to, host, tc.statusCode, tc.useGateway)
   113  							from.CallOrFail(t, callOpt)
   114  						})
   115  				})
   116  			}
   117  		})
   118  }
   119  
   120  // TestMutualTlsOrigination test MUTUAL TLS mode with TLS origination happening at Gateway proxy
   121  // It uses CredentialName set in DestinationRule API to fetch secrets from k8s API server
   122  func TestMutualTlsOrigination(t *testing.T) {
   123  	// nolint: staticcheck
   124  	framework.NewTest(t).
   125  		RequiresSingleNetwork(). // https://github.com/istio/istio/issues/37134
   126  		Run(func(t framework.TestContext) {
   127  			var (
   128  				credNameGeneric    = "mtls-credential-generic"
   129  				credNameNotGeneric = "mtls-credential-not-generic"
   130  				fakeCredNameA      = "fake-mtls-credential-a"
   131  				credNameMissing    = "mtls-credential-not-created"
   132  				simpleCredName     = "tls-credential-simple-cacert"
   133  				credWithCRL        = "mtls-credential-crl"
   134  				credWithDummyCRL   = "mtls-credential-dummy-crl"
   135  			)
   136  
   137  			// Add kubernetes secret to provision key/cert for gateway.
   138  
   139  			ingressutil.CreateIngressKubeSecret(t, credNameGeneric, ingressutil.Mtls, ingressutil.IngressCredential{
   140  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/cert-chain.pem")),
   141  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
   142  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
   143  			}, false)
   144  
   145  			ingressutil.CreateIngressKubeSecret(t, credNameNotGeneric, ingressutil.Mtls, ingressutil.IngressCredential{
   146  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/cert-chain.pem")),
   147  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
   148  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
   149  			}, true)
   150  
   151  			// Configured with an invalid ClientCert
   152  			ingressutil.CreateIngressKubeSecret(t, fakeCredNameA, ingressutil.Mtls, ingressutil.IngressCredential{
   153  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/fake-cert-chain.pem")),
   154  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
   155  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
   156  			}, false)
   157  
   158  			ingressutil.CreateIngressKubeSecret(t, simpleCredName, ingressutil.TLS, ingressutil.IngressCredential{
   159  				CaCert: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
   160  			}, false)
   161  
   162  			// Configured with valid CRL
   163  			ingressutil.CreateIngressKubeSecret(t, credWithCRL, ingressutil.Mtls, ingressutil.IngressCredential{
   164  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/cert-chain.pem")),
   165  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
   166  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
   167  				Crl:         file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/ca.crl")),
   168  			}, false)
   169  
   170  			// Configured with dummy CRL
   171  			ingressutil.CreateIngressKubeSecret(t, credWithDummyCRL, ingressutil.Mtls, ingressutil.IngressCredential{
   172  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/cert-chain.pem")),
   173  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
   174  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
   175  				Crl:         file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dummy.crl")),
   176  			}, false)
   177  
   178  			// Set up Host Namespace
   179  			host := apps.External.All.Config().ClusterLocalFQDN()
   180  
   181  			testCases := []struct {
   182  				name            string
   183  				statusCode      int
   184  				credentialToUse string
   185  				useGateway      bool
   186  			}{
   187  				// Use CA certificate and client certs stored as k8s secret with the same issuing CA as server's CA.
   188  				// This root certificate can validate the server cert presented by the echoboot server instance and server CA can
   189  				// validate the client cert. Secret is of type generic.
   190  				{
   191  					name:            "generic",
   192  					statusCode:      http.StatusOK,
   193  					credentialToUse: credNameGeneric,
   194  					useGateway:      true,
   195  				},
   196  				// Use CA certificate and client certs stored as k8s secret with the same issuing CA as server's CA.
   197  				// This root certificate can validate the server cert presented by the echoboot server instance and server CA can
   198  				// validate the client cert. Secret is not of type generic.
   199  				{
   200  					name:            "non-generic",
   201  					statusCode:      http.StatusOK,
   202  					credentialToUse: credNameNotGeneric,
   203  					useGateway:      true,
   204  				},
   205  				// Use CA certificate and client certs stored as k8s secret with the same issuing CA as server's CA.
   206  				// This root certificate can validate the server cert presented by the echoboot server instance and server CA
   207  				// cannot validate the client cert. Returns 503 response as TLS handshake fails.
   208  				{
   209  					name:            "invalid client cert",
   210  					statusCode:      http.StatusServiceUnavailable,
   211  					credentialToUse: fakeCredNameA,
   212  					useGateway:      false,
   213  				},
   214  
   215  				// Set up an UpstreamCluster with a CredentialName when secret doesn't even exist in istio-system ns.
   216  				// Secret fetching error at Gateway, results in a 503 response.
   217  				{
   218  					name:            "missing",
   219  					statusCode:      http.StatusServiceUnavailable,
   220  					credentialToUse: credNameMissing,
   221  					useGateway:      false,
   222  				},
   223  				{
   224  					name:            "no client certs",
   225  					statusCode:      http.StatusServiceUnavailable,
   226  					credentialToUse: strings.TrimSuffix(simpleCredName, "-cacert"),
   227  					useGateway:      false,
   228  				},
   229  				// Set up an UpstreamCluster with a CredentialName where the secret has a CRL specified with the server certificate as revoked.
   230  				// Certificate revoked error at Gateway, results in a 503 response.
   231  				{
   232  					name:            "credential with CRL having server certificate revoked",
   233  					statusCode:      http.StatusServiceUnavailable,
   234  					credentialToUse: credWithCRL,
   235  					useGateway:      false,
   236  				},
   237  				// Set up an UpstreamCluster with a CredentialName where the secret has a CRL specified with an unused server certificate as revoked.
   238  				// Since the certificate in action is not revoked, the communication should not be impacted.
   239  				{
   240  					name:            "credential with CRL having unused revoked server certificate",
   241  					statusCode:      http.StatusOK,
   242  					credentialToUse: credWithDummyCRL,
   243  					useGateway:      true,
   244  				},
   245  			}
   246  
   247  			newTLSGateway(t, t, apps.Ns1.Namespace, apps.External.All, i.Settings().EgressGatewayServiceNamespace,
   248  				i.Settings().EgressGatewayServiceName, i.Settings().EgressGatewayIstioLabel)
   249  			for _, tc := range testCases {
   250  				t.NewSubTest(tc.name).Run(func(t framework.TestContext) {
   251  					newTLSGatewayDestinationRule(t, apps.External.All, "MUTUAL", tc.credentialToUse)
   252  					newTLSGatewayTest(t).
   253  						Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
   254  							callOpt := newTLSGatewayCallOpts(to, host, tc.statusCode, tc.useGateway)
   255  							from.CallOrFail(t, callOpt)
   256  						})
   257  				})
   258  			}
   259  		})
   260  }
   261  
   262  // We want to test out TLS origination at Gateway, to do so traffic from client in client namespace is first
   263  // routed to egress-gateway service in istio-system namespace and then from egress-gateway to server in server namespace.
   264  // TLS origination at Gateway happens using DestinationRule with CredentialName reading k8s secret at the gateway proxy.
   265  func newTLSGateway(t test.Failer, ctx resource.Context, clientNamespace namespace.Instance,
   266  	to echo.Instances, egressNs string, egressSvc string, egressLabel string,
   267  ) {
   268  	args := map[string]any{"to": to, "EgressNamespace": egressNs, "EgressService": egressSvc, "EgressLabel": egressLabel}
   269  
   270  	gateway := `
   271  apiVersion: networking.istio.io/v1beta1
   272  kind: Gateway
   273  metadata:
   274    name: istio-egressgateway-sds
   275  spec:
   276    selector:
   277      istio: {{.EgressLabel}}
   278    servers:
   279      - port:
   280          number: 443
   281          name: https-sds
   282          protocol: HTTPS
   283        hosts:
   284        - {{ .to.Config.ClusterLocalFQDN }}
   285        tls:
   286          mode: ISTIO_MUTUAL
   287  ---
   288  apiVersion: networking.istio.io/v1beta1
   289  kind: DestinationRule
   290  metadata:
   291    name: egressgateway-for-server-sds
   292  spec:
   293    host: {{.EgressService}}.{{.EgressNamespace}}.svc.cluster.local
   294    subsets:
   295    - name: server
   296      trafficPolicy:
   297        portLevelSettings:
   298        - port:
   299            number: 443
   300          tls:
   301            mode: ISTIO_MUTUAL
   302            sni: {{ .to.Config.ClusterLocalFQDN }}
   303  `
   304  	vs := `
   305  apiVersion: networking.istio.io/v1beta1
   306  kind: VirtualService
   307  metadata:
   308    name: route-via-egressgateway-sds
   309  spec:
   310    hosts:
   311      - {{ .to.Config.ClusterLocalFQDN }}
   312    gateways:
   313      - istio-egressgateway-sds
   314      - mesh
   315    http:
   316      - match:
   317          - gateways:
   318              - mesh # from sidecars, route to egress gateway service
   319            port: 80
   320        route:
   321          - destination:
   322              host: {{.EgressService}}.{{.EgressNamespace}}.svc.cluster.local
   323              subset: server
   324              port:
   325                number: 443
   326            weight: 100
   327      - match:
   328          - gateways:
   329              - istio-egressgateway-sds
   330            port: 443
   331        route:
   332          - destination:
   333              host: {{ .to.Config.ClusterLocalFQDN }}
   334              port:
   335                number: 443
   336            weight: 100
   337        headers:
   338          request:
   339            add:
   340              handled-by-egress-gateway: "true"
   341  `
   342  	ctx.ConfigIstio().Eval(clientNamespace.Name(), args, gateway, vs).ApplyOrFail(t)
   343  }
   344  
   345  func newTLSGatewayDestinationRule(t framework.TestContext, to echo.Instances, destinationRuleMode string, credentialName string) {
   346  	args := map[string]any{
   347  		"to":             to,
   348  		"Mode":           destinationRuleMode,
   349  		"CredentialName": credentialName,
   350  	}
   351  
   352  	// Get namespace for gateway pod.
   353  	istioCfg := istio.DefaultConfigOrFail(t, t)
   354  	systemNS := namespace.ClaimOrFail(t, t, istioCfg.SystemNamespace)
   355  
   356  	dr := `
   357  apiVersion: networking.istio.io/v1alpha3
   358  kind: DestinationRule
   359  metadata:
   360    name: originate-tls-for-server-sds-{{.CredentialName}}
   361  spec:
   362    host: "{{ .to.Config.ClusterLocalFQDN }}"
   363    trafficPolicy:
   364      portLevelSettings:
   365        - port:
   366            number: 443
   367          tls:
   368            mode: {{.Mode}}
   369            credentialName: {{.CredentialName}}
   370            sni: {{ .to.Config.ClusterLocalFQDN }}
   371  `
   372  
   373  	t.ConfigKube(t.Clusters().Default()).Eval(systemNS.Name(), args, dr).
   374  		ApplyOrFail(t)
   375  }
   376  
   377  func newTLSGatewayCallOpts(to echo.Target, host string, statusCode int, useGateway bool) echo.CallOptions {
   378  	return echo.CallOptions{
   379  		To: to,
   380  		Port: echo.Port{
   381  			Name: "http",
   382  		},
   383  		HTTP: echo.HTTP{
   384  			Headers: headers.New().WithHost(host).Build(),
   385  		},
   386  		Check: check.And(
   387  			check.NoErrorAndStatus(statusCode),
   388  			check.Each(func(r echoClient.Response) error {
   389  				if _, f := r.RequestHeaders["Handled-By-Egress-Gateway"]; useGateway && !f {
   390  					return fmt.Errorf("expected to be handled by gateway. response: %s", r)
   391  				}
   392  				return nil
   393  			})),
   394  	}
   395  }
   396  
   397  func newTLSGatewayTest(t framework.TestContext) *echotest.T {
   398  	return echotest.New(t, apps.All.Instances()).
   399  		WithDefaultFilters(1, 1).
   400  		FromMatch(match.And(
   401  			match.Namespace(apps.Ns1.Namespace),
   402  			match.NotNaked,
   403  			match.NotProxylessGRPC)).
   404  		ToMatch(match.ServiceName(apps.External.All.NamespacedName()))
   405  }