This post builds on my previous guide:
Self-Managed Kubernetes Cluster on AWS (with Traefik & MetalLB)
If you haven’t set up your Kubernetes cluster, MetalLB, and Traefik yet, start there first!
In this follow-up, we’ll extend that setup by enabling automatic HTTPS using Let’s Encrypt with the DNS-01 challenge in Traefik. This method is ideal for:
- Environments behind firewalls or NAT (no need to expose port 80/443)
- Wildcard certificates like *.maksonlee.com
- Clusters managed in self-hosted infrastructure
This guide uses Cloudflare as the DNS provider.
Prerequisites
- You’ve deployed Traefik via Helm
- MetalLB is exposing Traefik with a static IP (e.g., 10.0.128.240)
- Your domain (e.g., maksonlee.com) is managed by Cloudflare
- You created AorCNAMErecords in Cloudflare for your app (e.g.,whoami.maksonlee.com)
- Those DNS records must be DNS-only (Cloudflare proxy disabled)
- Create a Cloudflare API Token
- Go to https://dash.cloudflare.com/profile/api-tokens
- Click “Create Token”
- Choose “Edit zone DNS” template
- Scope the token to your zone (e.g., maksonlee.com)
- Copy the token — you’ll use it in the next step
- Store the API Token as a Kubernetes Secret
Create a secret in the same namespace as Traefik (e.g., kube-system):
kubectl create secret generic cloudflare-api-secret --namespace kube-system --from-literal=CLOUDFLARE_DNS_API_TOKEN=your-cloudflare-token
Replace your-cloudflare-token with your real token string.
- Create a Persistent Volume for acme.json
Let’s Encrypt needs to persist certificate data in acme.json.
Create a PVC for that:
# acme-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: acme-pvc
  namespace: kube-system
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ebs-sc
  resources:
    requests:
      storage: 1Gi
Apply it:
kubectl apply -f acme-pvc.yaml
- Update traefik-values.yamlfor Cloudflare DNS-01
Edit the values used to deploy Traefik via Helm:
service:
  type: LoadBalancer
  loadBalancerIP: 10.0.128.240
additionalArguments:
  - "--ping=true"
  - "--ping.entrypoint=traefik"
  - "--api.dashboard=true"
  - "--certificatesresolvers.dns.acme.dnschallenge=true"
  - "--certificatesresolvers.dns.acme.dnschallenge.provider=cloudflare"
  - "--certificatesresolvers.dns.acme.email=your-email@example.com"
  - "--certificatesresolvers.dns.acme.storage=/data/acme.json"
  - "--certificatesresolvers.dns.acme.dnschallenge.resolvers=1.1.1.1:53"
envFrom:
  - secretRef:
      name: cloudflare-api-secret
ingressClass:
  enabled: true
  isDefaultClass: true
  name: traefik
deployment:
  additionalVolumes:
    - name: acme-storage
      persistentVolumeClaim:
        claimName: acme-pvc
  additionalVolumeMounts:
    - name: acme-storage
      mountPath: /data
nodeSelector:
  kubernetes.io/hostname: ip-10-0-128-6
tolerations:
  - key: "node-role.kubernetes.io/control-plane"
    operator: "Exists"
    effect: "NoSchedule"
Be sure to:
- Replace your-email@example.comwith your real email.
- Replace ip-10-0-128-6with your control-plane node hostname.
- Upgrade Traefik with New Settings
Apply the updated Helm values:
helm upgrade traefik traefik/traefik --namespace kube-system -f traefik-values.yaml
- Add or Update Your Ingress for TLS
Update the Ingress to request a certificate via Let’s Encrypt DNS-01:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    traefik.ingress.kubernetes.io/router.tls.certresolver: dns
spec:
  rules:
    - host: whoami.maksonlee.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami
                port:
                  number: 80
Apply it:
kubectl apply -f whoami.yaml
- Confirm Certificate Issuance
Check Traefik logs:
kubectl logs -n kube-system deploy/traefik
Traefik’s log should include:
Register... providerName=dns.acme
Testing certificate renew... acmeCA=https://acme-v02.api.letsencrypt.org/directory providerName=dns.acme
These lines confirm:
- ACME provider is registered (dns.acme)
- Traefik is checking and renewing certs properly
