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 }