Kubernetes

Traefik with ACME in Kubernetes with LetsEncrypt Certificates

Learn how to deploy Traefik with ACME in Kubernetes for automated SSL certificates to simplify SSL setup with LetsEncrypt and Cloudflare

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.

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

Traefik
Traefik

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:

  1. Have automation to issue certificates for your domains (using the ACME protocol)
  2. Manage and renew certificates before they expire
  3. 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:

Overview of how the acme protocol works with domain validation
Overview of how the acme protocol 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.

Subscribe to VirtualizationHowto via Email ๐Ÿ””

Enter your email address to subscribe to this blog and receive notifications of new posts by email.



Brandon Lee

Brandon Lee is the Senior Writer, Engineer and owner at Virtualizationhowto.com, and a 7-time VMware vExpert, with over two decades of experience in Information Technology. Having worked for numerous Fortune 500 companies as well as in various industries, He has extensive experience in various IT segments and is a strong advocate for open source technologies. Brandon holds many industry certifications, loves the outdoors and spending time with family. Also, he goes through the effort of testing and troubleshooting issues, so you don't have to.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.