Cloud Service Mesh by example: mTLS

Note: This guide only supports Cloud Service Mesh with Istio APIs and doesnot support Google Cloud APIs. For more information see,Cloud Service Mesh overview.

In Cloud Service Mesh 1.5 and later, auto mutual TLS (auto mTLS) is enabled bydefault. With auto mTLS, a client sidecar proxy automatically detects if theserver has a sidecar. The client sidecar sends mTLS to workloads with sidecarsand sends plaintext to workloads without sidecars. Note, however, servicesaccept both plaintext and mTLS traffic. As youinject sidecar proxies to your Pods, werecommend that you also configure your services to only accept mTLS traffic.

With Cloud Service Mesh, you can enforce mTLS, outside of your application code, byapplying a single YAML file. Cloud Service Mesh gives you the flexibility to apply anauthentication policy to the entire service mesh, to a namespace, or to anindividual workload.

mutual mTLS

Costs

In this document, you use the following billable components of Google Cloud:

To generate a cost estimate based on your projected usage, use thepricing calculator.

New Google Cloud users might be eligible for afree trial.

When you finish this tutorial, you can avoid ongoing costs by deleting theresources you created. For more information, seeClean up.

Before you begin

Deploy an ingress gateway

  1. Set the current context forkubectl to the cluster:

    Note: Use--region instead of--zone, if the cluster is a regionalcluster.
    gcloud container clusters get-credentialsCLUSTER_NAME  \--project=PROJECT_ID \--zone=CLUSTER_LOCATION
  2. Create a namespace for your ingress gateway:

    kubectl create namespace asm-ingress
  3. Enable the namespace for injection. The steps depend on yourcontrol plane implementation.

    Managed (TD)

    Apply the default injection label to the namespace:

    kubectllabelnamespaceasm-ingress\istio.io/rev-istio-injection=enabled--overwrite

    Managed (Istiod)

    Recommended: Run the following command to apply the default injection label to the namespace:

    kubectllabelnamespaceasm-ingress\istio.io/rev-istio-injection=enabled--overwrite

    If you are an existing user with the Managed Istiod control plane:We recommend that you use default injection, but revision-based injection issupported. Use the following instructions:

    1. Run the following command to locate the available release channels:

      kubectl-nistio-systemgetcontrolplanerevision

      The output is similar to the following:

      NAME                AGEasm-managed-rapid   6d7h
      Note: If two control plane revisions appear in the earlier list, remove one. Having multiple control plane channels in the cluster is not supported.

      In the output, the value under theNAME column is the revision label that corresponds to the availablerelease channel for the Cloud Service Mesh version.

    2. Apply the revision label to the namespace:

      kubectllabelnamespaceasm-ingress\istio-injection-istio.io/rev=REVISION_LABEL--overwrite
  4. Deploy the example gateway in theanthos-service-mesh-samples repository:

    kubectl apply -n asm-ingress \-f docs/shared/asm-ingress-gateway

    Expected output:

    serviceaccount/asm-ingressgateway configuredservice/asm-ingressgateway configureddeployment.apps/asm-ingressgateway configuredgateway.networking.istio.io/asm-ingressgateway configured

Deploy the Online Boutique sample application

  1. If you haven't, set the current context forkubectl to the cluster:

      gcloud container clusters get-credentialsCLUSTER_NAME  \    --project=PROJECT_ID \    --zone=CLUSTER_LOCATION
  2. Create the namespace for the sample application:

      kubectl create namespace onlineboutique
  3. Label theonlineboutique namespace to automatically inject Envoy proxies. Follow the stepsto enable automatic sidecar injection.

  4. Deploy the sample app, theVirtualService for the frontend, and service accounts for the workloads. For this tutorial, you will deployOnline Boutique, a microservice demo app.

      kubectl apply \  -n onlineboutique \  -f docs/shared/online-boutique/virtual-service.yaml  kubectl apply \  -n onlineboutique \  -f docs/shared/online-boutique/service-accounts

View your services

  1. View the pods in theonlineboutique namespace:

    kubectl get pods -n onlineboutique

    Expected output:

    NAME                                     READY   STATUS    RESTARTS   AGEadservice-85598d856b-m84m6               2/2     Running   0          2m7scartservice-c77f6b866-m67vd              2/2     Running   0          2m8scheckoutservice-654c47f4b6-hqtqr         2/2     Running   0          2m10scurrencyservice-59bc889674-jhk8z         2/2     Running   0          2m8semailservice-5b9fff7cb8-8nqwz            2/2     Running   0          2m10sfrontend-77b88cc7cb-mr4rp                2/2     Running   0          2m9sloadgenerator-6958f5bc8b-55q7w           2/2     Running   0          2m8spaymentservice-68dd9755bb-2jmb7          2/2     Running   0          2m9sproductcatalogservice-84f95c95ff-c5kl6   2/2     Running   0          114srecommendationservice-64dc9dfbc8-xfs2t   2/2     Running   0          2m9sredis-cart-5b569cd47-cc2qd               2/2     Running   0          2m7sshippingservice-5488d5b6cb-lfhtt         2/2     Running   0          2m7s

    All of the pods for your application should be up and running, with a2/2 in theREADY column. This indicates that the pods have an Envoy sidecar proxy injected successfully. If it does not show2/2 after a couple of minutes, visit theTroubleshooting guide.

  2. Get the external IP, and set it to a variable:

    kubectl get services -n asm-ingressexport FRONTEND_IP=$(kubectl --namespace asm-ingress \get service --output jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}' \)

    You see output similar to the following:

    NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                      AGEasm-ingressgateway   LoadBalancer   10.19.247.233   35.239.7.64   80:31380/TCP,443:31390/TCP,31400:31400/TCP   27m
  3. Visit theEXTERNAL-IP address in your web browser. You should expect to seethe Online Boutique shop in your browser.

    online boutique frontend

Create a TestCurl pod

Create a TestCurl pod to send plaintext traffic for testing.

apiVersion:v1kind:Podmetadata:name:testcurlnamespace:defaultannotations:sidecar.istio.io/inject:"false"spec:containers:-name:curlimage:curlimages/curlcommand:["sleep","600"]

Access Online Boutique

  1. Set the current context forkubectl to the cluster where you deployedOnline Boutique:

    gcloud container clusters get-credentialsCLUSTER_NAME  \    --project=PROJECT_ID \    --zone=CLUSTER_LOCATION
  2. List the services in thefrontend namespace:

    kubectl get services -n frontend

    Notice thatfrontend-external is aLoadBalancer, and it has anexternal IP address. The sample application includes a service that is aload balancer so that it can be deployed on GKE withoutCloud Service Mesh.

  3. Visit the application in your browser using the external IP address of thefrontend-external service:

    http://FRONTEND_EXTERNAL_IP/
  4. Cloud Service Mesh provides you the ability to deploy an ingress gateway. You canalso access the Online Boutique using the external IP address of the ingressgateway. Get the external IP of the gateway. Replace the placeholders withthe following information:

    • GATEWAY_SERVICE_NAME : The name of the ingress gatewayservice. If you deployed the sample gateway without modification, or ifyou deployed thedefault ingress gateway,the name isistio-ingressgateway.
    • GATEWAY_NAMESPACE: The namespace in which you deployedthe ingress gateway. If you deployed the default ingress gateway, thenamespace isistio-system.
    kubectl get serviceGATEWAY_NAME -nGATEWAY_NAMESPACE
  5. Open another tab in your browser and visit the application using theexternal IP address of the ingress gateway:

    http://INGRESS_GATEWAY_EXTERNAL_IP/
  6. Run the following command tocurl thefrontend service with plain HTTPfrom another Pod. Because the services are in different namespaces, youneed to curl theDNS name of thefrontend service.

    kubectl debug --image istio/base --target istio-proxy -it \  $(kubectl get pod -l app=productcatalogservice -n product-catalog -o jsonpath={.items..metadata.name}) \  -n product-catalog -- \  curl http://frontend.frontend.svc.cluster.local:80/ -o /dev/null -s -w '%{http_code}\n'

    Your request succeeds with status200, because by default, both TLS andplaintext traffic are accepted.

Enable mutual TLS per namespace

You enforce mTLS by applying aPeerAuthentication policy withkubectl.

  1. Save the following authentication policy asmtls-namespace.yaml.

    cat <<EOF > mtls-namespace.yamlapiVersion: "security.istio.io/v1beta1"kind: "PeerAuthentication"metadata:  name: "namespace-policy"spec:  mtls:    mode: STRICTEOF

    The linemode: STRICT in the YAML configures the services to onlyaccept mTLS. By default, themode isPERMISSIVE, which configuresservices to accept both plaintext and mTLS.

  2. Apply the authentication policy to configure all Online Boutiqueservices to only accept mTLS:

    fornsinadcartcheckoutcurrencyemailfrontendloadgenerator\paymentproduct-catalogrecommendationshipping;dokubectlapply-n$ns-fmtls-namespace.yamldone

    Expected output:

    peerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy createdpeerauthentication.security.istio.io/namespace-policy created

  3. Go to the tab in your browser that accesses the Online Boutique using theexternal IP address of thefrontend-external service:

    http://FRONTEND_EXTERNAL_IP/
  4. Refresh the page. The browser displays the following error:

    site can't be reached

    Refreshing the page causes plaintext to be sent to thefrontend service.Because of theSTRICTauthentication policy, the sidecar proxy blocks therequest to the service.

  5. Go to the tab in your browser that accesses the Online Boutique using theexternal IP address of theistio-ingressgateway, and refresh the page,which displays successfully. When you access Online Boutique using theingress gateway, the request takes the following path:

    mutual mTLS

    mTLS authentication flow:

    1. The browser sends a plaintext HTTP request to the server.
    2. The ingress gateway proxy container intercepts the request.
    3. The ingress gateway proxy performs a TLS handshake with theserver-side proxy (the frontend service in this example). This handshakeincludes an exchange of certificates. These certs are pre-loaded intothe proxy containers by Cloud Service Mesh.
    4. The ingress gateway proxy performs a secure naming check on theserver's certificate, verifying that an authorized identity is runningthe server.
    5. The ingress gateway and server proxies establish a mutual TLSconnection, and the server proxy forwards the request to the serverapplication container (the frontend service).
  6. Run the following command tocurl thefrontend service with plain HTTPfrom another Pod.

    kubectl exec testcurl -n default -- curl \  http://frontend.frontend.svc.cluster.local:80/ -o /dev/null -s -w '%{http_code}\n'

    Your request fails as we are sending plaintext traffic from sidecar-less workload where STRICTpeerAuthentication policy is applied.

Find and delete authentication policies

  1. For a list of all thePeerAuthentication policies in the service mesh:

    kubectl get peerauthentication --all-namespaces

    The output is similar to the following:

    NAMESPACE         NAME               MODE     AGEad                namespace-policy   STRICT   17mcart              namespace-policy   STRICT   17mcheckout          namespace-policy   STRICT   17mcurrency          namespace-policy   STRICT   17memail             namespace-policy   STRICT   17mfrontend          namespace-policy   STRICT   17mloadgenerator     namespace-policy   STRICT   17mpayment           namespace-policy   STRICT   17mproduct-catalog   namespace-policy   STRICT   17mrecommendation    namespace-policy   STRICT   17mshipping          namespace-policy   STRICT   17m
  2. Delete the authentication policy from all of the Online Boutiquenamespaces:

    fornsinadcartcheckoutcurrencyemailfrontendloadgeneratorpayment\product-catalogrecommendationshipping;dokubectldeletepeerauthentication-n$nsnamespace-policydone;

    Expected output:

    peerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deletedpeerauthentication.security.istio.io "namespace-policy" deleted
  3. Access the Online Boutique using the external IP address of thefrontend-external service, and refresh the page. The page displays asexpected.

  4. Run the following command tocurl thefrontend service with plain HTTPfrom another Pod.

    kubectl debug --image istio/base --target istio-proxy -it \  $(kubectl get pod -l app=productcatalogservice -n product-catalog -o jsonpath={.items..metadata.name}) \  -n product-catalog -- \  curl http://frontend.frontend.svc.cluster.local:80/ -o /dev/null -s -w '%{http_code}\n'

    Your request succeeds with status200, because by default, both TLS andplaintext traffic are accepted.

If you refresh the page in the Google Cloud console that displays theWorkloads list, it now shows that the mTLS status isPermissive.

Enable mutual TLS per workload

To set aPeerAuthentication policy for a specific workload, you must configuretheselector section and specify the labels that match the desired workload.However, Cloud Service Mesh can't aggregate workload-level policies for outboundmTLS traffic to a service. You need to configure a destination rule to managethat behavior.

  1. Apply an authentication policy to a specific workload. Notice how thefollowing policy uses labels and selectors to target the specificfrontenddeployment.

    cat <<EOF | kubectl apply -n frontend -f -apiVersion: "security.istio.io/v1beta1"kind: "PeerAuthentication"metadata:  name: "frontend"  namespace: "frontend"spec:  selector:    matchLabels:      app: frontend  mtls:    mode: STRICTEOF

    Expected output:

    peerauthentication.security.istio.io/frontend created
  2. Configure a matching destination rule.

    cat <<EOF | kubectl apply -n frontend -f -apiVersion: "networking.istio.io/v1alpha3"kind: "DestinationRule"metadata:  name: "frontend"spec:  host: "frontend.demo.svc.cluster.local"  trafficPolicy:    tls:      mode: ISTIO_MUTUALEOF

    Expected output:

    destinationrule.networking.istio.io/frontend created
  3. Access the Online Boutique using the external IP address of thefrontend-external service, and refresh the page. The page doesn'tdisplay because because thefrontend service is set toSTRICT mTLS, and the sidecar proxy blocks the request.

  4. Run the following command tocurl thefrontend service with plain HTTPfrom another Pod.

    kubectl exec testcurl -n default -- curl \  http://frontend.frontend.svc.cluster.local:80/ -o /dev/null -s -w '%{http_code}\n'

    Your request fails as we are sending plaintext traffic from sidecar-less workload where STRICTpeerAuthentication policy is applied.

  5. Delete the authentication policy:

    kubectl delete peerauthentication -n frontend frontend

    Expected output:

    peerauthentication.security.istio.io "frontend" deleted
  6. Delete the destination rule:

    kubectl delete destinationrule -n frontend frontend

    Expected output:

    destinationrule.networking.istio.io "frontend" deleted

Enforcing mesh-wide mTLS

To prevent all your services in the mesh from accepting plaintext traffic, seta mesh-widePeerAuthentication policy with the mTLS mode set toSTRICT.The mesh-widePeerAuthentication policy shouldn't have a selector and must beapplied in the root namespace,istio-system. When you deploy the policy, thecontrol plane automatically provisions TLS certificates so that workloads canauthenticate with each other.

  1. Enforce mesh-wide mTLS:

    kubectl apply -f - <<EOFapiVersion: "security.istio.io/v1beta1"kind: "PeerAuthentication"metadata:  name: "mesh-wide"  namespace: "istio-system"spec:  mtls:    mode: STRICTEOF

    Expected output:

    peerauthentication.security.istio.io/mesh-wide created

  2. Access the Online Boutique using the external IP address of thefrontend-external service, and refresh the page. The page doesn'tdisplay.

  3. Run the following command tocurl thefrontend service with plain HTTPfrom another Pod.

    kubectl exec testcurl -n default -- curl \  http://frontend.frontend.svc.cluster.local:80/ -o /dev/null -s -w '%{http_code}\n'

    Your request fails as we are sending plaintext traffic from sidecar-less workload where STRICTpeerAuthentication policy is applied.

  4. Delete themesh-wide policy:

    kubectl delete peerauthentication -n istio-system mesh-wide

    Expected output:

    peerauthentication.security.istio.io "mesh-wide" deleted
Note: For more information on authorization policies, seeCloud Service Mesh by example: Authorization.

Clean up

To avoid incurring charges to your Google Cloud account for the resources used in this tutorial, either delete the project that contains the resources, or keep the project and delete the individual resources.

  • If you want to prevent additional charges, delete the cluster:

    gcloud container clusters deleteCLUSTER_NAME  \    --project=PROJECT_ID \    --zone=CLUSTER_LOCATION
  • If you want to keep your cluster and remove the Online Boutique sample:

    1. Delete the application namespaces:
      kubectl delete -f online-boutique/kubernetes-manifests/namespaces

    Expected output:

    namespace "ad" deletednamespace "cart" deletednamespace "checkout" deletednamespace "currency" deletednamespace "email" deletednamespace "frontend" deletednamespace "loadgenerator" deletednamespace "payment" deletednamespace "product-catalog" deletednamespace "recommendation" deletednamespace "shipping" deleted
    1. Delete the service entries:
      kubectl delete -f online-boutique/istio-manifests/allow-egress-googleapis.yaml

    Expected output:

    serviceentry.networking.istio.io "allow-egress-googleapis" deletedserviceentry.networking.istio.io "allow-egress-google-metadata" deleted

What's next

Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2026-02-19 UTC.