Latest update

6/recent/ticker-posts

Traefik with ACME in Kubernetes with LetsEncrypt Certificates

 Traefik with acme

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=youremail@domain.com"],"env":[{"name":"CLOUDFLARE_EMAIL","value":"youremail@domain.com"},{"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=youremail@domain.com
          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: youremail@domain.com
            - 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 host@publicdomainname.com). 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.

Post a Comment

0 Comments