Traefik with ACME in Kubernetes with LetsEncrypt Certificates
One of the very cool things you can do with your Kubernetes cluster is have automated SSL certificates on your services. This makes managing SSL certificates extremely easy compared to manual methods using other means. However, setup in Kubernetes can be a little intimidating. Let’s take a look at how you can deploy Traefik with ACME in Kubernetes so that you can have automated SSL certificates.
Table of contents
What is Traefik?
Traefik is a free and open source solution that provides many different capabilities across Docker and Kubernetes. You can use it as a load balancer and ingress controller for your Kubernetes cluster as well as SSL termination.
You can learn more about Traefik here: Traefik Labs
What is ACME?
ACME stands for (Automated Certificate Management Environment) and it is a protocol used by Let’s Encrypt (and other certificate authorities). It essentially automates the process of issuing certificates, certificate renewal, and revocation.
Traefik can integrate with your Let’s Encrypt configuration via ACME to:
- Have automation to issue certificates for your domains (using the ACME protocol)
- Manage and renew certificates before they expire
- Store certificates securely in a file acme.json
Learn more about the ACME protocol here: https://www.globalsign.com/
Take a look at the overview diagram below showing how it works with domain validation:
Persistent Volume Claim
To make your Traefik certificate store peristent, you will need to make sure you have a persistent volume claim for Traefik in your Kuberentes environment and have a storage class to handle provisioning storage. This can also be automated depending on the storage class you are using. I am using Ceph in my Kubernetes cluster, so using rook-ceph CSI for the cluster for PVC provisioning.
Traefik deployment YAML code
Below is an example of Traefik deployment YAML that you can take and just plugin your API information for your environment (i.e. Cloudflare or another DNS provider) and have the ACME protocol automatically provision your certificates. I have bolded the values you need to change and insert to customize for your environment, if you are using Cloudflare.
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik
namespace: traefik
labels:
app.kubernetes.io/instance: traefik-traefik
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: traefik
annotations:
deployment.kubernetes.io/revision: '2'
kubectl.kubernetes.io/last-applied-configuration: >
{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"labels":{"app.kubernetes.io/instance":"traefik-traefik","app.kubernetes.io/managed-by":"Helm","app.kubernetes.io/name":"traefik"},"name":"traefik","namespace":"traefik"},"spec":{"progressDeadlineSeconds":600,"replicas":1,"revisionHistoryLimit":10,"selector":{"matchLabels":{"app.kubernetes.io/instance":"traefik-traefik","app.kubernetes.io/name":"traefik"}},"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0},"type":"RollingUpdate"},"template":{"metadata":{"annotations":{"prometheus.io/path":"/metrics","prometheus.io/port":"9100","prometheus.io/scrape":"true"},"labels":{"app.kubernetes.io/instance":"traefik-traefik","app.kubernetes.io/name":"traefik"}},"spec":{"containers":[{"args":["--global.checknewversion","--global.sendanonymoususage","--entrypoints.metrics.address=:9100/tcp","--entrypoints.traefik.address=:9000/tcp","--entrypoints.web.address=:8000/tcp","--entrypoints.websecure.address=:8443/tcp","--api.dashboard=true","--ping=true","--metrics.prometheus=true","--metrics.prometheus.entrypoint=metrics","--providers.kubernetescrd","--providers.kubernetesingress","--entrypoints.websecure.http.tls=true","--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json","--certificatesresolvers.letsencrypt.acme.dnschallenge=true","--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare","--certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=10","--certificatesresolvers.letsencrypt.acme.email=[email protected]"],"env":[{"name":"CLOUDFLARE_EMAIL","value":"[email protected]"},{"name":"CLOUDFLARE_DNS_API_TOKEN","value":"<your dns API token>"}],"image":"traefik:v2.9.6","livenessProbe":{"failureThreshold":3,"httpGet":{"path":"/ping","port":9000,"scheme":"HTTP"},"initialDelaySeconds":2,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":2},"name":"traefik","ports":[{"containerPort":9100,"name":"metrics","protocol":"TCP"},{"containerPort":9000,"name":"traefik","protocol":"TCP"},{"containerPort":8000,"name":"web","protocol":"TCP"},{"containerPort":8443,"name":"websecure","protocol":"TCP"}],"readinessProbe":{"failureThreshold":1,"httpGet":{"path":"/ping","port":9000,"scheme":"HTTP"},"initialDelaySeconds":2,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":2},"securityContext":{"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true,"runAsGroup":65532,"runAsNonRoot":true,"runAsUser":65532},"volumeMounts":[{"mountPath":"/data","name":"data"},{"mountPath":"/tmp","name":"tmp"}]}],"dnsPolicy":"ClusterFirst","initContainers":[{"command":["sh","-c","touch
/data/acme.json \u0026\u0026 chmod 600
/data/acme.json"],"image":"busybox:1.31.1","name":"init-acme-permissions","volumeMounts":[{"mountPath":"/data","name":"data"}]}],"restartPolicy":"Always","securityContext":{"fsGroup":65532},"serviceAccountName":"traefik","terminationGracePeriodSeconds":60,"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"<your persistent volume claim>}},{"emptyDir":{},"name":"tmp"}]}}}}
selfLink: /apis/apps/v1/namespaces/traefik/deployments/traefik
status:
observedGeneration: 2
replicas: 1
updatedReplicas: 1
readyReplicas: 1
availableReplicas: 1
conditions:
- type: Available
status: 'True'
lastUpdateTime: '2024-11-28T04:26:04Z'
lastTransitionTime: '2024-11-28T04:26:04Z'
reason: MinimumReplicasAvailable
message: Deployment has minimum availability.
- type: Progressing
status: 'True'
lastUpdateTime: '2024-11-28T04:26:04Z'
lastTransitionTime: '2024-11-28T04:26:04Z'
reason: NewReplicaSetAvailable
message: ReplicaSet "traefik-596bf66544" has successfully progressed.
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: traefik-traefik
app.kubernetes.io/name: traefik
template:
metadata:
creationTimestamp: null
labels:
app.kubernetes.io/instance: traefik-traefik
app.kubernetes.io/name: traefik
annotations:
kubectl.kubernetes.io/restartedAt: '2024-11-28T03:40:31Z'
prometheus.io/path: /metrics
prometheus.io/port: '9100'
prometheus.io/scrape: 'true'
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: <your persistent volume claim>
- name: tmp
emptyDir: {}
initContainers:
- name: init-acme-permissions
image: busybox:1.31.1
command:
- sh
- '-c'
- touch /data/acme.json && chmod 600 /data/acme.json
resources: {}
volumeMounts:
- name: data
mountPath: /data
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
imagePullPolicy: IfNotPresent
containers:
- name: traefik
image: traefik:v2.9.6
args:
- '--global.checknewversion'
- '--global.sendanonymoususage'
- '--entrypoints.metrics.address=:9100/tcp'
- '--entrypoints.traefik.address=:9000/tcp'
- '--entrypoints.web.address=:8000/tcp'
- '--entrypoints.websecure.address=:8443/tcp'
- '--api.dashboard=true'
- '--ping=true'
- '--metrics.prometheus=true'
- '--metrics.prometheus.entrypoint=metrics'
- '--providers.kubernetescrd'
- '--providers.kubernetesingress'
- '--entrypoints.websecure.http.tls=true'
- '--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json'
- '--certificatesresolvers.letsencrypt.acme.dnschallenge=true'
- >-
--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
- >-
--certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=10
- >-
--certificatesresolvers.letsencrypt.acme.email=[email protected]
ports:
- name: metrics
containerPort: 9100
protocol: TCP
- name: traefik
containerPort: 9000
protocol: TCP
- name: web
containerPort: 8000
protocol: TCP
- name: websecure
containerPort: 8443
protocol: TCP
env:
- name: CLOUDFLARE_EMAIL
value: [email protected]
- name: CLOUDFLARE_DNS_API_TOKEN
value: <your dns API token>
resources: {}
volumeMounts:
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
livenessProbe:
httpGet:
path: /ping
port: 9000
scheme: HTTP
initialDelaySeconds: 2
timeoutSeconds: 2
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
readinessProbe:
httpGet:
path: /ping
port: 9000
scheme: HTTP
initialDelaySeconds: 2
timeoutSeconds: 2
periodSeconds: 10
successThreshold: 1
failureThreshold: 1
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
drop:
- ALL
runAsUser: 65532
runAsGroup: 65532
runAsNonRoot: true
readOnlyRootFilesystem: true
restartPolicy: Always
terminationGracePeriodSeconds: 60
dnsPolicy: ClusterFirst
serviceAccountName: traefik
serviceAccount: traefik
securityContext:
fsGroup: 65532
schedulerName: default-scheduler
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
Add an ingress for your service
Now that we have Traefik deployed with the deployment configuration above, we can now create ingresses for services that we want to deploy and have these automatically pull SSL certificates for properly encrypting the web traffic and users not getting certificate errors.
Below, I have bolded the places where you would add your hostname (meaning [email protected]). As you can see below, we are telling it to use letsencrypt from our Traefik deployment and also pointing it to the Kubernetes service on the inside.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-ingress-1
namespace: argocd
labels:
app: argocd
k8slens-edit-resource-version: v1
annotations:
kubectl.kubernetes.io/last-applied-configuration: >
{"apiVersion":"networking.k8s.io/v1","kind":"Ingress","metadata":{"annotations":{"traefik.ingress.kubernetes.io/router.entrypoints":"websecure","traefik.ingress.kubernetes.io/router.tls":"true","traefik.ingress.kubernetes.io/router.tls.certresolver":"letsencrypt","traefik.ingress.kubernetes.io/service.serversscheme":"http"},"labels":{"app":"argocd"},"name":"argocd-ingress-1","namespace":"argocd"},"spec":{"ingressClassName":"traefik","rules":[{"host":"<your hostname that matches domain name for your API token>","http":{"paths":[{"backend":{"service":{"name":"argocd-server","port":{"number":80}}},"path":"/","pathType":"Prefix"}]}}]}}
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: 'true'
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
traefik.ingress.kubernetes.io/service.serversscheme: http
selfLink: /apis/networking.k8s.io/v1/namespaces/argocd/ingresses/argocd-ingress-1
status:
loadBalancer: {}
spec:
ingressClassName: traefik
rules:
- host: <your hostname that matches domain name for your API token>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 80
Wrapping up
Hopefully this will help you get on the right track with automatically issuing Cloudflare certificates to your Kubernetes services using Traefik with ACME which takes all the manual steps and leg work out of the process. Kubernetes with SSL can be intimidating. However, after a few times issuing your own certificates automatically, you will see it isn’t too difficult. You can also just copy the code for the ArgoCD server demonstrated above for other services, just replacing with the appropriate names and services and using this as a template for your ingresses.