Securing the istio Service Mesh using cert-manager
This guide will run through installing and using istio-csr from scratch. We'll use kind to create a new cluster locally in Docker, but this guide should work on any cluster as long as the relevant Istio Platform Setup has been performed.
Note that if you're following the Platform Setup guide for OpenShift, do not run the istioctl install
command listed in that guide; we'll run our own command later.
Initial Setup
You'll need the following tools installed on your machine:
In addition, Istio must not already be installed in your cluster. Installing istio-csr after Istio is not supported.
Creating the Cluster and Installing cert-manager
Kind will automatically set up kubectl to point to the newly created cluster.
We install cert-manager using helm here, but if you've got a preferred method you can install in any way.
kind create cluster --image=docker.io/kindest/node:v1.22.4# Helm setuphelm repo add jetstack https://charts.jetstack.iohelm repo update# install cert-manager CRDskubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.8.0/cert-manager.crds.yaml# install cert-manager; this might take a little timehelm install cert-manager jetstack/cert-manager \--namespace cert-manager \--create-namespace \--version v1.8.0# We need this namespace to exist since our cert will be placed therekubectl create namespace istio-system
Create a cert-manager Issuer and Issuing Certificate
An Issuer tells cert-manager how to issue certificates; we'll create a self-signed root CA in our cluster because it's really simple to configure.
The approach of using a locally generated root certificate would work in a production deployment too, but there are also several other issuers in cert-manager which could be used. Note that the ACME issuer will not work, since it can't add the required fields to issued certificates.
There are also some comments on the example-issuer providing a little more detail. Note also that this guide only uses Issuer
s and not ClusterIssuer
s - using a ClusterIssuer
isn't a drop-in replacement, and in any case we recommend that production deployments use Issuers for easier access controls and scoping.
kubectl apply -f https://raw.githubusercontent.com/cert-manager/website/master/content/docs/tutorials/istio-csr/example/example-issuer.yaml
Export the Root CA to a Local File
While it's possible to configure Istio such that it can automatically "discover" the root CA, this can be dangerous in some specific scenarios involving other security holes, enabling signer hijacking attacks.
As such, we'll export our Root CA and configure Istio later using that static cert.
# Export our cert from the secret it's stored in, and base64 decode to get the PEM data.kubectl get -n istio-system secret istio-ca -ogo-template='{{index .data "tls.crt"}}' | base64 -d > ca.pem# Out of interest, we can check out what our CA looks likeopenssl x509 -in ca.pem -noout -text# Add our CA to a secretkubectl create secret generic -n cert-manager istio-root-ca --from-file=ca.pem=ca.pem
Installing istio-csr
istio-csr is best installed via Helm, and it should be simple and quick to install. There are a bunch of other configuration options for the helm chart, which you can check out here.
helm repo add jetstack https://charts.jetstack.iohelm repo update# We set a few helm template values so we can point at our static root CAhelm install -n cert-manager cert-manager-istio-csr jetstack/cert-manager-istio-csr \--set "app.tls.rootCAFile=/var/run/secrets/istio-csr/ca.pem" \--set "volumeMounts[0].name=root-ca" \--set "volumeMounts[0].mountPath=/var/run/secrets/istio-csr" \--set "volumes[0].name=root-ca" \--set "volumes[0].secret.secretName=istio-root-ca"# Check to see that the istio-csr pod is running and readykubectl get pods -n cert-managerNAME READY STATUS RESTARTS AGEcert-manager-aaaaaaaaaa-11111 1/1 Running 0 9m46scert-manager-cainjector-aaaaaaaaaa-22222 1/1 Running 0 9m46scert-manager-istio-csr-bbbbbbbbbb-00000 1/1 Running 0 63scert-manager-webhook-aaaaaaaaa-33333 1/1 Running 0 9m46s
Installing Istio
If you're not running on kind, you may need to do some additional setup tasks before installing Istio.
We use the istioctl
CLI to install Istio, configured using a custom IstioOperator manifest.
The custom manifest does the following:
- Disables the CA server in istiod,
- Ensures that Istio workloads request certificates from istio-csr,
- Ensures that the istiod certificates and keys are mounted from the Certificate created when installing istio-csr.
First we download our demo manifest and then we apply it.
curl -sSL https://raw.githubusercontent.com/cert-manager/website/master/content/docs/tutorials/istio-csr/example/istio-config-getting-started.yaml > istio-install-config.yaml
You may wish to inspect and tweak istio-install-config.yaml
if you know what you're doing,
but this manifest should work for example purposes as-is.
If you set a custom app.tls.trustDomain
when installing istio-csr via helm earlier, you'll need to ensure that
value is repeated in istio-install-config.yaml
.
This final command will install Istio; the exact command you need might vary on different platforms, and will certainly vary on OpenShift.
# This takes a little time to completeistioctl install -f istio-install-config.yaml# If you're on OpenShift, you need a different profile:# istioctl install --set profile=openshift -f istio-install-config.yaml
You will be prompted for input to confirm your choice of Istio profile:
This will install the Istio 1.14.1 demo profile with ["Istio core" "Istiod" "Ingress gateways" "Egress gateways"] components into the cluster. Proceed? (y/N)
Confirm your selection by entering y
into the console to proceed with installation.
Validating Install
The following steps are option but can be followed to validate everything is hooked correctly:
- Deploy a sample application & watch for
certificaterequests.cert-manager.io
resources - Verify
cert-manager
logs for newcertificaterequests
and responses - Verify the CA Endpoint being used in a
istio-proxy
sidecar container - Using
istioctl
to fetch the certificate info for theistio-proxy
container
To see this all in action, lets deploy a very simple sample application from the Istio samples.
First set some environment variables whose values could be changed if needed:
# Set namespace for sample applicationexport NAMESPACE=default# Set env var for the value of the app label in manifestsexport APP=httpbin# Grab the installed version of istioexport ISTIO_VERSION=$(istioctl version -o json | jq -r '.meshVersion[0].Info.version')
We use the default
namespace for simplicity, so let's label the namespace for Istio injection:
kubectl label namespace $NAMESPACE istio-injection=enabled --overwrite
In a separate terminal you should now follow the logs for cert-manager
:
kubectl logs -n cert-manager $(kubectl get pods -n cert-manager -o jsonpath='{.items..metadata.name}' --selector app=cert-manager) --since 2m -f
In another separate terminal, lets watch the istio-system
namespace for certificaterequests
:
kubectl get certificaterequests.cert-manager.io -n istio-system -w
Now deploy the sample application httpbin
in the labeled namespace. Note the use of a
variable to match the manifest version to your installed Istio version:
kubectl apply -n $NAMESPACE -f https://raw.githubusercontent.com/istio/istio/$ISTIO_VERSION/samples/httpbin/httpbin.yaml
You should see something similar to the output here for certificaterequests
:
NAME APPROVED DENIED READY ISSUER REQUESTOR AGEistio-ca-74bnl True True selfsigned system:serviceaccount:cert-manager:cert-manager 2d2histiod-w9zh6 True True istio-ca system:serviceaccount:cert-manager:cert-manager 27mistio-csr-8ddcs istio-ca system:serviceaccount:cert-manager:cert-manager-istio-csr 0sistio-csr-8ddcs True istio-ca system:serviceaccount:cert-manager:cert-manager-istio-csr 0sistio-csr-8ddcs True True istio-ca system:serviceaccount:cert-manager:cert-manager-istio-csr 0sistio-csr-8ddcs True True istio-ca system:serviceaccount:cert-manager:cert-manager-istio-csr 0s
The key request being istio-csr-8ddcs
in our example output. You should then check your
cert-manager
log output for two log lines with this request being "Approved" and "Ready":
I0113 16:51:59.186482 1 conditions.go:261] Setting lastTransitionTime for CertificateRequest "istio-csr-8ddcs" condition "Approved" to 2022-01-13 16:51:59.186455713 +0000 UTC m=+3507.098466775I0113 16:51:59.258876 1 conditions.go:261] Setting lastTransitionTime for CertificateRequest "istio-csr-8ddcs" condition "Ready" to 2022-01-13 16:51:59.258837897 +0000 UTC m=+3507.170859959
You should now see the application is running with both the application container and the sidecar:
~ kubectl get pods -n $NAMESPACENAME READY STATUS RESTARTS AGEhttpbin-74fb669cc6-559cg 2/2 Running 0 4m
To validate that the istio-proxy
sidecar container has requested the certificate from the correct
service, check the container logs:
kubectl logs $(kubectl get pod -n $NAMESPACE -o jsonpath="{.items...metadata.name}" --selector app=$APP) -c istio-proxy
You should see some early logs similar to this example:
Istio v1.12 and earlier versions:
2022-01-13T16:51:58.495493Z info CA Endpoint cert-manager-istio-csr.cert-manager.svc:443, provider Citadel2022-01-13T16:51:58.495817Z info Using CA cert-manager-istio-csr.cert-manager.svc:443 cert with certs: var/run/secrets/istio/root-cert.pem2022-01-13T16:51:58.495941Z info citadelclient Citadel client using custom root cert: cert-manager-istio-csr.cert-manager.svc:443
Istio v1.13+
2022-01-13T16:51:58.495493Z info CA Endpoint cert-manager-istio-csr.cert-manager.svc:443, provider Citadel2022-01-13T16:51:58.495817Z info Using CA cert-manager-istio-csr.cert-manager.svc:443 cert with certs: var/run/secrets/istio/root-cert.pem2022-01-13T16:51:58.495941Z info citadelclient Citadel client using custom root cert: var/run/secrets/istio/root-cert.pem
Finally we can inspect the certificate being used in memory by Envoy. This one liner should return you the certificate being used:
istioctl proxy-config secret $(kubectl get pods -n $NAMESPACE -o jsonpath='{.items..metadata.name}' --selector app=$APP) -o json | jq -r '.dynamicActiveSecrets[0].secret.tlsCertificate.certificateChain.inlineBytes' | base64 --decode | openssl x509 -text -noout
In particular look for the following sections:
Signature Algorithm: ecdsa-with-SHA256Issuer: O=cert-manager, O=cluster.local, CN=istio-caValidityNot Before: Jan 13 16:51:59 2022 GMTNot After : Jan 13 17:51:59 2022 GMT...X509v3 Subject Alternative Name:URI:spiffe://cluster.local/ns/default/sa/httpbin
You should see the relevant Trust Domain inside the Issuer. In the default case, it should be:
cluster.local
as above. Note that the SPIFFE URI may be different if you used a different
namespace or application.
Clean up
Assuming your running inside kind, you can simply remove the cluster:
kind delete cluster