istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/egress_sidecar_tls_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/config/protocol"
    28  	"istio.io/istio/pkg/http/headers"
    29  	"istio.io/istio/pkg/test/env"
    30  	"istio.io/istio/pkg/test/framework"
    31  	"istio.io/istio/pkg/test/framework/components/echo"
    32  	"istio.io/istio/pkg/test/framework/components/echo/check"
    33  	"istio.io/istio/pkg/test/framework/components/namespace"
    34  	"istio.io/istio/pkg/test/framework/resource/config/apply"
    35  	"istio.io/istio/pkg/test/util/file"
    36  	ingressutil "istio.io/istio/tests/integration/security/sds_ingress/util"
    37  )
    38  
    39  // TestSidecarMutualTlsOrigination test MUTUAL TLS mode with TLS origination happening at the sidecar.
    40  // It uses CredentialName set in DestinationRule API to fetch secrets from k8s API server.
    41  func TestSidecarMutualTlsOrigination(t *testing.T) {
    42  	// nolint: staticcheck
    43  	framework.NewTest(t).
    44  		Run(func(t framework.TestContext) {
    45  			var (
    46  				credNameGeneric  = "mtls-credential-generic"
    47  				fakeCredName     = "fake-mtls-credential"
    48  				credWithCRL      = "mtls-credential-generic-valid-crl"
    49  				credWithDummyCRL = "mtls-credential-generic-dummy-crl"
    50  			)
    51  
    52  			// Create a valid kubernetes secret to provision key/cert for sidecar.
    53  			ingressutil.CreateIngressKubeSecretInNamespace(t, credNameGeneric, ingressutil.Mtls, ingressutil.IngressCredential{
    54  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/cert-chain.pem")),
    55  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
    56  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
    57  			}, true, apps.Ns1.Namespace.Name())
    58  
    59  			// Create a kubernetes secret with an invalid ClientCert
    60  			ingressutil.CreateIngressKubeSecretInNamespace(t, fakeCredName, ingressutil.Mtls, ingressutil.IngressCredential{
    61  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/fake-cert-chain.pem")),
    62  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
    63  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
    64  			}, false, apps.Ns1.Namespace.Name())
    65  
    66  			// Create a valid kubernetes secret to provision key/cert for sidecar, configured with valid CRL
    67  			ingressutil.CreateIngressKubeSecretInNamespace(t, credWithCRL, ingressutil.Mtls, ingressutil.IngressCredential{
    68  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/cert-chain.pem")),
    69  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
    70  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
    71  				Crl:         file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/ca.crl")),
    72  			}, false, apps.Ns2.Namespace.Name())
    73  
    74  			// Create a valid kubernetes secret to provision key/cert for sidecar, configured with dummy CRL
    75  			ingressutil.CreateIngressKubeSecretInNamespace(t, credWithDummyCRL, ingressutil.Mtls, ingressutil.IngressCredential{
    76  				Certificate: file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/cert-chain.pem")),
    77  				PrivateKey:  file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/key.pem")),
    78  				CaCert:      file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dns/root-cert.pem")),
    79  				Crl:         file.AsStringOrFail(t, path.Join(env.IstioSrc, "tests/testdata/certs/dummy.crl")),
    80  			}, false, apps.Ns2.Namespace.Name())
    81  
    82  			// Set up Host Namespace
    83  			host := apps.External.All.Config().ClusterLocalFQDN()
    84  
    85  			testCases := []struct {
    86  				name             string
    87  				credentialToUse  string
    88  				from             echo.Instances
    89  				authorizeSidecar bool
    90  				drSelector       string
    91  				expectedResponse ingressutil.ExpectedResponse
    92  			}{
    93  				// Mutual TLS origination from an authorized sidecar to https endpoint
    94  				{
    95  					name:             "authorized sidecar",
    96  					credentialToUse:  credNameGeneric,
    97  					from:             apps.Ns1.A,
    98  					drSelector:       "a",
    99  					authorizeSidecar: true,
   100  					expectedResponse: ingressutil.ExpectedResponse{
   101  						StatusCode: http.StatusOK,
   102  					},
   103  				},
   104  				// Mutual TLS origination from an unauthorized sidecar to https endpoint
   105  				// This will result in a 503 with the UH flag because the cluster will
   106  				// stay warming until a valid secret is sent.
   107  				{
   108  					name:            "unauthorized sidecar",
   109  					credentialToUse: credNameGeneric,
   110  					from:            apps.Ns1.B,
   111  					drSelector:      "b",
   112  					expectedResponse: ingressutil.ExpectedResponse{
   113  						StatusCode: http.StatusServiceUnavailable,
   114  					},
   115  				},
   116  				// Mutual TLS origination using an invalid client certificate
   117  				// This will result in a 503 with the UH flag because the cluster will
   118  				// stay warming until a valid secret is sent.
   119  				{
   120  					name:             "invalid client cert",
   121  					credentialToUse:  fakeCredName,
   122  					from:             apps.Ns1.C,
   123  					drSelector:       "c",
   124  					authorizeSidecar: true,
   125  					expectedResponse: ingressutil.ExpectedResponse{
   126  						StatusCode: http.StatusServiceUnavailable,
   127  					},
   128  				},
   129  				// Mutual TLS origination from an authorized sidecar to https endpoint with a CRL specifying the server certificate as revoked.
   130  				// This will result in `certificate verify failed`
   131  				{
   132  					name:             "valid crl",
   133  					credentialToUse:  credWithCRL,
   134  					from:             apps.Ns2.A,
   135  					drSelector:       "a",
   136  					authorizeSidecar: true,
   137  					expectedResponse: ingressutil.ExpectedResponse{
   138  						StatusCode:   http.StatusServiceUnavailable,
   139  						ErrorMessage: "CERTIFICATE_VERIFY_FAILED",
   140  					},
   141  				},
   142  				// Mutual TLS origination from an authorized sidecar to https endpoint with a CRL with a dummy revoked certificate.
   143  				// Since the certificate in action is not revoked, the communication should not be impacted.
   144  				{
   145  					name:             "dummy crl",
   146  					credentialToUse:  credWithDummyCRL,
   147  					from:             apps.Ns2.B,
   148  					drSelector:       "b",
   149  					authorizeSidecar: true,
   150  					expectedResponse: ingressutil.ExpectedResponse{
   151  						StatusCode: http.StatusOK,
   152  					},
   153  				},
   154  			}
   155  			for _, tc := range testCases {
   156  				t.NewSubTest(tc.name).Run(func(t framework.TestContext) {
   157  					if tc.authorizeSidecar {
   158  						serviceAccount := tc.from.Config().ServiceAccountName()
   159  						serviceAccountName := serviceAccount[strings.LastIndex(serviceAccount, "/")+1:]
   160  						authorizeSidecar(t, tc.from.Config().Namespace, serviceAccountName)
   161  					}
   162  					newTLSSidecarDestinationRule(t, apps.External.All, "MUTUAL", tc.drSelector, tc.credentialToUse,
   163  						tc.from.Config().Namespace)
   164  					callOpt := newTLSSidecarCallOpts(apps.External.All[0], host, tc.expectedResponse)
   165  					tc.from[0].CallOrFail(t, callOpt)
   166  				})
   167  			}
   168  		})
   169  }
   170  
   171  // Authorize only specific sidecars in the namespace with secret listing permissions.
   172  func authorizeSidecar(t framework.TestContext, clientNamespace namespace.Instance, serviceAccountName string) {
   173  	args := map[string]any{
   174  		"ServiceAccount": serviceAccountName,
   175  		"Namespace":      clientNamespace.Name(),
   176  	}
   177  
   178  	role := `
   179  apiVersion: rbac.authorization.k8s.io/v1
   180  kind: Role
   181  metadata:
   182    name: allow-list-secrets
   183  rules:
   184  - apiGroups:
   185    - ""
   186    resources:
   187    - secrets
   188    verbs:
   189    - list
   190  `
   191  
   192  	rolebinding := `
   193  apiVersion: rbac.authorization.k8s.io/v1
   194  kind: RoleBinding
   195  metadata:
   196    name: allow-list-secrets-to-{{ .ServiceAccount }}
   197  roleRef:
   198    apiGroup: rbac.authorization.k8s.io
   199    kind: Role
   200    name: allow-list-secrets
   201  subjects:
   202  - kind: ServiceAccount
   203    name: {{ .ServiceAccount }}
   204    namespace: {{ .Namespace }}
   205  `
   206  	t.ConfigIstio().Eval(clientNamespace.Name(), args, role, rolebinding).ApplyOrFail(t, apply.NoCleanup)
   207  }
   208  
   209  func newTLSSidecarDestinationRule(t framework.TestContext, to echo.Instances, destinationRuleMode string,
   210  	workloadSelector string, credentialName string, clientNamespace namespace.Instance,
   211  ) {
   212  	args := map[string]any{
   213  		"to":               to,
   214  		"Mode":             destinationRuleMode,
   215  		"CredentialName":   credentialName,
   216  		"WorkloadSelector": workloadSelector,
   217  	}
   218  	se := `
   219  apiVersion: networking.istio.io/v1alpha3
   220  kind: ServiceEntry
   221  metadata:
   222    name: originate-mtls-for-nginx
   223  spec:
   224    hosts:
   225    - "{{ .to.Config.ClusterLocalFQDN }}"
   226    ports:
   227    - number: 80
   228      name: http-port
   229      protocol: HTTP
   230      targetPort: 443
   231    - number: 443
   232      name: https-port
   233      protocol: HTTPS
   234    resolution: DNS
   235  `
   236  	dr := `
   237  apiVersion: networking.istio.io/v1alpha3
   238  kind: DestinationRule
   239  metadata:
   240    name: originate-tls-for-server-sds-{{.WorkloadSelector}}
   241  spec:
   242    workloadSelector:
   243      matchLabels:
   244        app: {{.WorkloadSelector}}
   245    exportTo:
   246      - .
   247    host: "{{ .to.Config.ClusterLocalFQDN }}"
   248    trafficPolicy:
   249      portLevelSettings:
   250        - port:
   251            number: 80
   252          tls:
   253            mode: {{.Mode}}
   254            credentialName: {{.CredentialName}}
   255            sni: {{ .to.Config.ClusterLocalFQDN }}
   256  `
   257  	t.ConfigIstio().Eval(clientNamespace.Name(), args, se, dr).ApplyOrFail(t, apply.NoCleanup)
   258  }
   259  
   260  func newTLSSidecarCallOpts(to echo.Target, host string, exRsp ingressutil.ExpectedResponse) echo.CallOptions {
   261  	return echo.CallOptions{
   262  		To: to,
   263  		Port: echo.Port{
   264  			Protocol: protocol.HTTP,
   265  		},
   266  		HTTP: echo.HTTP{
   267  			Headers: headers.New().WithHost(host).Build(),
   268  		},
   269  		Check: func(result echo.CallResult, err error) error {
   270  			// Check that the error message is expected.
   271  			if err != nil {
   272  				// If expected error message is empty, but we got some error
   273  				// message then it should be treated as error.
   274  				if len(exRsp.ErrorMessage) == 0 {
   275  					return fmt.Errorf("unexpected error: %w", err)
   276  				}
   277  				if !strings.Contains(err.Error(), exRsp.ErrorMessage) {
   278  					return fmt.Errorf("expected response error message %s but got %w and the response code is %+v",
   279  						exRsp.ErrorMessage, err, result.Responses)
   280  				}
   281  				return nil
   282  			}
   283  			return check.And(check.NoErrorAndStatus(exRsp.StatusCode), check.BodyContains(exRsp.ErrorMessage)).Check(result, nil)
   284  		},
   285  	}
   286  }