https, letsencrypt, kubernetes, ingress

Get Automatic HTTPS with Let's Encrypt and Kubernetes Ingress

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, just need to "switch on HTTPS" if you want. Plugins will take care of the rest.

Requirements

To have automatic HTTPS with Kubernetes you need to deploy the ingress controller first. But, 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 will manage SSL/TLS certificates for us is Cert manager. It will create the new certificates automatically for each ingress endpoint. Also, it will renew 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 will need to configure the default cluster issuer when deploying Cert manager to support kubernetes.io/tls-acme: "true" annotation for automatic TLS:

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

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

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

⚡ 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

When installed Cert manager provides Kubernetes custom resources:

⚡ kubectl get crd
NAME                                         AGE
certificates.certmanager.k8s.io              1m
clusterissuers.certmanager.k8s.io            1m
issuers.certmanager.k8s.io                   1m

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.certmanager.k8s.io:

⚡ cat << EOF| kubectl create -n ingress -f -
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod
    http01: {}
EOF

NOTE: Please use the valid email address!

When all is set it is time for testing. I will deploy the new Ghost blog on this cluster which will be 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
  annotations:
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
  hosts:
    - ghost.test.akomljen.com
  tls:
   - secretName: test-app-tls
     hosts:
       - ghost.test.akomljen.com
mariadb:
  replication:
    enabled: true
EOF

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

NOTE: There is a Helm issue where boolean values are not parsed as the string with a set argument. That is why I created a values file instead using --set and --set-string arguments to update default values.

After a few minutes, you can go right ahead and open defined endpoint in your browser. HTTPS is enabled 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 really helping us to automate all those things. Stay tuned for the next one.