How to terminate TLS certificate at traefik Load Balancer

Reading Time: 4 minutes

Traefik is a reverse proxy which we can use as an ingress controller for the Kubernetes. Nowadays, security is one the most important thing. So, people prefer https over http. But the challenges with https are managing the TLS certificate, renewing them. Now cert-manager solves this challenge of managing certificates. Traefik also has the built in feature where it can issue certifcate from Let’s Encrypt. But what if our we are using EKS and we want to delegate the certificates management to ACM rather than deploying cert-manager or configuring traefik.

In this blog, I will cover how we can deploy traefik with Network Load Balancer as the Load Balancer type and delegate the cert management to ACM. The Kubernetes cluster has to be EKS. ACM doesnot provide us with the private key for the certificates. So, we can only attach directly to the services running on AWS and not on services running else where.

Prerequisites

  • EKS Cluster
  • Helm 3+
  • kubectl client installed
  • certificate on ACM

Deploying Traefik to use certificate from ACM

To issue public certificate, follow the blog: https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html

Now let’s deploy the traefik:

helm repo add traefik https://helm.traefik.io/traefik

helm repo update

Next, we will need to make a slight change the traefik chart. Firstly, we will need to make these changes because we are delegating the certificate termination task to Load Balancer. So, the traffic between Load Balancer and pods are in plain text. So, we have to ensure that all traffic from load balancer are redirected to http port of traefik.

Execute the following command:

helm fetch traefik/traefik --untar

This will download the traefik Chart. Firstly, we will change the service.yaml for the Helm Chart.

{{- if .Values.service.enabled -}}

{{ $tcpPorts := dict }}
{{ $udpPorts := dict }}
{{- range $name, $config := .Values.ports }}
  {{- if or $config.http3 (eq (toString $config.protocol) "UDP") }}
    {{ $_ := set $udpPorts $name $config }}
  {{- end }}
  {{- if eq (toString (default "TCP" $config.protocol)) "TCP" }}
    {{ $_ := set $tcpPorts $name $config }}
  {{- end }}
{{- end }}

apiVersion: v1
kind: List
metadata:
  name: {{ template "traefik.fullname" . }}
items:
{{- if $tcpPorts }}
  - apiVersion: v1
    kind: Service
    metadata:
      name: {{ template "traefik.fullname" . }}
      labels:
        app.kubernetes.io/name: {{ template "traefik.name" . }}
        helm.sh/chart: {{ template "traefik.chart" . }}
        app.kubernetes.io/managed-by: {{ .Release.Service }}
        app.kubernetes.io/instance: {{ .Release.Name }}
      {{- with .Values.service.labels }}
      {{- toYaml . | nindent 8 }}
      {{- end }}
      annotations:
      {{- with (merge .Values.service.annotationsTCP .Values.service.annotations) }}
      {{- toYaml . | nindent 8 }}
      {{- end }}
    spec:
      {{- $type := default "LoadBalancer" .Values.service.type }}
      type: {{ $type }}
      {{- with .Values.service.spec }}
      {{- toYaml . | nindent 6 }}
      {{- end }}
      selector:
        app.kubernetes.io/name: {{ template "traefik.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
      ports:
      {{- range $name, $config := $tcpPorts }}
      {{- if $config.expose }}
      - port: {{ default $config.port $config.exposedPort }}
        name: {{ $name }}
# Changes are made here
        {{ if eq $name "websecure" -}}
        targetPort: "web"
        {{ else -}}
        targetPort: {{ $name | quote }}
        {{ end -}}
# Changes
        protocol: TCP
        {{- if $config.nodePort }}
        nodePort: {{ $config.nodePort }}
        {{- end }}
      {{- end }}
      {{- end }}
      {{- if eq $type "LoadBalancer" }}
      {{- with .Values.service.loadBalancerSourceRanges }}
      loadBalancerSourceRanges:
      {{- toYaml . | nindent 6 }}
      {{- end -}}
      {{- end -}}
      {{- with .Values.service.externalIPs }}
      externalIPs:
      {{- toYaml . | nindent 6 }}
      {{- end -}}
      {{- if .Values.service.ipFamilyPolicy }}
      ipFamilyPolicy: {{ .Values.service.ipFamilyPolicy }}
      {{- end }}
      {{- with .Values.service.ipFamilies }}
      ipFamilies: 
      {{- toYaml . | nindent 6 }}
      {{- end -}}
{{- end }}

{{- if $udpPorts }}
  - apiVersion: v1
    kind: Service
    metadata:
      name: {{ template "traefik.fullname" . }}-udp
      labels:
        app.kubernetes.io/name: {{ template "traefik.name" . }}
        helm.sh/chart: {{ template "traefik.chart" . }}
        app.kubernetes.io/managed-by: {{ .Release.Service }}
        app.kubernetes.io/instance: {{ .Release.Name }}
      {{- with .Values.service.labels }}
      {{- toYaml . | nindent 8 }}
      {{- end }}
      annotations:
      {{- with (merge .Values.service.annotationsUDP .Values.service.annotations) }}
      {{- toYaml . | nindent 8 }}
      {{- end }}
    spec:
      {{- $type := default "LoadBalancer" .Values.service.type }}
      type: {{ $type }}
      {{- with .Values.service.spec }}
      {{- toYaml . | nindent 6 }}
      {{- end }}
      selector:
        app.kubernetes.io/name: {{ template "traefik.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
      ports:
      {{- range $name, $config := $udpPorts }}
      {{- if $config.expose }}
      - port: {{ default $config.port $config.exposedPort }}
        name: {{ $name }}
        # Due to https://github.com/kubernetes/kubernetes/issues/58477 it is not possible
        # to define the HTTP/3 UDP port as a containerPort. TCP and UDP containerPort will have
        # the same value which will break the chart upgrade.
        targetPort: {{ if $config.http3 }}{{ $config.port }}{{ else }}{{ $name | quote }}{{ end }}
        protocol: UDP
        {{- if $config.nodePort }}
        nodePort: {{ $config.nodePort }}
        {{- end }}
      {{- end }}
      {{- end }}
      {{- if eq $type "LoadBalancer" }}
      {{- with .Values.service.loadBalancerSourceRanges }}
      loadBalancerSourceRanges:
      {{- toYaml . | nindent 6 }}
      {{- end -}}
      {{- end -}}
      {{- with .Values.service.externalIPs }}
      externalIPs:
      {{- toYaml . | nindent 6 }}
      {{- end -}}
      {{- if .Values.service.ipFamilyPolicy }}
      ipFamilyPolicy: {{ .Values.service.ipFamilyPolicy }}
      {{- end }}
      {{- with .Values.service.ipFamilies }}
      ipFamilies: 
      {{- toYaml . | nindent 6 }}
      {{- end -}}
{{- end }}
{{- end -}}

Store the above content in traefik/templates/service.yaml. The changes are enclosed in # .Now we will create a values file named values-custom.yaml:

ingressClass:
  enabled: true

image:
  name: traefik
  tag: "2.6.1"

deployment:
  enabled: true
  kind: Deployment
  replicas: 2
  imagePullSecrets: []

ingressRoute:
  dashboard:
    enabled: false
    
service:
  enabled: true
  type: LoadBalancer
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp"
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: <certificate-arn>
    service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
    

Replace the placeholder <certificate-arn> with the ARN for the certificate.

Now, deploy the traefik using the command:

helm upgrade -i traefik /path/to/traefik -f /path/to/traefik/values-custom.yaml --namespace traefik --create-namespace --atomic

This will deploy the traefik with NLB type Load Balancer. We can go to the listeners section in ELB and verify that the certificate is attached.

Conclusion

Summing up, since NLB terminates the TLS, we won’t be needing traefik to take care of the tls. So, every traffic coming from Load Balancer to traefik pod is plain text and should not be interpreted as https. So we are changing the service template to redirect all traffic to http endpoint of traefik. Following this blog, we can set up traefik ingress which uses ACm to manage certifcates.

Refernces:

knoldus

Written by 

Dipayan Pramanik is a DevOps Software Consultant at Knoldus Inc. He is passionate about coding, DevOps tools, automating tasks and is always ready to take up challenges. His hobbies include music and gaming.