Setup Istio to handle Mutual TLS (mTLS) with an external site using an Egress gateway.

15 minute read

In this post I endeavour to go through setting up Istio Egress Gateway with TLS Origination using a real-world external/remote server setup to do MTLS between an outside client and itself.

Why do I care?

I came across the need for this setup on a previous client engagement where Security was super important. The client wanted all points in the system to be secured as much as possible, which included mTLS between microservices in the AKS cluster; network segregation between all components, and the final piece was to setup MTLS between the azure cloud application and a 3rd party vendor with a public endpoint.

So, here we are!

Architecture Diagram

architecture

Key Components

  • AKS cluster
  • Istio Service Mesh
  • External NGINX Webserver with MTLS enabled.
  • FQDN mtls.cloudbuild.site used for as my example domain for the remote server.

Pre-requisites

  • az-cli installed
  • kubectl instlaled
  • istioctl installed
  • azure dns zone (or something you can resolve dns to for your certs)

MTLS Server

Certificates: server certs, client certs and intermediate certs

As per Istio’s documentation, we will use the following repo by Nicholas Jackson called mtls-go-example to create the cert combination we need.

To create your certs, as per the website run:

./generate.sh <domain_name> <some-password>

for the example they use localhost to run it, and connect to it locally via https://localhost.

When you run the ./generate.sh script, it will create 4 folders of certs for you that will make up a complete “certificate chain” aka “chain of trust”.

and looks like this:

go certs

with the following directories:

  • root cert (ca.cert.pem) & private key
  • intermediate certs (ca-chain.pem and intermediate.cert.pem) & private key
  • application (aka “server”) cert (<domain_name>.cert.pem) & private key – e.g. mtls.cloudbuild.site.cert.pem
  • client cert (<domain_name>.cert.pem) & {: .notice–info}private key – e.g. mtls.cloudbuild.site.cert.pem

now that your certs are created and you understand what each one does,

NGINX Webserver

All you need is a server that runs nginx so you can throw this nginx.conf and the certs in there to be the MTLS endpoint that we will call from our “client” later on.

I spent way too much time getting this done via terraform and ansible that I’m going to put the ansible code here: link to gist.

nginx.conf & certs

Once you have nginx up & running on your server:

  • upload the nginx.conf below and save it to /etc/nginx/nginx.conf
  • upload the following certs (I’m using my domain mtls.cloudbuild.site as an example):
    • server cert: certs/3_application/certs/mtls.cloudbuild.site.cert.pem = /etc/nginx-server-certs/tls.crt
    • server private key: certs/3_application/private/mtls.cloudbuild.site.key.pem = /etc/nginx-server-certs/tls.key
    • ca-certs: certs/2_intermediate/certs/ca-chain.cert.pem = /etc/nginx-ca-certs/ca-chain.cert.pem

The ssl_verify_client is the thing that enables MTLS with incoming calls, and ssl_verify_depth needs to be set to >2 or things behave badly.

  • restart your nginx.service e.g. on ubuntu sudo systemctl restart nginx.service

the MTLS client (test)

With our MTLS server setup We’ll use curl to test if we can talk to the server using our client certificates.

The MTLS URL to call is https://mtls.cloudbuild.site.

curl without certificates:

curl -v -k -I https://mtls.cloudbuild.site
* Rebuilt URL to: https://mtls.cloudbuild.site/
*   Trying 13.70.185.45...
* TCP_NODELAY set
* Connected to mtls.cloudbuild.site (13.70.185.45) port 443
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=US; ST=Denial; L=Springfield; O=Dis; CN=mtls.cloudbuild.site
*  start date: Apr  4 11:36:35 2020 GMT
*  expire date: Apr 14 11:36:35 2021 GMT
*  issuer: C=US; ST=Denial; O=Dis; CN=mtls.cloudbuild.site
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
> HEAD / HTTP/1.1
> Host: mtls.cloudbuild.site
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
HTTP/1.1 400 Bad Request
< Server: nginx/1.10.3 (Ubuntu)
Server: nginx/1.10.3 (Ubuntu)
< Date: Wed, 08 Apr 2020 10:13:19 GMT
Date: Wed, 08 Apr 2020 10:13:19 GMT
< Content-Type: text/html
Content-Type: text/html
< Content-Length: 262
Content-Length: 262
< Connection: close
Connection: close

<
* Closing connection 0
* TLSv1.2 (OUT), TLS alert, Client hello (1):

We get an HTTP 400 error code, telling us to go away.

Now curl the same endpoint but this time present the server with the right client certificates:

$ curl --cacert certs/2_intermediate/certs/ca-chain.cert.pem \
    --cert certs/4_client/certs/mtls.cloudbuild.site.cert.pem \
    --key certs/4_client/private/mtls.cloudbuild.site.key.pem \
    https://mtls.cloudbuild.site

Note, the ca-chain.pem will be the same intermediate ca-cert the MTLS nginx server has configured for its setup. The server has the server cert & key pair, and our client will present the client cert & key pair which is signed by the same intermediate ca-cert as the server.

And we get a successful HTTP 200 OK

* Rebuilt URL to: https://mtls.cloudbuild.site/
*   Trying 13.70.185.45...
* TCP_NODELAY set
* Connected to mtls.cloudbuild.site (13.70.185.45) port 443
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: certs/2_intermediate/certs/ca-chain.cert.pem
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS handshake, CERT verify (15):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=US; ST=Denial; L=Springfield; O=Dis; CN=mtls.cloudbuild.site
*  start date: Apr  4 11:36:35 2020 GMT
*  expire date: Apr 14 11:36:35 2021 GMT
*  common name: mtls.cloudbuild.site (matched)
*  issuer: C=US; ST=Denial; O=Dis; CN=mtls.cloudbuild.site
*  SSL certificate verify ok.
> HEAD / HTTP/1.1
> Host: mtls.cloudbuild.site
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: nginx/1.10.3 (Ubuntu)
Server: nginx/1.10.3 (Ubuntu)
< Date: Wed, 08 Apr 2020 10:25:29 GMT
Date: Wed, 08 Apr 2020 10:25:29 GMT
< Content-Type: text/html
Content-Type: text/html
< Content-Length: 557
Content-Length: 557
< Last-Modified: Wed, 08 Apr 2020 00:53:53 GMT
Last-Modified: Wed, 08 Apr 2020 00:53:53 GMT
< Connection: keep-alive
Connection: keep-alive
< ETag: "5e8d20a1-22d"
ETag: "5e8d20a1-22d"
< Accept-Ranges: bytes
Accept-Ranges: bytes

<
* Connection #0 to host mtls.cloudbuild.site left intact

And just for posterity, the logs from /var/log/nginx/listener.log with the new log_format we configured above looks like this:

curl with no certs

115.189.88.95 - - [08/Apr/2020:10:13:11 +0000] "HEAD / HTTP/1.1" 400 0 "Client fingerprint" - "Client DN" -

curl with certs

115.189.88.95 - - [08/Apr/2020:10:25:29 +0000] "HEAD / HTTP/1.1" 200 0 "Client fingerprint" 7dccb99b6584e9b6cf624290952c7bd4b905412b "Client DN" /C=US/ST=Denial/L=Springfield/O=Dis/CN=mtls.cloudbuild.site

Ok, now the real work begins… setting up an Istio Egress Gateway to do this MTLS certificate exchange with the MTLS server on the K8s cluster’s behalf.

Istio Egress Gateway Setup

You should have istioctl installed already (if not, go here).

install istio with egressgateway

istioctl manifest apply --set values.global.istioNamespace=istio-system \
    --set values.gateways.istio-ingressgateway.enabled=false \
    --set values.gateways.istio-egressgateway.enabled=true \
    --set values.global.proxy.accessLogFile="/dev/stdout" \
    --set values.sidecarInjectorWebhook.rewriteAppHTTPProbe=true
  • enables istio-egressgateway
  • disables istio-ingressgateway
  • enables access logs for istio-proxy containers
  • enables rewriteAppHTTPProbe which helps with healthcheck 503 errors.

create the cert k8s secrets

create the following secrets in the istio-system namespace so egressgateway can find them

  • 1 x tls secret with the cert & key pair
  • 1 x generic secret with the ca-chain.cert.pem file contents.
$ kubectl -n istio-system create secret tls nginx-client-certs --key certs/4_client/private/mtls.cloudbuild.site.key.pem --cert certs/4_client/certs/mtls.cloudbuild.site.cert.pem
$ kubectl -n istio-system create secret generic nginx-ca-certs --from-file=certs/2_intermediate/certs/ca-chain.cert.pem

patch the egressgateway

There’s a better way to do this by setting these mounts during the istioctl install, but this is the manual post-install way:

kubectl -n istio-system patch --type=json deploy istio-egressgateway -p "$(cat patch-egress.json)"

where patch-egress.json is

[{
  "op": "add",
  "path": "/spec/template/spec/containers/0/volumeMounts/0",
  "value": {
    "mountPath": "/etc/istio/nginx-client-certs",
    "name": "nginx-client-certs",
    "readOnly": true
  }
},
{
  "op": "add",
  "path": "/spec/template/spec/volumes/0",
  "value": {
  "name": "nginx-client-certs",
    "secret": {
      "secretName": "nginx-client-certs",
      "optional": true
    }
  }
},
{
  "op": "add",
  "path": "/spec/template/spec/containers/0/volumeMounts/1",
  "value": {
    "mountPath": "/etc/istio/nginx-ca-certs",
    "name": "nginx-ca-certs",
    "readOnly": true
  }
},
{
  "op": "add",
  "path": "/spec/template/spec/volumes/1",
  "value": {
  "name": "nginx-ca-certs",
    "secret": {
      "secretName": "nginx-ca-certs",
      "optional": true
    }
  }
}]

kill the egressgateway pod ($ kubectl -n istio-system delete pods -lapp=istio-egressgateway) so it can pick up the secrets (and the certs inside them).

Check you can now see the certs in the egressgateway pod in the istio-system namespace:

client certs

$ kubectl -n istio-system exec -ti istio-egressgateway-8544965cd5-2hdnc -- ls -al /etc/istio/nginx-client-certs
total 8
drwxrwxrwt 3 root root  120 Apr  8 13:56 .
drwxr-xr-x 1 root root 4096 Apr  8 13:56 ..
drwxr-xr-x 2 root root   80 Apr  8 13:56 ..2020_04_08_13_56_42.418467475
lrwxrwxrwx 1 root root   31 Apr  8 13:56 ..data -> ..2020_04_08_13_56_42.418467475
lrwxrwxrwx 1 root root   14 Apr  8 13:56 tls.crt -> ..data/tls.crt
lrwxrwxrwx 1 root root   14 Apr  8 13:56 tls.key -> ..data/tls.key

ca-certs

$ kubectl -n istio-system exec -ti istio-egressgateway-8544965cd5-2hdnc -- ls -al /etc/istio/nginx-ca-certs
total 8
drwxrwxrwt 3 root root  100 Apr  8 13:56 .
drwxr-xr-x 1 root root 4096 Apr  8 13:56 ..
drwxr-xr-x 2 root root   60 Apr  8 13:56 ..2020_04_08_13_56_42.910605930
lrwxrwxrwx 1 root root   31 Apr  8 13:56 ..data -> ..2020_04_08_13_56_42.910605930
lrwxrwxrwx 1 root root   24 Apr  8 13:56 ca-chain.cert.pem -> ..data/ca-chain.cert.pem

WARNING: Istio Documented Configs (NOT WORKING)

I followed the Istio documentation closely, and for the life of me could not get the configurations to work, or find any resolution to the error messages via the github issues section, or on their discuss forums and slack channel.

I logged an issues ticket (https://github.com/istio/istio.io/issues/7063) with the istio.io repo as I don’t think it’s an issue with Istio itself, just the documentation.

So the “not working” setup is as follows:

With everything setup as above, and following the Egress Gateways with TLS Origination (v1.5.0) I deployed the following configurations to a namespace called mesh-internal

---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    hosts:
    - mtls.cloudbuild.site
    tls:
      mode: MUTUAL
      serverCertificate: /etc/certs/cert-chain.pem
      privateKey: /etc/certs/key.pem
      caCertificates: /etc/certs/root-cert.pem

---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-nginx
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
  - name: nginx
    trafficPolicy:
      loadBalancer:
        simple: ROUND_ROBIN
      portLevelSettings:
      - port:
          number: 443
        tls:
          mode: ISTIO_MUTUAL
          sni: mtls.cloudbuild.site

---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-nginx-through-egress-gateway
spec:
  hosts:
  - mtls.cloudbuild.site
  gateways:
  - istio-egressgateway
  - mesh
  http:
  - match:
    - gateways:
      - mesh
      port: 80
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: nginx
        port:
          number: 443
      weight: 100
  - match:
    - gateways:
      - istio-egressgateway
      port: 443
    route:
    - destination:
        host: mtls.cloudbuild.site
        port:
          number: 443
      weight: 100
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: originate-mtls-for-nginx
spec:
  host: mtls.cloudbuild.site
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
    portLevelSettings:
    - port:
        number: 443
      tls:
        mode: MUTUAL
        clientCertificate: /etc/istio/nginx-client-certs/tls.crt
        privateKey: /etc/istio/nginx-client-certs/tls.key
        caCertificates: /etc/istio/nginx-ca-certs/ca-chain.cert.pem
        sni: mtls.cloudbuild.site

this creates 4 x objects

  • gateway.networking.istio.io/istio-egressgateway created
  • destinationrule.networking.istio.io/egressgateway-for-nginx created
  • virtualservice.networking.istio.io/direct-nginx-through-egress-gateway created
  • destinationrule.networking.istio.io/originate-mtls-for-nginx created

Errors: nginx certs & root-cert.pem

invalid path nginx certs

checking the istio-proxy container for a sleep pod inside my mesh-internal namespace:

2020-04-12T02:39:07.916502Z	info	Envoy proxy is ready
[Envoy (Epoch 0)] [2020-04-12 02:40:53.418][16][warning][config] [external/envoy/source/common/config/grpc_subscription_impl.cc:87] gRPC config for type.googleapis.com/envoy.api.v2.Cluster rejected: Error adding/updating cluster(s) outbound|443||mtls.cloudbuild.site: Invalid path: /etc/istio/nginx-ca-certs/ca-chain.cert.pem

invalid path /etc/certs/root-cert.pem

checking the istio-egressgateway pod I can see the following errors as well:

[Envoy (Epoch 0)] [2020-04-12 02:54:07.787][15][warning][config] [external/envoy/source/common/config/grpc_subscription_impl.cc:87] gRPC config for type.googleapis.com/envoy.api.v2.Listener rejected: Error adding/updating listener(s) 0.0.0.0_443: Invalid path: /etc/certs/root-cert.pem

HTTP/1.1 503 Service Unavailable (obviously)

Without the certs, checking with curl calling my mtls server I get:

$ kubectl -n mesh-internal exec sleep-74997ffb46-flkcg -c sleep -- curl -s -I http://mtls.cloudbuild.site
HTTP/1.1 503 Service Unavailable
content-length: 91
content-type: text/plain
date: Sat, 11 Apr 2020 13:24:33 GMT
server: envoy

WORKING CONFIGURATION: Unofficial.

After a lot of reading through istio githubs issues and discuss.istio.io forum, I pieced together the following changes that eventually lead to a successful TLS client-verified session with my external MTLS server.

Disclaimer: I don’t think this is how this setup is supposed to work (being able to only call the mtls server from a deployment with the annotation is a bit meh right?), this is just how I got things to work so that a bare curl to an http endpoint gets upgraded to tls/443 and does the clint-certificate verification automatically.

Our architectural diagram ends up looking more like this:

architecture

So, first the fixes for the cert errors-

/etc/root-cert.pem fix

I changed port protocal from HTTPS

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    hosts:
    - mtls.cloudbuild.site
    tls:
      mode: MUTUAL
      serverCertificate: /etc/certs/cert-chain.pem
      privateKey: /etc/certs/key.pem
      caCertificates: /etc/certs/root-cert.pem

to TLS

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: TLS
    hosts:
    - mtls.cloudbuild.site
    tls:
      mode: MUTUAL
      serverCertificate: /etc/certs/cert-chain.pem
      privateKey: /etc/certs/key.pem
      caCertificates: /etc/certs/root-cert.pem

And the error goes away.

I’m assuming its because there’s a tls section there and cert lookups get treated differently?

Invalid path: /etc/istio/nginx-ca-certs/ca-chain.cert.pem fix

For this one I came across an open issue where someone advised the sidecar of the pod calling the MTLS backend server needs to have the certs mounted to it - which sort of defeats the purpose of this “egressgateway will handle verifying calls to the backend using istio” example right?

Anyway, I did the following:

  1. created the nginx-client-certs and nginx-ca-certs secrets inside my namespace mesh-internal (where my sleep pod is deployed)
  2. added the following annotations (sidecar.istio.io/userVolumeMount and sidecar.istio.io/userVolume) to my sleep pods deployment manifest:
  ---
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: sleep
    namespace: mesh-internal
  spec:
    replicas: 1
    selector:
      matchLabels:
        app: sleep
    template:
      metadata:
        annotations:                                                                                       
          sidecar.istio.io/userVolumeMount: '[{"name":"nginx-client-certs", "mountPath":"/etc/istio/nginx-client-certs", "readonly":true},{"name":"nginx-ca-certs", "mountPath":"/etc/istio/nginx-ca-certs", "readonly":true}]'
          sidecar.istio.io/userVolume: '[{"name":"nginx-client-certs", "secret":{"secretName":"nginx-client-certs"}},{"name":"nginx-ca-certs", "secret":{"secretName":"nginx-ca-certs"}}]'
        labels:
          app: sleep
      spec:
        serviceAccountName: sleep
        containers:
        - name: sleep
          image: governmentpaas/curl-ssl
          command: ["/bin/sleep", "3650d"]
          imagePullPolicy: IfNotPresent
          volumeMounts:
          - mountPath: /etc/sleep/tls
            name: secret-volume
        volumes:
        - name: secret-volume
          secret:
            secretName: sleep-secret
            optional: true

Now my sleep pod doesn’t complain about the nginx certs anymore.

I see other pods like prometheus and an httpbin pod in my mesh-internal namespace complaining about not finding the certs, but I understand (currently) it’s because I haven’t “sidecar mounted” these certs directly to them.

Add ServiceEntry and VirtualService

I added a ServiceEntry and VirtualService combination (it wasn’t clear in the example that I needed to have one, and the previous section of the documentation delete’s the ServiceEntry so the following section seems to go ahead without one and doesn’t specify creating a new one?)

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-mtls-nginx-server
  namespace: mesh-internal
spec:
  hosts:
  - mtls.cloudbuild.site
  ports:
  - number: 80
    name: http
    protocol: HTTP
  - number: 443
    name: https
    protocol: TLS
  resolution: DNS

---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: nginx
  namespace: mesh-internal
spec:
  hosts:
  - mtls.cloudbuild.site
  tls:
  - match:
    - port: 443
      sni_hosts:
      - mtls.cloudbuild.site
    route:
    - destination:
        host: mtls.cloudbuild.site
        port:
          number: 443
      weight: 100

Changed Gateway to HTTP

Changed this from tls..

---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    hosts:
    - mtls.cloudbuild.site
    tls:
      mode: MUTUAL
      serverCertificate: /etc/certs/cert-chain.pem
      privateKey: /etc/certs/key.pem
      caCertificates: /etc/certs/root-cert.pem

to HTTP

---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
  namespace: mesh-internal
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - mtls.cloudbuild.site

Changed DestinationRule

from

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-nginx
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
  - name: nginx
    trafficPolicy:
      loadBalancer:
        simple: ROUND_ROBIN
      portLevelSettings:
      - port:
          number: 443
        tls:
          mode: ISTIO_MUTUAL
          sni: mtls.cloudbuild.site

to

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-nginx
  namespace: mesh-internal
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
  - name: nginx

Changed VirtualService to port 80

Now that my Gateway is port 80, I update the following route from istio-egressgateway.istio-system.svc.cluster.local:443

---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-nginx-through-egress-gateway
spec:
  hosts:
  - mtls.cloudbuild.site
  gateways:
  - istio-egressgateway
  - mesh
  http:
  - match:
    - gateways:
      - mesh
      port: 80
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: nginx
        port:
          number: 443
      weight: 100
  - match:
    - gateways:
      - istio-egressgateway
      port: 443
    route:
    - destination:
        host: mtls.cloudbuild.site
        port:
          number: 443
      weight: 100

to istio-egressgateway.istio-system.svc.cluster.local:80

---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-nginx-through-egress-gateway
  namespace: mesh-internal
spec:
  hosts:
  - mtls.cloudbuild.site
  gateways:
  - istio-egressgateway
  - mesh
  http:
  - match:
    - gateways:
      - mesh
      port: 80
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: nginx
        port:
          number: 80
      weight: 100
  - match:
    - gateways:
      - istio-egressgateway
      port: 80
    route:
    - destination:
        host: mtls.cloudbuild.site
        port:
          number: 443
      weight: 100

And then it all works.

Working Output

So now when I curl from the sleep pod inside the mesh-internal namespace, I get the expected output:

$ kubectl -n mesh-internal exec sleep-74997ffb46-cxs77 -c sleep -- curl  http://mtls.cloudbuild.site
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to the Mutual TLS Server!</h1>

<p>If you see this page, you have successfully used the correct client-side certificates that match the ones
  deployed on this server.
</p>

<p>For more information please visit my website:<a href="https://iamronamo.io/">iamronamo.io</a>.

<p><em>Thank you and goodnight.</em></p>
</body>
</html>

From the sleep pod’s istio-proxy container I can see it hitting my port 80 outbound endpoint:

[2020-04-11T15:03:16.493Z] "GET / HTTP/1.1" 200 - "-" "-" 0 557 4 4 "-" "curl/7.64.0" "738ceb49-93c9-4462-a53b-ab690bef4b93" "mtls.cloudbuild.site" "16.0.1.90:80" outbound|80|nginx|istio-egressgateway.istio-system.svc.cluster.local 16.0.1.103:39040 52.189.232.175:80 16.0.1.103:45092 - -

and from the istio-egressgateway pod I can see it going outbound on 443:

[2020-04-11T15:04:37.866Z] "GET / HTTP/2" 200 - "-" "-" 0 557 4 4 "16.0.1.103" "curl/7.64.0" "7d158baa-3ac4-4da7-9e91-a4ae6115c090" "mtls.cloudbuild.site" "52.189.232.175:443" outbound|443||mtls.cloudbuild.site 16.0.1.90:43866 16.0.1.90:80 16.0.1.103:39040 - -

Conclusion

I have found Istio’s documentation to be workable most of the time. But sometimes the examples run into each other, so its hard to know what are the specifics of that example without something explicitly saying “this example will create n components”, or a repo/folder with the exact configs used to achieve whatever the thing was in the example.

The discuss forum and slack channels were very underwhelming. I don’t know what to put that down to, but they weren’t very active or helpful.

Anyway I’ve spent way too much time on this and it’s just good to get it working so I can put this all behind me.

Thanks for reading!