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 }