--- title: Mutual TLS Migration description: Shows you how to incrementally migrate your Istio services to mutual TLS. weight: 40 keywords: [security,authentication,migration] aliases: - /docs/tasks/security/mtls-migration/ owner: istio/wg-security-maintainers test: yes --- This task shows how to ensure your workloads only communicate using mutual TLS as they are migrated to Istio. Istio automatically configures workload sidecars to use [mutual TLS](/docs/tasks/security/authentication/authn-policy/#auto-mutual-tls) when calling other workloads. By default, Istio configures the destination workloads using `PERMISSIVE` mode. When `PERMISSIVE` mode is enabled, a service can accept both plaintext and mutual TLS traffic. In order to only allow mutual TLS traffic, the configuration needs to be changed to `STRICT` mode. You can use the [Grafana dashboard](/docs/tasks/observability/metrics/using-istio-dashboard/) to check which workloads are still sending plaintext traffic to the workloads in `PERMISSIVE` mode and choose to lock them down once the migration is done. ## Before you begin * Understand Istio [authentication policy](/docs/concepts/security/#authentication-policies) and related [mutual TLS authentication](/docs/concepts/security/#mutual-tls-authentication) concepts. * Read the [authentication policy task](/docs/tasks/security/authentication/authn-policy) to learn how to configure authentication policy. * Have a Kubernetes cluster with Istio installed, without global mutual TLS enabled (for example, use the `default` configuration profile as described in [installation steps](/docs/setup/getting-started)). In this task, you can try out the migration process by creating sample workloads and modifying the policies to enforce STRICT mutual TLS between the workloads. ## Set up the cluster * Create two namespaces, `foo` and `bar`, and deploy [httpbin]({{< github_tree >}}/samples/httpbin) and [sleep]({{< github_tree >}}/samples/sleep) with sidecars on both of them: {{< text bash >}} $ kubectl create ns foo $ kubectl apply -f <(istioctl kube-inject -f @samples/httpbin/httpbin.yaml@) -n foo $ kubectl apply -f <(istioctl kube-inject -f @samples/sleep/sleep.yaml@) -n foo $ kubectl create ns bar $ kubectl apply -f <(istioctl kube-inject -f @samples/httpbin/httpbin.yaml@) -n bar $ kubectl apply -f <(istioctl kube-inject -f @samples/sleep/sleep.yaml@) -n bar {{< /text >}} * Create another namespace, `legacy`, and deploy [sleep]({{< github_tree >}}/samples/sleep) without a sidecar: {{< text bash >}} $ kubectl create ns legacy $ kubectl apply -f @samples/sleep/sleep.yaml@ -n legacy {{< /text >}} * Verify the setup by sending http requests (using curl) from the sleep pods, in namespaces `foo`, `bar` and `legacy`, to `httpbin.foo` and `httpbin.bar`. All requests should succeed with return code 200. {{< text bash >}} $ for from in "foo" "bar" "legacy"; do for to in "foo" "bar"; do kubectl exec "$(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name})" -c sleep -n ${from} -- curl http://httpbin.${to}:8000/ip -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done sleep.foo to httpbin.foo: 200 sleep.foo to httpbin.bar: 200 sleep.bar to httpbin.foo: 200 sleep.bar to httpbin.bar: 200 sleep.legacy to httpbin.foo: 200 sleep.legacy to httpbin.bar: 200 {{< /text >}} {{< tip >}} If any of the curl commands fail, ensure that there are no existing authentication policies or destination rules that might interfere with requests to the httpbin service. {{< text bash >}} $ kubectl get peerauthentication --all-namespaces No resources found {{< /text >}} {{< text bash >}} $ kubectl get destinationrule --all-namespaces No resources found {{< /text >}} {{< /tip >}} ## Lock down to mutual TLS by namespace After migrating all clients to Istio and injecting the Envoy sidecar, you can lock down workloads in the `foo` namespace to only accept mutual TLS traffic. {{< text bash >}} $ kubectl apply -n foo -f - <}} Now, you should see the request from `sleep.legacy` to `httpbin.foo` failing. {{< text bash >}} $ for from in "foo" "bar" "legacy"; do for to in "foo" "bar"; do kubectl exec "$(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name})" -c sleep -n ${from} -- curl http://httpbin.${to}:8000/ip -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done sleep.foo to httpbin.foo: 200 sleep.foo to httpbin.bar: 200 sleep.bar to httpbin.foo: 200 sleep.bar to httpbin.bar: 200 sleep.legacy to httpbin.foo: 000 command terminated with exit code 56 sleep.legacy to httpbin.bar: 200 {{< /text >}} If you installed Istio with `values.global.proxy.privileged=true`, you can use `tcpdump` to verify traffic is encrypted or not. {{< text bash >}} $ kubectl exec -nfoo "$(kubectl get pod -nfoo -lapp=httpbin -ojsonpath={.items..metadata.name})" -c istio-proxy -- sudo tcpdump dst port 80 -A tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes {{< /text >}} You will see plain text and encrypted text in the output when requests are sent from `sleep.legacy` and `sleep.foo` respectively. If you can't migrate all your services to Istio (i.e., inject Envoy sidecar in all of them), you will need to continue to use `PERMISSIVE` mode. However, when configured with `PERMISSIVE` mode, no authentication or authorization checks will be performed for plaintext traffic by default. We recommend you use [Istio Authorization](/docs/tasks/security/authorization/authz-http/) to configure different paths with different authorization policies. ## Lock down mutual TLS for the entire mesh You can lock down workloads in all namespaces to only accept mutual TLS traffic by putting the policy in the system namespace of your Istio installation. {{< text bash >}} $ kubectl apply -n istio-system -f - <}} Now, both the `foo` and `bar` namespaces enforce mutual TLS only traffic, so you should see requests from `sleep.legacy` failing for both. {{< text bash >}} $ for from in "foo" "bar" "legacy"; do for to in "foo" "bar"; do kubectl exec "$(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name})" -c sleep -n ${from} -- curl http://httpbin.${to}:8000/ip -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done {{< /text >}} ## Clean up the example 1. Remove the mesh-wide authentication policy. {{< text bash >}} $ kubectl delete peerauthentication -n foo default $ kubectl delete peerauthentication -n istio-system default {{< /text >}} 1. Remove the test namespaces. {{< text bash >}} $ kubectl delete ns foo bar legacy Namespaces foo bar legacy deleted. {{< /text >}}