Skip to main content

Implementing TLS in Kubernetes

Written by:
Rubaiat Hossain
Rubaiat Hossain
wordpress-sync/feature-kubernetes-polp

July 31, 2023

0 mins read

As cloud technology continues to evolve, the demand for Kubernetes is skyrocketing. As a result, security has become a top priority for developers looking to protect their application data. That's where Transport Layer Security (TLS) comes into play. TLS is essential for ensuring a secure connection between your applications and the internet.

TLS leverages asymmetric and symmetric cryptographies to keep your data secure in transit and at rest. The TLS certificate used in Kubernetes is typically a public key certificate issued by a trusted certificate authority (CA). This certificate contains information about the server's identity, the public key used for encryption, and the digital signature of the CA that issued the certificate.

This article will guide you through the implementation of an TLS connection for your applications on a Kubernetes cluster, providing you with a comprehensive understanding of TLS and its importance in securing your applications and data.

How TLS is implemented for Kubernetes workloads

When incoming requests come to an application running in a Kubernetes cluster, they go through a network component serving as a gateway for the incoming traffic. Depending on how you configure your application, this network component can be a Kubernetes Ingress controller, a load balancer service, a cluster IP service, or a node port service.

Kubernetes does not provide a default implementation of TLS encryption for the communication between the client and the network component. This means that requests are served through HTTP, not HTTPS. However, you can easily configure your Kubernetes app to use HTTPS for secure communication using TLS.

Kubernetes also provides features such as Secrets and ConfigMaps, that can be used to securely manage your TLS certificates and other sensitive data required by your application.

Historically, TLS (and its predecessor, SSL) was implemented between systems and users that required secure communications. As distributed systems have proliferated along with a massive reduction in the cost and preformance impact of implementing TLS — not to mention ever-growing privacy concerns — most experts now recommend all traffic be encryted. Some common use cases for network encryption include the following:

  • Payment systems: If your application deals with payment data, such as bank accounts and credit cards, you have to implement TLS. This is a regulatory requirement and is necessary to ensure user data is safe in transit. Encrypting payment data also instills confidence in your customers and ensures they feel secure when using your services.

  • End-to-end data encryption with a service mesh: Using an end-to-end data encryption mechanism with a service mesh like Istio, TLS can secure communication between different microservices within a Kubernetes cluster. This is a popular approach for modern, distributed microservice architectures.

  • IoT and edge computing: Kubernetes applications interacting with IoT devices or edge computing systems should implement TLS to secure data transmission between devices and the cloud, preventing unauthorized access to data generated by IoT devices.

  • Content delivery networks (CDNs): If your applications use CDNs for caching and delivering content, implementing TLS can help you ensure secure transmission of content and prevent data tampering or unauthorized access.

Implementing a TLS configuration on Kubernetes

In the following sections, you'll implement TLS for a simple Kubernetes application using a Node.js project that mocks a payment processing application. The application code and necessary Kubernetes configuration are available in this GitHub repository.

Prerequisites

To follow along with this tutorial, you need the following:

  • A container runtime engine: For this article, we will assume you have Docker installed and configured on your workstation.

  • A Kubernetes distribution: You need to install a Kubernetes distribution to create the Kubernetes cluster and other necessary resources, such as deployments and services. This tutorial uses kind (v0.18.0), but you can use any other Kubernetes distribution, including minikube or K3s.

  • kubectl: This command line interface is used to communicate with your Kubernetes cluster and issue commands. This tutorial uses version 1.26.

  • mkcert: This is used to obtain a trusted TLS certificate with a custom domain name for your development machine. You can install mkcert on your development machine following the official instructions.

  • Git and Helm: You need Git installed to use the demo project locally and the Helm package manager for Kubernetes. This guide uses Helm v3.11.2.

The following sections will walk you through implementing TLS for a demo payment application deployed to a local Kubernetes cluster. You initially deploy the app to your kind cluster and verify that it uses HTTP instead of HTTPS. Then, you’ll generate a self-signed TLS certificate and configure your payment app to use that. Finally, you’ll create a trusted TLS certificate using mkcert and verify that your Kubernetes app can be accessed over HTTPS.

Deploy the payment app to Kubernetes

To start the process of implementing TLS in your Kubernetes-based sample application, you need to clone the application to your local machine by running the following command:

git clone https://github.com/snyk-snippets/tls-in-k8s.git

Then change into the project directory using this command:

cd k8s-tls-demo

Once you're in the project directory, you can deploy the application to your Kubernetes cluster.

Use the following command to create your kind cluster:

kind create cluster --config=config.yaml

Note: The config.yaml file binds a couple of ports on the kind nodes to your localhost, this is nessesary on Docker Desktop instalations as the kind containers are actually running inside a virtual machine (VM). If you are running on a Linux workstation with docker natively installed (no Docker Desktop VM), you can omit the --config option here.

Next, we will build the container image and load it into our kind cluster.

1docker build -t demo-payment-app .
2kind load docker-image demo-payment-app

Note: optionally, you could push that image to a registry instead of using kind load, if you do so, you will also need to edit the deployment.yaml file (used below) with your registry information and update the imagePullPolicy to “Always” so it will pull that image down.

Once the cluster is created, deploy the application to your cluster with these commands:

1kubectl apply -f deployment.yaml
2kubectl apply -f service.yaml

Wait a few minutes for Kubernetes to create the pods and other necessary resources. You can check the status of your resources using these commands:

1kubectl get deployments
2kubectl get pods

Your demo project exposes the payment application using a nodePort service on port 30080. When you define a Kubernetes service without specifying the type field, Kubernetes assigns the default service type — ClusterIP.

However, the IP address exposed by ClusterIP is only accessible from within the cluster's network. This means you can't view the application running in your cluster using an external client like your web browser. In production, you might want to use a LoadBalancer service so your cloud provider can provide an external load balancer with a publicly accessible IP address.

Use these commands to see the services for this demo application:

kubectl get services

Depending on your runtime environment, you can access the demo application in one of two ways:

VM based (i.e. Docker Desktop):

To access the demo application from your browser, you will use the kind port binding set up in the config.yaml file and simply use localhost:30080.

Non-VM based (i.e. Linux w/out Docker Desktop):

To access the demo application from your browser, you need to find the IP address exposed by the nodePort with the following command:

kubectl get nodes -o wide

This should display the INTERNAL-IP of your Kubernetes control plane (ie 172.18.0.2). Now, you can access the payment application using the following:

http://172.18.0.2:30080

Make sure to replace the IP address with the actual INTERNAL-IP of your Kubernetes control plane or localhost per above directions.

Opening your browser to the above IP and port brings up the Payment Page where you can enter the card information. Because this is a mock application, there's no actual processing behind this page and the data isn't saved anywhere:

blog-kubernetes-tls-http

Depending on your browser, you should see some kind of indication that the page is Not secure near your URL bar, indicating that this application is being served through HTTP, not HTTPS. You can verify this by clicking the prompt:

blog-kubernetes-tls-http-message-not-secure

Implement a self-signed TLS certificate for your app

Now, implement TLS for the sample Kubernetes application. Create a self-signed certificate and private key:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=my-demo-app.local"

Then create a Kubernetes Secret containing the certificate and private key:

kubectl create secret tls my-demo-app-tls --key tls.key --cert tls.crt

Now, you need to install the Nginx Ingress Controller so that it can redirect incoming requests to your payment app to use HTTPS. Since you've exposed the app using nodePort, you need to install the Ingress using a custom value file that specifies the service type to NodePort.

Create a file called ingress-values.yaml with the following content:

1controller:
2  service:
3    type: NodePort
4    nodePorts:
5      http: 32080
6      https: 32443

Then install the Nginx Ingress Controller using Helm and the custom values file:

1helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
2helm repo update
3helm install ingress-nginx ingress-nginx/ingress-nginx -f ingress-values.yaml

Create an ingress.yaml file with the following content:

1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4  name: demo-payment-app-ingress
5  annotations:
6    kubernetes.io/ingress.class: "nginx"
7spec:
8  tls:
9  - hosts:
10    - my-demo-app.local
11    secretName: my-demo-app-tls
12  rules:
13  - host: my-demo-app.local
14    http:
15      paths:
16      - path: /
17        pathType: Prefix
18        backend:
19          service:
20            name: demo-payment-app
21            port:
22              number: 80

And apply this manifest file by using the following command:

kubectl apply -f ingress.yaml

Update your /etc/hosts file with the following entry:

<control_plane_IP> my-demo-app.local

Make sure you replace <control_plane_IP> with 127.0.0.1 if you are running Docker Desktop, or the IP address you retrieved from running kubectl get nodes -o wide if not.

Now you can access the payment app using HTTPS by going to https://my-demo-app.local:32443 from your browser. When you visit the app for the first time, your browser should display a warning page, allowing you to accept the risk and proceed. After you do this, you can access the site with HTTPS:

blog-kubernetes-tls-https-not-secure

Your browser displays this error because the TLS certificate is self-signed. This is evident from the fact that the HTTPS part of the URL is crossed out.

Implement a trusted TLS certificate for your app

To get around this, you can use mkcert to create a trusted certificate for your development environment. To do so, create a local CA and generate a trusted certificate for your local domain:

1mkcert -install
2mkcert my-demo-app.local

Delete the previous Secret and create a new one for mkcert:

1kubectl delete secret my-demo-app-tls
2kubectl create secret tls my-demo-app-tls --key 
3my-demo-app.local-key.pem --cert my-demo-app.local.pem

Finally, redeploy the Ingress resource by running the following:

1kubectl delete ingress demo-payment-app-ingress
2kubectl apply -f ingress.yaml

Verify the TLS for your Kubernetes application

At this point, you should be able to visit your payment app with a trusted TLS certificate. Restart the browser, then go to https://my-demo-app.local:32443 so that the new certificate can work. You should see that there's no warning this time, and the HTTPS part of the URL isn't crossed out, just like normal TLS-secured websites:

blog-kubernetes-tls-https

Click on the lock icon in the URL bar to verify that the TLS certificate is trusted by your browser and that the connection is secure:

blog-kubernetes-tls-https-message-secure

You can find the updated code examples on the solution branch of our GitHub project repository.

Conclusion

When working with sensitive data, TLS provides you with the security you need. However, implementing TLS can be challenging if you don't know what to do.

In this tutorial, you learned how to implement TLS for an application deployed to a local Kubernetes cluster. You can also use these principles to implement TLS for applications hosted on-premise or in a cloud provider.

When implementing TLS for a production system, you should consider using a public certificate from a trusted CA, such as Let's Encrypt. While this requires that you have access to your site domain, you can implement additional configurations, such as using a load balancer service to expose your application or managing multiple certificates through a certificate manager like cert-manager. The advantages of exposing your application through a load balancer include improved availability, scalability, and resilience. And using a certificate manager makes the provisioning and management of your cluster certificates effortless in the future.