If security was ever important, it is even more so today. Imagine sending a confidential letter through a public mail system where anyone can read or alter its content.
That is what happens when we transmit data online without securing it. That is where SSL/TLS come in; you can think of SSL/TLS
as the security envelope of the digital world.
With the rapid rise of cloud-native applications, microservices architecture, and Software as a Service (SaaS), ensuring secure communication between front-end applications and users is more critical than ever. This is where SSL/TLS (Secure Sockets Layer/Transport Layer Security) comes into play.
In this article, we’ll explore how to secure front-end applications in a Kubernetes environment, using SSL/TLS.
Implementing SSL/TLS is not only a security best practice but it is also a necessity for safeguarding your data. It would also ensure the integrity of the communication between front-end and back-end components.
Let’s begin by understanding what SSL/TLS is and why it is so important.
SSL/TLS: A Foundation of Security
SSL/TLS
, or Secure Sockets Layer and its upgraded version, Transport Layer Security, are both cryptographic protocols that establish secure communication over the internet. In other words, they provide encryption and authentication mechanisms that enable sensitive data to travel securely between a client (e.g., a web browser) and a server. SSL is the older version, while TLS is the newer and more secure iteration.
For the purposes of this article, we’ll use the term SSL/TLS interchangeably.
How does it work?
The load balancer is typically located outside the cluster and directs internet traffic to the Ingress controller, active within the cluster. The Ingress or edge proxy accepts the traffic, reads the information in the Ingress resource, then directs the traffic to the appropriate services and pods based on the requests. The ingress also continuously monitors pods and automatically updates load balancing rules, whenever pods are added or removed.
Prerequisites:
Before we get into configuring SSL/TLS for our front-end applications in Kubernetes, these are some assumptions to keep in mind:
- You have a functioning Kubernetes cluster.
- You have helm installed on your cluster
- You have a solid understanding of Kubernetes ingress concept.
- You have the
kubectl
tool set up on your system. - You own or have control over a domain name.
- We will need SSL/TSL certificates. If you already have them, then skip steps ….. In case you don’t already have them, don’t worry; we’ll use Let’s Encrypt to obtain them.
1. Comprehending SSL/TLS in Kubernetes
Before we go any further. It is necessary for us to start by understanding the role of SSL/TLS in Kubernetes:
- Encryption: SSL/TLS protocols ensure that the communication between your front-end applications and the outside world remains secure and protected. They encrypt the data exchanged between the client and server, thereby preventing unauthorized access and data tampering.
- Authentication: Even beyond encryption, SSL/TLS also verifies the authenticity of the server. It ensures that the client communicates with the intended server and not an imposter.
Like sending mail to a verified physical address.
- Data Integrity: SSL/TLS elevates the overall security of your applications, building trust with your users and also helping you meet compliance requirements by safeguarding sensitive data. It also guarantees that the data remains unaltered during transit, much like a wax seal protecting an unaltered letter.
2. Deploying our sample front-end application
I will be using my Dog Pic website as my sample frontend application. Below is our Kubernetes manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: doggie-web
labels:
app.kubernetes.io/name: doggie-web
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: doggie-web
template:
metadata:
labels:
app.kubernetes.io/name: doggie-web
spec:
containers:
- name: doggie-container
image: learncloudnative/dogpic-service:0.1.0
ports:
- containerPort: 3000
---
kind: Service
apiVersion: v1
metadata:
name: doggie-service
labels:
app.kubernetes.io/name: doggie-web
spec:
selector:
app.kubernetes.io/name: doggie-web
ports:
- port: 3000
name: http
We will go ahead and save the above YAML file as dog-pic-application.yaml
and run the command below to create the deployment and service
kubectl apply -f dog-pic-application.yaml
2.1 Deploying cert-manager
We’ll proceed by deploying the cert-manager
inside our Kubernetes cluster. As the name suggests, the cert-manager
specializes in handling certificates. Whenever we need a new certificate or the renewal of an existing one, the cert-manager
can help us with that.
Our first step is to create a namespace for deploying the cert-manager
in.
$ kubectl create ns cert-manager
namespace/cert-manager created
Next, we will add the jetstack Helm repository and refresh the local repository cache:
First we will run the command below to add it:
$ helm repo add jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
Next, we will update our local helm repo:
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jetstack" chart repository
Update Complete. ⎈ Happy Helming!⎈
We can now go ahead and install the cert-manager. We will run the following Helm command to install it:
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v0.15.1 \
--set installCRDs=true
Notice the output of our command, you will notice the message confirming that cert-manager was deployed successfully.
However, before we can use it, we need to set up either an Issuer resource or a ClusterIssuer and configure it. This resource represents a CA (Certified Signing Authority) and allows our cert-manager to issue certificates.
While a ClusterIssuer operates at the cluster level, the Issuer resource operates at the namespace level instead. For instance, while the ClusterIssuer can be used to issue certificates for any application in the cluster, regardless of which namespace it is in. An Issuer resource, on the other hand, can only be used to issue certificates for applications in the namespace in which it is created.
Our Cert-manager supports multiple issuer types. We’ll be configuring an ACME issuer type since Let’s Encrypt relies on the ACME protocol. These protocols offer various challenge mechanisms to establish and confirm domain ownership.
2.2 Challenges
Cert-manager facilitates the verification of domain ownership using two challenges in the case of the ACME protocol: the HTTP-01 challenge and the DNS-01 challenge. Read more on the Let’s Encrypt website.
- The HTTP-01 challenge is the most common challenge. It requires you to place a file containing a token in a specific location on our server. For example http://[my-domain]/.well-known/acme-challenge/[token-file].
- The DNS-01 challenge requires you to modify a domain DNS record. To pass this challenge, we will need to create a TXT DNS record with a specific value under the domain we want to claim. It is only logical to utilize the DNS-01 challenge, if your domain registrar provides an API for updating DNS records automatically. View the complete list of service providers that have incorporated the Let’s Encrypt DNS validation.
We will be using the HTTP-01 challenge since it is more generic than the DNS-01 challenge, which depends on your domain registrar.
Now let us proceed. Let us deploy a ClusterIssuer
we will be using. Make sure you replace the email with your email address.
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: hello@thedomain.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
selector: {}
Before we go any further, let us run the command below to make sure that our ClusterIssuer has been created and that our ACME account was registered with the email we provided in the ClusterIssuer as above.
$ kubectl describe clusterissuer
...
Status:
Acme:
Last Registered Email: hello@thedomain.com
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/89498526
Conditions:
Last Transition Time: 2023-10-22T20:36:04Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>
Similarly, if we run the command kubectl get clusterissuer
We will see the indication that the ClusterIssuer
is ready:
$ kubectl get clusterissuer
NAME READY AGE
letsencrypt-prod True 5m30s
Later on, once we have deployed our Ingress controller and set up the DNS record on our domain, we will then create a Certificate
resource.
2.3 Ambassador Gateway
We will install Ambassador Gateway which is an open-source Kubernetes-native API gateway for microservices. We will use it as a reverse proxy to manage external access to services within our Kubernetes cluster.
To install Ambassador gateway, let us run the commands below. The first one will install all CRD (custom resource definitions).
$ kubectl apply -f https://www.getambassador.io/yaml/ambassador/ambassador-crds.yaml
The second command will install RBAC (Role-Based Access Control) resources and will create the Ambassador deployment.
$ kubectl apply -f https://www.getambassador.io/yaml/ambassador/ambassador-rbac.yaml
Let us now create our LoadBalancer service that will expose port 80 for HTTP traffic and port 443 for HTTPS.
apiVersion: v1
kind: Service
metadata:
name: ambassador
spec:
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
selector:
service: ambassador
Like we did before, save the YAML file above in ambassador-serv.yaml
. The run the command below
$ kubectl apply -f ambassador-svc.yaml
Please note that deploying the service above will create a load balancer in your cloud providers’ account
Let us now check if our ambassador service, has been created:
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ambassador LoadBalancer 10.0.78.66 51.143.120.54 80:31365/TCP 97s
ambassador-admin NodePort 10.0.65.191 <none> 8877:30189/TCP 4m20s
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 30d
Great job so far.
Now that we have created the external IP address, we can go to the website where we registered our domain and create a DNS record, that will point the domain to this external IP. This allows us to enter our domain http://our-domain.com in our browser and this will resolve to the IP address of the ingress controller inside the cluster.
I will be using my domain http://k8s4today.com. I will set up a subdomain, http://mydogs.k8s4today.com to point to the IP address of our Load Balancer (for example 51.140.123.54) using an A record. Irrespective of where you registered your domain, you should be able to update the DNS records. Refer to your domain registrar’s documentation for guidance on how to do that.
We will now set up our Ingress resource, so we can reach the My Dog Picture website we deployed on our subdomain (make sure you replace the dogs.k8s4today.com
with your domain or subdomain name):
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: my-ingress
annotations:
kubernetes.io/ingress.class: ambassador
spec:
rules:
- host: dogs.k8s4today.com
http:
paths:
- backend:
serviceName: doggie-service
servicePort: 3000
With our ingress deployed, the Dog Pic website can be accessed at http://dogs.k8s4today.com.
2.4 Requesting a certificate
To obtain a new certificate, we must first establish a ‘Certificate‘ resource. This resource contains the issuer reference (‘ClusterIssuer‘, which we defined before), the DNS domains for which we want certificates (‘dogs.k8s4today.com‘), and the Secret name where the certificate will be stored.
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: ambassador-certs
namespace: default
spec:
secretName: ambassador-certs
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- dogs.k8s4today.com
Make sure you replace the dogs.k8s4today.com with your domain name.
Now we will go ahead and save the YAML above in cert.yaml
and create the certificate using:
kubectl apply -f -cert.yaml
This command will create our resources. Now if we list the pods, we will notice a new pod called cm-acme-http-solver
:
$ kubectl get po
NAME READY STATUS RESTARTS AGE
ambassador-9db7b5d76-jlcdg 1/1 Running 0 22h
ambassador-9db7b5d76-qcwgk 1/1 Running 0 22h
ambassador-9db7b5d76-xsfw4 1/1 Running 0 22h
cm-acme-http-solver-qzh6l 1/1 Running 0 25m
doggie-web-7bf547bd54-f2pff 1/1 Running 0 22h
This pod was made by cert-manager to serve the token file as described in the Challenges part and check the domain name.
We can also look at the logs from the pod to see the values pod expects for the challenge:
$ kubectl logs cm-acme-http-solver-qzh6l
I0622 20:39:26.712391 1 solver.go:39] cert-manager/acmesolver "msg"="starting listener" "expected_domain"="dogs.k8s4today.com" "expected_key"="iqUZlG9v1K8czpAKaTpLfL278piwf-mN4VZNvuwD0Ks.xonKHFvEQg2Ox_mI0cPM7UpCUHfu6H4aKtRcdrpiLik" "expected_token"="iqUZlG9v1K8czpAKaTpLfL278piwf-mN4VZNvuwD0Ks" "listen_port"=8089
However, because this pod is not available (not exposed), Let’s Encrypt cannot access it and perform the challenge. As a result, we must expose this pod via an ingress. This entails creating a Kubernetes Service and updating the ingress to point to the pod. We will use Ambassador’s Mapping resource to update the ingress. This resource specifies a mapping that will redirect requests with the prefix /.well-known/acme-challenge
to the Kubernetes service that will be used by the pod.
apiVersion: getambassador.io/v2
kind: Mapping
metadata:
name: challenge-mapping
spec:
prefix: /.well-known/acme-challenge/
rewrite: ''
service: challenge-service
---
apiVersion: v1
kind: Service
metadata:
name: challenge-service
spec:
ports:
- port: 80
targetPort: 8089
selector:
acme.cert-manager.io/http01-solver: 'true'
We will save the yaml above as challenge.yaml
and deploy it using kubectl apply -f challenge.yaml
. The cert-manager will retry the challenge and issue the certificate.
Now we can run kubectl get cert
and confirm the READY column shows True as below:
$ kubectl get cert
NAME READY SECRET AGE
ambassador-certs True ambassador-certs 35m
Here are the steps we will take to request a certificate:
- Create the Certificate resource to request the certificate.
- Cert-manager constructs the http-solver pod (exposed via the challenge-service we developed).
- Cert-manager uses the issuer referenced in the Certificate to request certificates for our dnsNames from the authority (Let’s Encrypt).
- The authority (CA) sends the http-solver challenge to prove that we own the domains and verifies that the challenges are solved (by downloading the file from
/.well-known/acme-challenge/
). - The issued certificate and key are stored in the secret and are referred to by the Issuer resource.
Below is a diagram to visualize the process
Configuring TLS in Ingress
To secure our Ingress we will have to specify a Secret that contains the certificate and the private key. We defined the ambassador-certs
secret name in the Certificate
resource we created earlier.
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: my-ingress
annotations:
kubernetes.io/ingress.class: ambassador
spec:
tls:
- hosts:
- dogs.k8s4today.com
secretName: ambassador-certs
rules:
- host: dogs.k8s4today.com
http:
paths:
- path: /
backend:
serviceName: doggie-service
servicePort: 3000
Under resource specification (spec
), we use the tls
key to specify the hosts and the secret name where the certificate and private key are stored.
We will save the above YAML as ingress-tls.yaml
and apply it with
kubectl apply -f ingress-tls.yaml
3. Verification
We can now test our app. If we navigate to our sub-domain using https (e.g. https://dogs.k8s4today.com
) we can now see that the connection is secure, using a valid certificate from Let’s Encrypt.
Conclusion
Configuring SSL/TLS for your Kubernetes-deployed applications is much more than just encryption. It is about ensuring the integrity of data, establishing trust, and paving a smooth road for user interactions. Some things to keep in mind are to prioritize safety, renew certificates when necessary, and maintain a secure environment.
Today we learned how to protect our Ingress gateway to do this. We did that using the Ambassador gateway, however the same process could be followed for any other Kubernetes ingress controller.