How to set up HTTPS ingress for your Kubernetes cluster using Traefik
Estimated time to read: 9 minutes
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.
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?
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 and create a Grafana dashboard. You can learn how to do this in our next Traefik tutorial!
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 setting 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.