Mutual TLS communication between Istio mesh
Mutual TLS communication is about trusting each other between client and server. It is different from standard TLS that only client need to trust the server. In mutual TLS, server also need to trust client.
In Istio mesh, this type of communication can be archive directly because all of the proxy share same root CA. Also mutual TLS communication between multiple Istio mesh also can be archive by sharing the root CA via multicluster configuration. But what should we do when we cannot share the root CA to other?
This article give you another prespective how to archive mutual TLS communication between Istio mesh using 3rd root CA that both of the mesh agree and trust, as shown in picture below:
Environment
- two Kubernetes cluster with Istio mesh enabled
- a domain (that use to access the service) that resolve to istio-ingressgateway (that should be public, we can simulate using dnsmasq but I am to lazy to do that)
- step & step-ca to generate certificate
Configuration
Certificate
Before configuring the cluster, we need to generate 3rd root CA, client & server certificate that will be use to make mutual TLS, we will using step certificate for that.
step certificate create a-b-root-ca root-cert.pem root-key.pem --profile root-ca \
--kty RSA --no-password --insecure --not-after 87600h
step certificate create cluster-a-client cluster-a-client-cert.pem cluster-a-client-key.pem \
--profile leaf --no-password --insecure --ca ./root-cert.pem --ca-key ./root-key.pem --kty RSA \
--size 2048 --not-after 8760h --san helloworld.zufar.io
step certificate create cluster-b-server cluster-b-server-cert.pem cluster-b-server-key.pem \
--profile leaf --no-password --insecure --ca ./root-cert.pem --ca-key ./root-key.pem --kty RSA \
--size 2048 --not-after 8760h --san helloworld.zufar.io
Note: I still need more context how we define the subject alternative name, will comeback here to fix this if necessary. Because we will using helloworld.zufar.io
as the service domain.
Cluster A
Cluster A will act as client side of the mutual TLS communication.
- Create namespace client
apiVersion: v1
kind: Namespace
metadata:
name: client
labels:
istio-injection: enabled
- Create certificate for client
kubectl -n client create secret generic \
cluster-a-client --from-file=cluster-a-client-cert.pem \
--from-file=cluster-a-client-key.pem --from-file=root-cert.pem
- Create client deployment and mount the certificate via Istio annotation
apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
namespace: client
spec:
replicas: 1
selector:
matchLabels:
app: sleep
template:
metadata:
annotations:
sidecar.istio.io/userVolumeMount: '[{"name":"cluster-a-client", "mountPath":"/etc/cluster-a-client", "readonly":true}]'
sidecar.istio.io/userVolume: '[{"name":"cluster-a-client", "secret":{"secretName":"cluster-a-client"}}]'
labels:
app: sleep
spec:
containers:
- name: sleep
image: curlimages/curl
command: ["/bin/sleep", "3650d"]
imagePullPolicy: IfNotPresent
This annotation will mount certificate inside a secret to istio-proxy
sidecar in the /etc/cluster-a-client
.
- Create VirtualService object
virtualService will upgrade HTTP connection to HTTPS connection when accessing helloworld.zufar.io
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: helloworld-zufar-io-vs
namespace: client
spec:
hosts:
- helloworld.zufar.io
http:
- match:
- port: 80
route:
- destination:
host: helloworld.zufar.io
subset: tls-origination
port:
number: 443
VirtualService add a subset name tls-origination
to HTTPS connection. This subset will be used by DestinationRule
.
- Create ServiceEntry object
ServiceEntry object will enable Istio control feature when accessing helloworld.zufar.io
. We will inject certificate so we need the Istio control feature
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: helloworld-zufar-io-se
namespace: client
spec:
hosts:
- helloworld.zufar.io
ports:
- number: 443
name: https-port-for-tls-origination
protocol: HTTPS
resolution: DNS
location: MESH_EXTERNAL
- Create DestinationRule object
DestinationRule object will inject certificate that require by mutual TLS
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: helloworld-zufar-io-dr
namespace: client
spec:
host: helloworld.zufar.io
subsets:
- name: tls-origination
trafficPolicy:
portLevelSettings:
- port:
number: 443
tls:
mode: MUTUAL
clientCertificate: /etc/cluster-a-client/cluster-a-client-cert.pem
privateKey: /etc/cluster-a-client/cluster-a-client-key.pem
caCertificates: /etc/cluster-a-client/root-cert.pem
Thats all on the client side, for making mutual TLS communication, client can use HTTP communication http://helloworld.zufar.io
and no code change. Istio sidecar will upgrade to HTTPS and enable mutual TLS!
Cluster B
Cluster B will act as server side, have service helloworld inside. The traffic will enter istio-ingressgateway and terminate mutual TLS there. Istio then forward traffic as plain http to helloworld service.
- Create certificate for istio-ingressgateway
The certificate must be on istio-system
namespace. Or if you have custom istio-ingressgateway, it must place in the same namespace as the istio-ingressgateway.
kubectl create -n istio-system secret generic cluster-b-server-cert \
--from-file=key=cluster-b-server-key.pem \
--from-file=cert=cluster-b-server-cert.pem \
--from-file=cacert=root-cert.pem
- Create namespace service
apiVersion: v1
kind: Namespace
metadata:
name: service
labels:
istio-injection: enabled
- Create deployment and service for helloworld
apiVersion: v1
kind: Service
metadata:
name: helloworld
namespace: service
labels:
app: helloworld
spec:
ports:
- port: 5000
name: http
selector:
app: helloworld
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld-v1
namespace: service
labels:
version: v1
spec:
replicas: 1
selector:
matchLabels:
app: helloworld
version: v1
template:
metadata:
labels:
app: helloworld
version: v1
spec:
containers:
- name: helloworld
image: docker.io/istio/examples-helloworld-v1
resources:
requests:
cpu: "100m"
imagePullPolicy: IfNotPresent #Always
ports:
- containerPort: 5000
- Create Gateway object
Gateway object will serve the HTTPS communication and add the certificate to istio-ingressgateway.
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: service-gateway
namespace: istio-system
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
tls:
httpsRedirect: true
hosts:
- "*"
- port:
number: 443
name: https
protocol: HTTPS
hosts:
- "*"
tls:
mode: MUTUAL
credentialName: cluster-b-server-cert
Note: I need more context about why the hosts should asterisk (*). Currently, my understanding is because the root CA is created without any DNS attach to it. I will get back as I get more understanding about this.
- Create VirtualService object
VirtualService object will match host helloworld.zufar.io
from service-gateway
and route to the correct helloworld
service in service
namespace.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: helloworld-zufar-io-vs
namespace: istio-system
spec:
hosts:
- helloworld.zufar.io
gateways:
- service-gateway
http:
- route:
- destination:
host: helloworld.service.svc.cluster.local
port:
number: 5000
Testing
We cannot se the mutual TLS communication because it is abstracted by Istio sidecar and Istio-ingressgateway. Client can use HTTP communication http://helloworld.zufar.io
and Istio sidecar will upgrade to HTTPS and enable mutual TLS. To see the packet, we can use wireshark with sniff to capture the packet. We can capture packet inside sidecar (istio-proxy) with this command from cluster A:
kubectl sniff POD_NAME -c istio-proxy -n client \
-f "port 443" \
-p -o - | /Applications/Wireshark.app/Contents/MacOS/Wireshark -i -
Conclusion
Mutual TLS enabled trust between client and server. Mutual TLS can be archive by difference approach, via Istio multicluster or via this way. Thank you!