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 }