Kubernetes: Setup Ingress TLS Termination

A small very well explanatory tutorial guiding you to setup an ingress with TLS termination in kubernetes

...
Raphaël ParréePublished on

In this short tutorial, you'll be setting up TLS Termination on an (nginx) Ingress controller. We know you're busy, so let's get right to it.

You'll be performing the following steps:

  • If needed: setup a cluster
  • Define a simple test http service deployment
  • Define the plain Ingress object for our service
  • Create a (self-signed) certificate using openssl
  • create the Kubernetes Secrets
  • Update the Ingress object with TLS termination

You will be creating a https-service for the imaginary company Evil Corp.

Define the cluster and namespace

You can use any cluster with an ingress controller for this tutorial. If you want to set-up a fresh clean minikube cluster for this tutorial use the following command:

$ minikube start -p edc4it

This will spin up a minikube cluster using the default driver and default memory/cpu resource.

Either way we will assume you created the following namespace while following this tutorial:

$ kubectl create namespace tls-tutorial 

Define a sample service

In order to test our TLS termination, we'll need a web service/site. We will quickly create a Deployment and Service for this purpose.

Define the Deployment inside a file named evilcorp-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: evilcorp
  namespace: tls-tutorial
  labels:
    app.kubernetes.io/name: evilcorp
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: evilcorp    
  template:
    metadata:
      labels:
        app.kubernetes.io/name: evilcorp
    spec:
      containers:
        - name: main
          image: edc4it/generic-service:1.0
          ports:
            - containerPort: 80
              protocol: TCP
              name: http

Then the service in evilcorp-svc.yaml:

apiVersion: v1
kind: Service
metadata:
  name: evilcorp
  namespace: tls-tutorial
  labels:
    app.kubernetes.io/name: evilcorp
spec:
  selector:
    app.kubernetes.io/name: evilcorp
  ports:
    - port: 80
      targetPort: http
      appProtocol: http

Apply both files using kubectl

kubectl apply -f .

Perform the following verification steps:

  • Your deployment has span up its single pod: kubectl -n tls-tutorial get deploy evilcorp
  • Your service was created: kubectl -n tls-tutorial get svc evilcorp
  • And that its endpoints have been populated: kubectl -n tls-tutorial get ep evilcorp

Define a standard Ingress (without TLS)

To start and to work progressively, let's start with a plain Ingress for our service.

First make sure you have an ingress controller installed. To enable ingress for minikube run the following command:

$ minikube -p edc4it addons enable ingress

This creates an ingress controller in the ingress-nginx namespace:

$ kubectl get deployments -l app.kubernetes.io/instance=ingress-nginx --all-namespaces
NAMESPACE       NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
ingress-nginx   ingress-nginx-controller   1/1     1            1           127d21h

Create a new file evilcorp-ing.yaml and paste the following contents in it:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: evilcorp
  namespace: tls-tutorial
  labels:
    app.kubernetes.io/name: evilcorp
spec:
  rules:
    - host: www.evilcorp.com
      http: 
        paths:
        - path: /
          pathType: Prefix
          backend: 
             service:
              name: evilcorp
              port:
                name: http
        

get the HTTP NodePort of your Ingress controller:

$ kg  svc -l app.kubernetes.io/instance=ingress-nginx --all-namespaces 
NAMESPACE       NAME                                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx   ingress-nginx-controller             NodePort    10.107.156.187   <none>        80:32115/TCP,443:31957/TCP   127d22h
ingress-nginx   ingress-nginx-controller-admission   ClusterIP   10.106.58.204    <none>        443/TCP                      127d22h

The NodePort is available under the Ports header. In the example above it is 32115 for HTTP (to port 80)

For the ip address, we just need an ip address of one of your nodes. If you using minikube for this tutorial, you can get its ip using the following command:

$ minikube -p edc4it ip

You can then use that to make a request to our service

$ curl http://192.168.49.2:32115/version -H "Host: www.evilcorp.com" 

You can also use the following command, which obtains the minikube ip and ingress controller's node port using sub commands:

$ IP=$(minikube -p edc4it ip) \
   PORT=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}') && \
    curl http://$IP:$PORT/version -H "Host: www.evilcorp.com"

Define the (SAN) Certificate

In order to define a certificate to be used with an Ingress Controller, we need to use a SAN Certificate. Kubernetes still supports the Common Name Field, but it has been deprecated. In case you are not familiar with these concepts: in the "past" we would use the CN= field in the certificate to denote the server's domain name. The problem with this was that it could only hold one entry (wildcard or non-wildcard). SAN ("Subject Alternate Name") was introduced to solve this limitation.

To create a SAN certificate, define the following configuration file (we assume you name this file evilcorp.conf:

[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = NY
L = New York
O = E Corp
OU = Security
CN = www.evilcorp.com
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.evilcorp.com
DNS.2 = evilcorp.com

Note the distinguished_name points to the details in the req_distinguished_name. When using this command later, you won't be prompted for certificate fields (due to the prompt field set to false). SAN is an extension, therefore we added x509_extensions which refers to the v3_req section. Beside the SAN extension, we are using the Key usage and Extended Key Usage. The subjectAltName refers to the list under the alt_names extension. As you can see it may contain multiple domain names associated with this certificate.

To create the key and certificate, use the following command:

$ openssl req -x509 -nodes -days 730 -newkey rsa:2048 \
   -keyout evilcorp.key \
   -out evilcorp.crt \
   -config evilcorp.conf \
   -extensions 'v3_req'

For those that are unfamiliar with openssl and curious:

  • With the req command we use the certificate request and certificate generating utility
  • By using the -newkey option, we also create a new private key using RSA with 2048 nBits
  • In this article we are using a self-signed certificate. This is accomplished by passing the x509 option (By default openssl outputs a certificate signing request)
  • The certificate will be valid for 730 days
  • The nodes ("no DES") option makes sure the key won't be encrypted
  • We use the keyout and out options to save the newly created private key and certificate file
  • Our input configuration (evilcorp.conf) is specified using the config option
  • We are enabling/configuring the extensions which can be found under our v3_req section

Out of curiosity let's have a look the certificate (this also verifies if the command above was successful)

$ openssl x509 -in evilcorp.crt -noout -text

We don't want you to type commands and not understanding them, so here is the explanation of the command:

  • This time the command is x509. This is used to summon the "Certificate display and signing utility"
  • We want to display the certificate we just generated. This is supplied using the in option
  • We don't want to sign but merely display our
  • We don't need to see the encoded certificate. Therefore we use noout option
  • And finally, the goal is to display the certificate. Hence the text option (which prints the certificate in text form)

After running the command, you should be able to see your certificate. You might want to reassure yourself and notice the Issuer value (which you should recognize as Evil Corp). You can see this is a SAN certificate when looking for the "X509v3 Subject Alternative Name" extension settings. You should find your two DNS entries.

Great we've got a private key (evilcorp.key) and a certificate (evilcorp.crt).

Define the Kubernetes Secrets

Kubernetes secrets hold sensitive information. This includes arbitrary data (type: Opaque) service-accounts tokens (kubernetes.io/service-account-token), docker configuration (kubernetes.io/dockerconfigjson) and and among others, our kubernetes.io/tls

It is always best to define your kubernetes objects using the declarative approach using yaml files. In this article we will however resort to the imperative way (feel free to append -oyaml --dry-run=client > evilcorp-tls.yaml to the command below to obtain the yaml file)

$ kubectl -n tls-tutorial  create secret tls evilcorp-tls \
  --key evilcorp.key \
  --cert evilcorp.crt

This defines a Kubernetes secret named evtls of type kubernetes.io/tls with the required data entries tls.crt and tls.key as shown below (we have abbreviated the base64 encoded certificate and key data):

apiVersion: v1
kind: Secret
metadata:
  name: evilcorp-tls
  namespace: tls-tutorial
type: kubernetes.io/tls
data:
  tls.crt: LS0tL…UtLS0tLQo=
  tls.key: LS0tL…S0tLS0K

Check if the secret has been successfully created on your cluster:

$ kubectl -n tls-tutorial  describe secrets evilcorp-tls

Define TLS Termination on our Ingress

In order to configure TLS termination, we need to add tls configuration to our Ingress object.

Open your evilcorp-svc.yaml and add the following yaml under the spec:

  tls:
    - hosts:
        - www.evilcorp.com
      secretName: evilcorp-tls 

Apply your updated evilcorp-svc.yaml:

$ kubectl -f evilcorp-svc.yam

Then Check the logs of your ingress controller, that your ingress controller configuration has been updated:

$ kubectl -n ingress-nginx -l app.kubernetes.io/instance=ingress-nginx    

We need to obtain the https port on your ingress controller. Use the same command you used before when you needed the standard http port:

$ kg  svc -l app.kubernetes.io/instance=ingress-nginx --all-namespaces 
NAMESPACE       NAME                                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx   ingress-nginx-controller             NodePort    10.107.156.187   <none>        80:32115/TCP,443:31957/TCP   127d22h
ingress-nginx   ingress-nginx-controller-admission   ClusterIP   10.106.58.204    <none>        443/TCP                      127d22h

In the example above, the https port is available on the ephemeral port 31957. Let's use this to send an https request.

This time we cannot just set the Host header (with SAN there's actually a preflight request). We could update your host file, but that would just leave skeletons on your machine after this tutorial. There's an --resolve option on cUrl to resolve a hostname.

Use all of this to send the following cUrl request (recall the ip address is one of your node's. With minikube you can get such an ip using minikube -p edc4it ip)

(Spoiler it will fail!)

$  curl https://www.evilcorp.com:31957/version  --resolve "www.evilcorp.com:31957:192.168.49.2"

Or using a one-liner (will still fail :)

$ IP=$(minikube -p edc4it ip) 
   PORT=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}') && \
   curl https://www.evilcorp.com:$PORT/version --resolve "www.evilcorp.com:$PORT:$IP"

It fails because the certificate we're using is self-signed. You can obtain the certificate using the following command:

$ curl --insecure -vvI https://www.evilcorp.com:31957  --resolve  "www.evilcorp.com:31957:192.168.49.2"   2>&1 | awk 'BEGIN { cert=0 } /^\* SSL connection/ { cert=1 } /^\*/ { if (cert) print }'

or using a one-liner:

$ IP=$(minikube -p edc4it ip) 
   PORT=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}') && \
   curl --insecure -vvI https://www.evilcorp.com:31957  --resolve  "www.evilcorp.com:$PORT:$IP"   2>&1 | awk 'BEGIN { cert=0 } /^\* SSL connection/ { cert=1 } /^\*/ { if (cert) print }'

You should recognise your old friend (notice the subject)

For purposes of this tutorial, we can safely ignore the fact that we are using a self-signed certificate. To do so using cUrl, just add the --insecure (or the abbreviated -k) option to our original request:

$ IP=$(minikube -p edc4it ip) 
   PORT=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}') && \
   curl --insecure https://www.evilcorp.com:$PORT/version --resolve "www.evilcorp.com:$PORT:$IP"

And that should work. You have successfully configured your Ingress with its TLS secret to apply TLS Termination.

Even though we used a self-signed certificate, the steps to configure Kubernetes (TLS Secret and the Ingress) are the same regardless of what kind of certificate you use.

Hope this tutorial was helpful for you.