How to setup HTTPS ingress for your Kubernetes cluster using Traefik

In this tutorial, we explain step by step how to set up Traefik 2 and Let’s Encrypt in a Kubernetes v1.18 cluster and use it to secure your service with HTTPS.

When you’ve got an HTTP service and you want to publish it to the internet, it’s probably a good idea to make sure it’s reachable via HTTPS. This is where Traefik comes in; one of its many features is TLS termination and it turns out it’s really easy to set up!

Before you follow this tutorial, make sure you’re able to assign a domain name to the Traefik instance. Otherwise the Let’s Encrypt challenge will not work.

Step 1: set up a Kubernetes cluster

You probably already have a kubernetes cluster running, but if you don’t: it’s really easy to create one in the Fuga dashboard.

Just go to the “Compute” tab and click on “Kubernetes”, this will bring you to a screen where you can create and configure your new cluster. Note that we will use Kubernetes v1.18 for this tutorial. Also, we’ll enable floating IPs for all nodes so we can easily access them.

Once your k8s cluster is ready, go to the cluster info where you can download the Kubeconfig file. This file contains the configuration for kubectl which we will use to set up our cluster. The default location for this file is “~/.kube/config”, so you might want to place it there. Otherwise, use the “--kubeconfig” parameter to specify the location.

Make sure everything is working by fetching the pods in the “kube-system” namespace.

$ kubectl get pods -n kube-system
NAME                                       READY   STATUS    RESTARTS   AGE
calico-kube-controllers-795c4545c7-7lh4h   1/1     Running   0          3m5s
calico-node-2qh9j                          1/1     Running   0          3m5s
calico-node-h8tsf                          1/1     Running   0          64s
calico-node-psw7r                          1/1     Running   0          60s
coredns-786ffb7797-g8f5n                   1/1     Running   0          3m6s
coredns-786ffb7797-lqbdx                   1/1     Running   0          3m6s
csi-cinder-controllerplugin-0              5/5     Running   1          3m1s
csi-cinder-nodeplugin-r2h9l                2/2     Running   0          40s
csi-cinder-nodeplugin-v6qtx                2/2     Running   0          43s
k8s-keystone-auth-jcnkq                    1/1     Running   0          3m3s
kube-dns-autoscaler-75859754fd-2h9sh       1/1     Running   0          3m6s
magnum-metrics-server-79556d6999-nzpvz     0/1     Running   0          2m54s
npd-qb8nm                                  1/1     Running   0          40s
npd-xk5hn                                  1/1     Running   0          44s
octavia-ingress-controller-0               1/1     Running   0          3m4s
openstack-cloud-controller-manager-dsbxw   1/1     Running   0          3m7s

Step 2: create a HTTP service

We will create a dummy service which Traefik will be proxying: a simple nginx container which serves a “Hello, world!” website.

We start out by creating a new namespace in which we will deploy our service.

$ kubectl create namespace traefik-test
namespace/traefik-test created

The “Hello, world!” page is saved as a configMap which will be mounted in the nginx container.

$ echo "Hello, world!" > index.html
$ kubectl -n traefik-test create configmap index.html --from-file index.html
configmap/index.html created

The nginx container is defined in a deployment configuration. We also define a service to allow connections to this deployment. Create a file called nginx.yaml with the following contents:

---
apiVersion: v1
kind: Service
metadata:
 name: nginx

spec:
 ports:
 - port: 80
 selector:
   app: nginx
 type: NodePort

---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: nginx-deployment

spec:
 selector:
   matchLabels:
     app: nginx
 replicas: 1
 template:
   metadata:
     labels:
       app: nginx
   spec:
     containers:
     - name: nginx
       image: nginx:1.14.2
       ports:
       - containerPort: 80
       volumeMounts:
       - name: htmlcontent
         mountPath: "/usr/share/nginx/html/"
         readOnly: true
     volumes:
     - name: htmlcontent
       configMap:
         name: index.html
         items:
         - key: index.html
            path: index.html

Now that we’ve created the service and deployment configuration, let’s deploy them using kubectl.

$ kubectl -n traefik-test apply -f nginx.yaml
service/nginx created
deployment.apps/nginx-deployment created

You should be able to see the new service in the “traefik-test” namespace.

$ kubectl get services -n traefik-test
NAME TYPE   CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
nginx   NodePort   10.x.x.x   <none>    80:31176/TCP   53m

We can quickly test the web server by temporarily adding a port-forward to our local machine and visiting http://0.0.0.0:8000.

$ kubectl -n traefik-test port-forward --address 0.0.0.0 service/nginx 8000:80
Forwarding from 0.0.0.0:8000 -> 80

Step 3: set up Traefik

In order to use Traefik in our Kubernetes cluster, we first need to create some custom resources and roles with permissions. For more information, please visit the Traefik v2 docs.

Create a traefik-crs.yaml file for the custom resource definitions:

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ingressroutes.traefik.containo.us

spec:
 group: traefik.containo.us
 version: v1alpha1
 names:
   kind: IngressRoute
   plural: ingressroutes
   singular: ingressroute
 scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ingressroutetcps.traefik.containo.us

spec:
 group: traefik.containo.us
 version: v1alpha1
 names:
   kind: IngressRouteTCP
   plural: ingressroutetcps
   singular: ingressroutetcp
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: middlewares.traefik.containo.us

spec:
 group: traefik.containo.us
 version: v1alpha1
 names:
   kind: Middleware
   plural: middlewares
   singular: middleware
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: tlsoptions.traefik.containo.us

spec:
 group: traefik.containo.us
 version: v1alpha1
 names:
   kind: TLSOption
   plural: tlsoptions
   singular: tlsoption
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
 name: traefikservices.traefik.containo.us

spec:
 group: traefik.containo.us
 version: v1alpha1
 names:
   kind: TraefikService
   plural: traefikservices
   singular: traefikservice
  scope: Namespaced

Create another file called traefik-rbac.yaml for the RBAC-related configuration:

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: traefik-ingress-controller

rules:
 - apiGroups:
     - ""
   resources:
     - services
     - endpoints
     - secrets
   verbs:
     - get
     - list
     - watch
 - apiGroups:
     - extensions
   resources:
     - ingresses
   verbs:
     - get
     - list
     - watch
 - apiGroups:
     - extensions
   resources:
     - ingresses/status
   verbs:
     - update
 - apiGroups:
     - traefik.containo.us
   resources:
     - middlewares
   verbs:
     - get
     - list
     - watch
 - apiGroups:
     - traefik.containo.us
   resources:
     - ingressroutes
   verbs:
     - get
     - list
     - watch
 - apiGroups:
     - traefik.containo.us
   resources:
     - ingressroutetcps
   verbs:
     - get
     - list
     - watch
 - apiGroups:
     - traefik.containo.us
   resources:
     - tlsoptions
   verbs:
     - get
     - list
     - watch
 - apiGroups:
     - traefik.containo.us
   resources:
     - traefikservices
   verbs:
     - get
     - list
      - watch

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: traefik-ingress-controller

roleRef:
 apiGroup: rbac.authorization.k8s.io
 kind: ClusterRole
 name: traefik-ingress-controller
subjects:
 - kind: ServiceAccount
   name: traefik-ingress-controller
    namespace: default

We’re ready to deploy these resources using kubectl.

$ kubectl apply -f traefik-crs.yaml
customresourcedefinition.apiextensions.k8s.io/ingressroutes.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/ingressroutetcps.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/middlewares.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/tlsoptions.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/traefikservices.traefik.containo.us created
$ kubectl apply -f traefik-rbac.yaml
clusterrole.rbac.authorization.k8s.io/traefik-ingress-controller created
clusterrolebinding.rbac.authorization.k8s.io/traefik-ingress-controller created

Let’s verify the resources exist using “kubectl get crd”. Note that “kubectl describe crd <crd-name>” gives you more information about the custom resource.

$ kubectl get crd | grep traefik
ingressroutes.traefik.containo.us            2021-07-05T13:22:14Z
ingressroutetcps.traefik.containo.us         2021-07-05T13:22:14Z
middlewares.traefik.containo.us              2021-07-05T13:22:14Z
tlsoptions.traefik.containo.us               2021-07-05T13:22:14Z
traefikservices.traefik.containo.us          2021-07-05T13:22:14Z

With these custom resources and roles set up, we can create the actual Traefik service. Just like our dummy web server, the Traefik instance is divided into a deployment containing the pod and a service to allow connections. However, this time we want the service to be open to the outside world, so we choose the LoadBalancer type.

Create a new configuration file called traefik.yaml containing the following content.

---
apiVersion: v1
kind: Service
metadata:
  name: traefik

spec:
 ports:
   - protocol: TCP
     name: web
     port: 80
   - protocol: TCP
     name: admin
     port: 8080
   - protocol: TCP
     name: websecure
     port: 443
 selector:
   app: traefik
  type: LoadBalancer

---
apiVersion: v1
kind: ServiceAccount
metadata:
 namespace: default
  name: traefik-ingress-controller

---
kind: Deployment
apiVersion: apps/v1
metadata:
 namespace: default
 name: traefik
 labels:
    app: traefik

spec:
 replicas: 1
 selector:
   matchLabels:
     app: traefik
 template:
   metadata:
     labels:
       app: traefik
   spec:
     serviceAccountName: traefik-ingress-controller
     containers:
       - name: traefik
         image: traefik:v2.1
         args:
           - --api.insecure
           - --accesslog
           - --entrypoints.web.Address=:80
           - --entrypoints.websecure.Address=:443
           - --providers.kubernetescrd
           - --certificatesresolvers.myresolver.acme.tlschallenge
           - --certificatesresolvers.myresolver.acme.email=foo@you.com
           - --certificatesresolvers.myresolver.acme.storage=acme.json
           # Please note that this is the staging Let's Encrypt server.
           # Once you get things working, you should remove that whole line altogether.
           - --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
         ports:
           - name: web
             containerPort: 80
           - name: websecure
             containerPort: 443
           - name: admin
              containerPort: 8080

Note that this file also contains configuration for the Let’s Encrypt challenge and certificate. Make sure you change the value for “--certificatesresolvers.myresolver.acme.email” and you’re ready to deploy Traefik.

$ kubectl apply -f traefik.yaml
service/traefik created
serviceaccount/traefik-ingress-controller created
deployment.apps/traefik created

It might take a minute for the service to get an external IP, but you should be able to see the service running.

$ kubectl get svc
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                                     AGE
kubernetes   ClusterIP      x.x.x.x          <none>        443/TCP                                     73m
traefik      LoadBalancer   x.x.x.x          x.x.x.x       80:30695/TCP,8080:31233/TCP,443:31692/TCP   2m18s

Congratulations, you’ve successfully set up Traefik! The dashboard is reachable on port 8080 of the external IP.

Step 4: create a DNS record

In order for Traefik to create a valid HTTPS certificate using Let’s Encrypt, a domain name needs to be set up for the external IP. Please create an A record for your domain to the external IP.

Step 5: set up a route

Traefik routes are defined in your kubernetes cluster as IngressRoute resources. Let’s create one for our nginx service.

Create a file called nginx-ingressroute.yaml with the following contents:

---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
 name: nginx-ingress
spec:
 entryPoints:
   - websecure
 routes:
 - match: Host(`your.domain.com`)
   kind: Rule
   services:
   - name: nginx
     port: 80
 tls:
    certResolver: myresolver

Replace “your.domain.com” with your own domain and deploy the routes in the “traefik-test” namespace.

$ kubectl apply -n traefik-test -f nginx-ingressroute.yaml
ingressroute.traefik.containo.us/nginx-ingress created

Visiting your domain via HTTPS should give you the Hello World page! However, it may take some time before Traefik has fetched and is using the new certificate. Also, some browsers might not even allow you to visit the page as staging Let’s Encrypt does not provide you with a valid certificate, so you could continue with the next step without verifying this step.

Step 6: change Let’s Encrypt to production

Our “Hello, world!” website is currently using a staging certificate, causing web browsers like Chrome and Firefox to distrust the website. Removing or commenting out the reference to “acme-staging-v02.api.letsencrypt.org” in traefik.yaml should allow us to get a trusted certificate.

-            - --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
+            #- --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory

Now re-apply the Traefik deployment.

$ kubectl apply -f traefik.yaml
service/traefik unchanged
serviceaccount/traefik-ingress-controller unchanged
deployment.apps/traefik configured

You might have to clear your browser cache, but visiting your domain via HTTPS should serve the “Hello, world!” page with a valid certificate!

What’s next?

Traefik currently supports all TLS versions by default, but it’s a good idea to set the minimum TLS version to 1.2 knowing that TLS 1.0 and 1.1 are considered insecure.

Adding extra routes is as easy as deploying a simple IngressRoute resource. E.g. you could use the Traefik instance as a combined ingress for your frontend website and backend API. It’s also recommended to set up a route for the Traefik dashboard itself; just add an IngressRoute resource, remove the admin port from the LoadBalancer Service and add a NodePort Service for Traefik, just like we did for our dummy backend.

Visiting the Traefik documentation shows that the list of features goes way beyond TLS termination. For example, you might want to enable metrics with Prometheus or add tracing with Elastic to visualize request flows.