kubernetes, ingress, security, https

Get Automatic HTTPS with Let's Encrypt and Kubernetes Ingress

Last update:

A few days ago I read a great post from Troy Hunt about HTTPS. The title "HTTPS is easy" is there for a good reason! HTTPS is easy, especially with the platforms like Kubernetes. Unfortunately, not all people agree with this. I understand that for some huge organizations moving all traffic to HTTPS is not trivial, but for all others saying how Google is evil with forcing it is just nonsense. You should use HTTPS for every external endpoint and with Kubernetes ingress and Let's Encrypt this can be automatic. Meaning, you need to "switch on HTTPS" if you want. Plugins take care of the rest.

Requirements

To have automatic HTTPS with Kubernetes, you need to deploy the ingress controller first. However, what is ingress? With ingress in Kubernetes, you control the routing of external traffic. Ingress controller is tightly coupled with Kubernetes API which makes it that good.

Let's wrap up all the requirements:

  • Ingress controller on top of Kubernetes
  • Automatic DNS

I wrote about the ingress controller in the past. Instructions on how to fulfill all those requirements are available in this blog post, AWS Cost Savings by Utilizing Kubernetes Ingress with Classic ELB.

Glue Everything Together

The component which manages SSL/TLS certificates is Cert manager. It creates the new certificates automatically for each ingress endpoint. Also, it renews certificates automatically when they expire. Cert manager can work with other providers as well, HashiCorp Vault for example. For all my Kubernetes related articles I use Helm for deployment because of simplicity. And not just that I highly recommend using it for production workloads. Please read my blog post about Helm if you are new to it.

You need to configure the default cluster issuer when deploying Cert manager to support kubernetes.io/tls-acme: "true" annotation for automatic TLS with those properties:

ingressShim.defaultIssuerName=letsencrypt-prod
ingressShim.defaultIssuerKind=ClusterIssuer

You can define letsencrypt-prod cluster issuer later. Let's deploy Cert manager first:

⚡ helm repo add jetstack https://charts.jetstack.io

⚡ kubectl apply --validate=false -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.12/deploy/manifests/00-crds.yaml

⚡ helm install --name cert-manager \
    --namespace ingress \
    --set ingressShim.defaultIssuerName=letsencrypt-prod \
    --set ingressShim.defaultIssuerKind=ClusterIssuer \
    jetstack/cert-manager \
    --version v0.12.0

⚡ kubectl get pod -n ingress --selector=app=cert-manager
NAME                                        READY     STATUS    RESTARTS   AGE
cert-manager-cert-manager-7797579f9-m4dbc   1/1       Running   0          1m

NOTE: Be aware of versions for cert manager and follow the instructions later for upgrades!

When installed Cert manager provides Kubernetes custom resources:

⚡ kubectl get crd | grep cert-manager.io
certificaterequests.cert-manager.io   2019-12-19T16:40:28Z
certificates.cert-manager.io          2019-12-19T16:40:28Z
challenges.acme.cert-manager.io       2019-12-19T16:40:29Z
clusterissuers.cert-manager.io        2019-12-19T16:40:31Z
issuers.cert-manager.io               2019-12-19T16:40:31Z
orders.acme.cert-manager.io           2019-12-19T16:40:32Z

The last step is to define cluster-wide issuer letsencrypt-prod, which we already set in the above steps. Let's define cluster issuer using custom resource clusterissuers.cert-manager.io:

⚡ cat << EOF| kubectl create -n ingress -f -
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
       ingress:
         class: nginx
EOF

NOTE: Please use the valid email address!

When all is ready, it is time for testing. Let's deploy the new Ghost blog on this cluster accessible through ghost.test.akomljen.com domain and with HTTPS by default. Again let's use Helm to install it:

⚡ cat > values.yaml <<EOF
serviceType: ClusterIP
ghostHost: ghost.test.akomljen.com
ingress:
  enabled: true
  hosts:
    - name: ghost.test.akomljen.com
      tls: true
      tlsSecret: test-app-tls
      annotations:
        kubernetes.io/ingress.class: nginx
        kubernetes.io/tls-acme: "true"
mariadb:
  replication:
    enabled: true
EOF

⚡ helm install --name test-app \
    -f values.yaml \
    stable/ghost

After a few minutes, you can go right ahead and open defined endpoint in your browser. HTTPS is on by default!

Summary

Easy right 😉. We are lucky that there are security professionals like Troy Hunt who promote security as something that can be "easily" implemented with the right set of patterns. At the same time, cloud-native technologies are helping us to automate all those things. Stay tuned for the next one.