Egress TLS Origination

The Accessing External Services task demonstrates how external, i.e., outside of the service mesh, HTTP and HTTPS services can be accessed from applications inside the mesh. As described in that task, a ServiceEntry is used to configure Istio to access external services in a controlled way. This example shows how to configure Istio to perform TLS origination for traffic to an external service. Istio will open HTTPS connections to the external service while the original traffic is HTTP.

Use case

Consider a legacy application that performs HTTP calls to external sites. Suppose the organization that operates the application receives a new requirement which states that all the external traffic must be encrypted. With Istio, this requirement can be achieved just by configuration, without changing any code in the application. The application can send unencrypted HTTP requests and Istio will then encrypt them for the application.

Another benefit of sending unencrypted HTTP requests from the source, and letting Istio perform the TLS upgrade, is that Istio can produce better telemetry and provide more routing control for requests that are not encrypted.

Before you begin

  • Setup Istio by following the instructions in the Installation guide.

  • Start the sleep sample which will be used as a test source for external calls.

    If you have enabled automatic sidecar injection, deploy the sleep application:

    Zip

    1. $ kubectl apply -f @samples/sleep/sleep.yaml@

    Otherwise, you have to manually inject the sidecar before deploying the sleep application:

    Zip

    1. $ kubectl apply -f <(istioctl kube-inject -f @samples/sleep/sleep.yaml@)

    Note that any pod that you can exec and curl from will do for the procedures below.

  • Create a shell variable to hold the name of the source pod for sending requests to external services. If you used the sleep sample, run:

    1. $ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})

Configuring access to an external service

First start by configuring access to an external service, edition.cnn.com, using the same technique shown in the Accessing External Services task. This time, however, use a single ServiceEntry to enable both HTTP and HTTPS access to the service.

  1. Create a ServiceEntry to enable access to edition.cnn.com:

    1. $ kubectl apply -f - <<EOF
    2. apiVersion: networking.istio.io/v1
    3. kind: ServiceEntry
    4. metadata:
    5. name: edition-cnn-com
    6. spec:
    7. hosts:
    8. - edition.cnn.com
    9. ports:
    10. - number: 80
    11. name: http-port
    12. protocol: HTTP
    13. - number: 443
    14. name: https-port
    15. protocol: HTTPS
    16. resolution: DNS
    17. EOF
  2. Make a request to the external HTTP service:

    1. $ kubectl exec "${SOURCE_POD}" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
    2. HTTP/1.1 301 Moved Permanently
    3. ...
    4. location: https://edition.cnn.com/politics
    5. ...
    6. HTTP/2 200
    7. ...

    The output should be similar to the above (some details replaced by ellipsis).

Notice the -L flag of curl which instructs curl to follow redirects. In this case, the server returned a redirect response (301 Moved Permanently) for the HTTP request to http://edition.cnn.com/politics. The redirect response instructs the client to send an additional request, this time using HTTPS, to https://edition.cnn.com/politics. For the second request, the server returned the requested content and a 200 OK status code.

Although the curl command handled the redirection transparently, there are two issues here. The first issue is the redundant request, which doubles the latency of fetching the content of http://edition.cnn.com/politics. The second issue is that the path of the URL, politics in this case, is sent in clear text. If there is an attacker who sniffs the communication between your application and edition.cnn.com, the attacker would know which specific topics of edition.cnn.com the application fetched. For privacy reasons, you might want to prevent such disclosure.

Both of these issues can be resolved by configuring Istio to perform TLS origination.

TLS origination for egress traffic

  1. Redefine your ServiceEntry from the previous section to redirect HTTP requests to port 443 and add a DestinationRule to perform TLS origination:

    1. $ kubectl apply -f - <<EOF
    2. apiVersion: networking.istio.io/v1
    3. kind: ServiceEntry
    4. metadata:
    5. name: edition-cnn-com
    6. spec:
    7. hosts:
    8. - edition.cnn.com
    9. ports:
    10. - number: 80
    11. name: http-port
    12. protocol: HTTP
    13. targetPort: 443
    14. - number: 443
    15. name: https-port
    16. protocol: HTTPS
    17. resolution: DNS
    18. ---
    19. apiVersion: networking.istio.io/v1
    20. kind: DestinationRule
    21. metadata:
    22. name: edition-cnn-com
    23. spec:
    24. host: edition.cnn.com
    25. trafficPolicy:
    26. portLevelSettings:
    27. - port:
    28. number: 80
    29. tls:
    30. mode: SIMPLE # initiates HTTPS when accessing edition.cnn.com
    31. EOF

    The above DestinationRule will perform TLS origination for HTTP requests on port 80 and the ServiceEntry will then redirect the requests on port 80 to target port 443.

  2. Send an HTTP request to http://edition.cnn.com/politics, as in the previous section:

    1. $ kubectl exec "${SOURCE_POD}" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
    2. HTTP/1.1 200 OK
    3. ...

    This time you receive 200 OK as the first and the only response. Istio performed TLS origination for curl so the original HTTP request was forwarded to edition.cnn.com as HTTPS. The server returned the content directly, without the need for redirection. You eliminated the double round trip between the client and the server, and the request left the mesh encrypted, without disclosing the fact that your application fetched the politics section of edition.cnn.com.

    Note that you used the same command as in the previous section. For applications that access external services programmatically, the code does not need to be changed. You get the benefits of TLS origination by configuring Istio, without changing a line of code.

  3. Note that the applications that used HTTPS to access the external service continue to work as before:

    1. $ kubectl exec "${SOURCE_POD}" -c sleep -- curl -sSL -o /dev/null -D - https://edition.cnn.com/politics
    2. HTTP/2 200
    3. ...

Additional security considerations

Because the traffic between the application pod and the sidecar proxy on the local host is still unencrypted, an attacker that is able to penetrate the node of your application would still be able to see the unencrypted communication on the local network of the node. In some environments a strict security requirement might state that all the traffic must be encrypted, even on the local network of the nodes. With such a strict requirement, applications should use HTTPS (TLS) only. The TLS origination described in this example would not be sufficient.

Also note that even with HTTPS originated by the application, an attacker could know that requests to edition.cnn.com are being sent by inspecting Server Name Indication (SNI). The SNI field is sent unencrypted during the TLS handshake. Using HTTPS prevents the attackers from knowing specific topics and articles but does not prevent attackers from learning that edition.cnn.com is accessed.

Cleanup the TLS origination configuration

Remove the Istio configuration items you created:

  1. $ kubectl delete serviceentry edition-cnn-com
  2. $ kubectl delete destinationrule edition-cnn-com

Mutual TLS origination for egress traffic

This section describes how to configure a sidecar to perform TLS origination for an external service, this time using a service that requires mutual TLS. This example is considerably more involved because it requires the following setup:

  1. Generate client and server certificates
  2. Deploy an external service that supports the mutual TLS protocol
  3. Configure the client (sleep pod) to use the credentials created in Step 1

Once this setup is complete, you can then configure the external traffic to go through the sidecar which will perform TLS origination.

Generate client and server certificates and keys

For this task you can use your favorite tool to generate certificates and keys. The commands below use openssl

  1. Create a root certificate and private key to sign the certificate for your services:

    1. $ openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt
  2. Create a certificate and a private key for my-nginx.mesh-external.svc.cluster.local:

    1. $ openssl req -out my-nginx.mesh-external.svc.cluster.local.csr -newkey rsa:2048 -nodes -keyout my-nginx.mesh-external.svc.cluster.local.key -subj "/CN=my-nginx.mesh-external.svc.cluster.local/O=some organization"
    2. $ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in my-nginx.mesh-external.svc.cluster.local.csr -out my-nginx.mesh-external.svc.cluster.local.crt

    Optionally, you can add SubjectAltNames to the certificate if you want to enable SAN validation for the destination. For example:

    1. $ cat > san.conf <<EOF
    2. [req]
    3. distinguished_name = req_distinguished_name
    4. req_extensions = v3_req
    5. x509_extensions = v3_req
    6. prompt = no
    7. [req_distinguished_name]
    8. countryName = US
    9. [v3_req]
    10. keyUsage = critical, digitalSignature, keyEncipherment
    11. extendedKeyUsage = serverAuth, clientAuth
    12. basicConstraints = critical, CA:FALSE
    13. subjectAltName = critical, @alt_names
    14. [alt_names]
    15. DNS = my-nginx.mesh-external.svc.cluster.local
    16. EOF
    17. $
    18. $ openssl req -out my-nginx.mesh-external.svc.cluster.local.csr -newkey rsa:4096 -nodes -keyout my-nginx.mesh-external.svc.cluster.local.key -subj "/CN=my-nginx.mesh-external.svc.cluster.local/O=some organization" -config san.conf
    19. $ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in my-nginx.mesh-external.svc.cluster.local.csr -out my-nginx.mesh-external.svc.cluster.local.crt -extfile san.conf -extensions v3_req
  3. Generate client certificate and private key:

    1. $ openssl req -out client.example.com.csr -newkey rsa:2048 -nodes -keyout client.example.com.key -subj "/CN=client.example.com/O=client organization"
    2. $ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 1 -in client.example.com.csr -out client.example.com.crt

Deploy a mutual TLS server

To simulate an actual external service that supports the mutual TLS protocol, deploy an NGINX server in your Kubernetes cluster, but running outside of the Istio service mesh, i.e., in a namespace without Istio sidecar proxy injection enabled.

  1. Create a namespace to represent services outside the Istio mesh, namely mesh-external. Note that the sidecar proxy will not be automatically injected into the pods in this namespace since the automatic sidecar injection was not enabled on it.

    1. $ kubectl create namespace mesh-external
  2. Create Kubernetes Secrets to hold the server’s and CA certificates.

    1. $ kubectl create -n mesh-external secret tls nginx-server-certs --key my-nginx.mesh-external.svc.cluster.local.key --cert my-nginx.mesh-external.svc.cluster.local.crt
    2. $ kubectl create -n mesh-external secret generic nginx-ca-certs --from-file=example.com.crt
  3. Create a configuration file for the NGINX server:

    1. $ cat <<\EOF > ./nginx.conf
    2. events {
    3. }
    4. http {
    5. log_format main '$remote_addr - $remote_user [$time_local] $status '
    6. '"$request" $body_bytes_sent "$http_referer" '
    7. '"$http_user_agent" "$http_x_forwarded_for"';
    8. access_log /var/log/nginx/access.log main;
    9. error_log /var/log/nginx/error.log;
    10. server {
    11. listen 443 ssl;
    12. root /usr/share/nginx/html;
    13. index index.html;
    14. server_name my-nginx.mesh-external.svc.cluster.local;
    15. ssl_certificate /etc/nginx-server-certs/tls.crt;
    16. ssl_certificate_key /etc/nginx-server-certs/tls.key;
    17. ssl_client_certificate /etc/nginx-ca-certs/example.com.crt;
    18. ssl_verify_client on;
    19. }
    20. }
    21. EOF
  4. Create a Kubernetes ConfigMap to hold the configuration of the NGINX server:

    1. $ kubectl create configmap nginx-configmap -n mesh-external --from-file=nginx.conf=./nginx.conf
  5. Deploy the NGINX server:

    1. $ kubectl apply -f - <<EOF
    2. apiVersion: v1
    3. kind: Service
    4. metadata:
    5. name: my-nginx
    6. namespace: mesh-external
    7. labels:
    8. run: my-nginx
    9. annotations:
    10. "networking.istio.io/exportTo": "." # simulate an external service by not exporting outside this namespace
    11. spec:
    12. ports:
    13. - port: 443
    14. protocol: TCP
    15. selector:
    16. run: my-nginx
    17. ---
    18. apiVersion: apps/v1
    19. kind: Deployment
    20. metadata:
    21. name: my-nginx
    22. namespace: mesh-external
    23. spec:
    24. selector:
    25. matchLabels:
    26. run: my-nginx
    27. replicas: 1
    28. template:
    29. metadata:
    30. labels:
    31. run: my-nginx
    32. spec:
    33. containers:
    34. - name: my-nginx
    35. image: nginx
    36. ports:
    37. - containerPort: 443
    38. volumeMounts:
    39. - name: nginx-config
    40. mountPath: /etc/nginx
    41. readOnly: true
    42. - name: nginx-server-certs
    43. mountPath: /etc/nginx-server-certs
    44. readOnly: true
    45. - name: nginx-ca-certs
    46. mountPath: /etc/nginx-ca-certs
    47. readOnly: true
    48. volumes:
    49. - name: nginx-config
    50. configMap:
    51. name: nginx-configmap
    52. - name: nginx-server-certs
    53. secret:
    54. secretName: nginx-server-certs
    55. - name: nginx-ca-certs
    56. secret:
    57. secretName: nginx-ca-certs
    58. EOF

Configure the client (sleep pod)

  1. Create Kubernetes Secrets to hold the client’s certificates:

    1. $ kubectl create secret generic client-credential --from-file=tls.key=client.example.com.key \
    2. --from-file=tls.crt=client.example.com.crt --from-file=ca.crt=example.com.crt

    The secret must be created in the same namespace as the client pod is deployed in, default in this case.

    Optionally, the credential may include a certificate revocation list (CRL) using the key ca.crl. If so, add another argument to the above example to provide the CRL: –from-file=ca.crl=/some/path/to/your-crl.pem.

  2. Create required RBAC to make sure the secret created in the above step is accessible to the client pod, which is sleep in this case.

    1. $ kubectl create role client-credential-role --resource=secret --verb=list
    2. $ kubectl create rolebinding client-credential-role-binding --role=client-credential-role --serviceaccount=default:sleep

Configure mutual TLS origination for egress traffic at sidecar

  1. Add a ServiceEntry to redirect HTTP requests to port 443 and add a DestinationRule to perform mutual TLS origination:

    1. $ kubectl apply -f - <<EOF
    2. apiVersion: networking.istio.io/v1
    3. kind: ServiceEntry
    4. metadata:
    5. name: originate-mtls-for-nginx
    6. spec:
    7. hosts:
    8. - my-nginx.mesh-external.svc.cluster.local
    9. ports:
    10. - number: 80
    11. name: http-port
    12. protocol: HTTP
    13. targetPort: 443
    14. - number: 443
    15. name: https-port
    16. protocol: HTTPS
    17. resolution: DNS
    18. ---
    19. apiVersion: networking.istio.io/v1
    20. kind: DestinationRule
    21. metadata:
    22. name: originate-mtls-for-nginx
    23. spec:
    24. workloadSelector:
    25. matchLabels:
    26. app: sleep
    27. host: my-nginx.mesh-external.svc.cluster.local
    28. trafficPolicy:
    29. loadBalancer:
    30. simple: ROUND_ROBIN
    31. portLevelSettings:
    32. - port:
    33. number: 80
    34. tls:
    35. mode: MUTUAL
    36. credentialName: client-credential # this must match the secret created earlier to hold client certs, and works only when DR has a workloadSelector
    37. sni: my-nginx.mesh-external.svc.cluster.local
    38. # subjectAltNames: # can be enabled if the certificate was generated with SAN as specified in previous section
    39. # - my-nginx.mesh-external.svc.cluster.local
    40. EOF

    The above DestinationRule will perform mTLS origination for HTTP requests on port 80 and the ServiceEntry will then redirect the requests on port 80 to target port 443.

    Istio has auto_sni and auto_san_validation enabled by default. This means, whenever there is no explicit sni set in your DestinationRule, transport socket SNI for new upstream connections will be set based on the downstream HTTP host/authority header. If there are no subjectAltNames set in the DestinationRule when sni is unset, auto_san_validation will kick in, and the upstream-presented certificate for new upstream connections will be automatically validated based on the downstream HTTP host/authority header.

  2. Verify that the credential is supplied to the sidecar and active.

    1. $ istioctl proxy-config secret deploy/sleep | grep client-credential
    2. kubernetes://client-credential Cert Chain ACTIVE true 1 2024-06-04T12:15:20Z 2023-06-05T12:15:20Z
    3. kubernetes://client-credential-cacert Cert Chain ACTIVE true 10792363984292733914 2024-06-04T12:15:19Z 2023-06-05T12:15:19Z
  3. Send an HTTP request to http://my-nginx.mesh-external.svc.cluster.local:

    1. $ kubectl exec "$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})" -c sleep -- curl -sS http://my-nginx.mesh-external.svc.cluster.local
    2. <!DOCTYPE html>
    3. <html>
    4. <head>
    5. <title>Welcome to nginx!</title>
    6. ...
  4. Check the log of the sleep pod for a line corresponding to our request.

    1. $ kubectl logs -l app=sleep -c istio-proxy | grep 'my-nginx.mesh-external.svc.cluster.local'

    You should see a line similar to the following:

    1. [2022-05-19T10:01:06.795Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 615 1 0 "-" "curl/7.83.1-DEV" "96e8d8a7-92ce-9939-aa47-9f5f530a69fb" "my-nginx.mesh-external.svc.cluster.local:443" "10.107.176.65:443"

Cleanup the mutual TLS origination configuration

  1. Remove created Kubernetes resources:

    1. $ kubectl delete secret nginx-server-certs nginx-ca-certs -n mesh-external
    2. $ kubectl delete secret client-credential
    3. $ kubectl delete rolebinding client-credential-role-binding
    4. $ kubectl delete role client-credential-role
    5. $ kubectl delete configmap nginx-configmap -n mesh-external
    6. $ kubectl delete service my-nginx -n mesh-external
    7. $ kubectl delete deployment my-nginx -n mesh-external
    8. $ kubectl delete namespace mesh-external
    9. $ kubectl delete serviceentry originate-mtls-for-nginx
    10. $ kubectl delete destinationrule originate-mtls-for-nginx
  2. Delete the certificates and private keys:

    1. $ rm example.com.crt example.com.key my-nginx.mesh-external.svc.cluster.local.crt my-nginx.mesh-external.svc.cluster.local.key my-nginx.mesh-external.svc.cluster.local.csr client.example.com.crt client.example.com.csr client.example.com.key
  3. Delete the generated configuration files used in this example:

    1. $ rm ./nginx.conf

Cleanup common configuration

Delete the sleep service and deployment:

  1. $ kubectl delete service sleep
  2. $ kubectl delete deployment sleep