Enable HTTPS for Traefik on Bare-Metal Kubernetes with a Wildcard Let’s Encrypt Certificate (cert-manager + Cloudflare DNS-01)

In a previous post,

We have a Kubernetes cluster that exposes HTTP for

  • app1.maksonlee.com
  • app2.maksonlee.com

through Traefik and MetalLB.

In this follow-up, we’ll enable HTTPS using a single wildcard certificate:

  • *.maksonlee.com (subdomains only; the naked maksonlee.com is out of scope for this lab)

Goals

  • Use cert-manager + Let’s Encrypt DNS-01 via Cloudflare.
  • Keep Traefik stateless (no acme.json).
  • Configure the wildcard certificate once for Traefik as a default TLS certificate.
  • Any future app that uses ingressClassName: traefik automatically gets HTTPS with *.maksonlee.com, no per-app TLS config.

Flow:

  • Prerequisites: existing bare-metal HA cluster with Traefik + MetalLB.
  • Install cert-manager with Helm.
  • Store an existing Cloudflare API token as a Kubernetes Secret.
  • Create a staging ClusterIssuer.
  • Request a staging wildcard cert for *.maksonlee.com (smoke test).
  • Create a production ClusterIssuer.
  • Request a production wildcard cert in the traefik namespace.
  • Configure Traefik’s default TLSStore to use the wildcard cert and redirect HTTP→HTTPS.
  • Verify existing apps over HTTPS and summary.

Prerequisites: Existing HA Cluster with Traefik + MetalLB

This post assumes you already have:

  • Kubernetes:
    • 3-node kubeadm cluster, all nodes are control-plane + worker.
    • CNI: Calico (10.244.0.0/16 Pod CIDR).
  • Nodes:
    • k8s-1.maksonlee.com192.168.0.99
    • k8s-2.maksonlee.com192.168.0.100
    • k8s-3.maksonlee.com192.168.0.101
  • Control-plane VIP (kube-vip):
    • k8s.maksonlee.com192.168.0.97:6443
  • MetalLB:
    • L2 mode, pool: 192.168.0.98-192.168.0.98
  • Traefik:
    • Installed via Helm in namespace traefik
    • LoadBalancer Service with EXTERNAL-IP: 192.168.0.98
    • 3 replicas, one pod per node (podAntiAffinity).
  • Example apps:
    • app1.maksonlee.com192.168.0.98
    • app2.maksonlee.com192.168.0.98
    • Ingress uses ingressClassName: traefik, HTTP only.

  1. Install cert-manager with Helm

Install cert-manager from the official Helm chart (example: v1.19.1):

helm install \
  cert-manager oci://quay.io/jetstack/charts/cert-manager \
  --version v1.19.1 \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true \
  --set 'extraArgs={--dns01-recursive-nameservers-only,--dns01-recursive-nameservers=8.8.8.8:53\,1.1.1.1:53}'

Notes:

  • --create-namespace creates the cert-manager namespace automatically.
  • crds.enabled=true installs cert-manager CRDs.
  • extraArgs forces DNS-01 checks to use public resolvers (8.8.8.8 and 1.1.1.1), which helps with split-horizon / internal DNS setups.

Verify:

kubectl get pods -n cert-manager

You should see:

  • cert-manager-xxxxx
  • cert-manager-cainjector-xxxxx
  • cert-manager-webhook-xxxxx

all Running and READY=1/1.


  1. Store the Cloudflare API Token in Kubernetes

Assume you already created a Cloudflare API token for maksonlee.com that:

  • Has permission to edit DNS records in that zone.

Store this token as a Secret in the cert-manager namespace:

kubectl create secret generic cloudflare-api-token-secret \
  --namespace cert-manager \
  --from-literal=api-token='<YOUR_CLOUDFLARE_API_TOKEN>'

Replace <YOUR_CLOUDFLARE_API_TOKEN> with your actual token string.

Verify:

kubectl get secret cloudflare-api-token-secret -n cert-manager

Now cert-manager’s ACME solver can access Cloudflare to create _acme-challenge.* TXT records.


  1. Create the Staging ClusterIssuer

clusterissuer-staging.yaml:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: admin@maksonlee.com
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token-secret
              key: api-token

Apply:

kubectl apply -f clusterissuer-staging.yaml

Check it:

kubectl describe clusterissuer letsencrypt-staging

Once cert-manager finishes registering the ACME account, you should see:

Status:
  Acme:
    Last Registered Email:  admin@maksonlee.com
  Conditions:
    Type:    Ready
    Status:  True
    Reason:  ACMEAccountRegistered
    Message: The ACME account was registered with the ACME server

Temporary rate-limit warnings from the staging endpoint are normal; cert-manager retries and eventually marks the issuer Ready=True.


  1. Staging Wildcard Certificate Smoke Test (*.maksonlee.com)

Request a staging wildcard certificate in the traefik namespace as an immediate smoke test.

wildcard-staging-cert.yaml:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-maksonlee-com-staging
  namespace: traefik
spec:
  secretName: wildcard-maksonlee-com-staging-tls
  dnsNames:
    - "*.maksonlee.com"
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer

Apply:

kubectl apply -f wildcard-staging-cert.yaml

You may see:

Warning: spec.privateKey.rotationPolicy: In cert-manager >= v1.18.0, the default value changed from `Never` to `Always`.

This is informational; default Always is fine.

Verify:

kubectl get certificate -n traefik
kubectl describe certificate wildcard-maksonlee-com-staging -n traefik

You want:

  • Type: Ready
  • Status: True
  • Message: Certificate is up to date and has not expired

and the Secret:

kubectl get secret wildcard-maksonlee-com-staging-tls -n traefik

Checking challenges:

kubectl get challenges.acme.cert-manager.io -A

It’s normal to see No resources found once issuance has completed; cert-manager cleans them up.

This confirms your DNS-01 solver, Cloudflare token, and wildcard name are all working in staging before moving to production.


  1. Production ClusterIssuer

clusterissuer-prod.yaml:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: admin@maksonlee.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token-secret
              key: api-token

Apply:

kubectl apply -f clusterissuer-prod.yaml

  1. Production Wildcard Certificate (in traefik)

wildcard-prod-cert.yaml:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-maksonlee-com
  namespace: traefik
spec:
  secretName: wildcard-maksonlee-com-tls
  dnsNames:
    - "*.maksonlee.com"
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer

Apply:

kubectl apply -f wildcard-prod-cert.yaml

kubectl get certificate -n traefik
kubectl describe certificate wildcard-maksonlee-com -n traefik

Wait until:

  • Type: Ready
  • Status: True
  • Message: Certificate is up to date and has not expired

Then confirm the Secret:

kubectl get secret wildcard-maksonlee-com-tls -n traefik

Now you have a real Let’s Encrypt wildcard cert stored in:

  • traefik namespace
  • Secret wildcard-maksonlee-com-tls

Next step: teach Traefik to use this as the default TLS certificate.


  1. Configure Traefik: HTTP→HTTPS and Default Wildcard Cert

We want:

  • Traefik Service to stay as LoadBalancer with EXTERNAL-IP: 192.168.0.98.
  • HTTP on web redirected to HTTPS on websecure.
  • Any HTTPS route without an explicit Secret to use the wildcard certificate from wildcard-maksonlee-com-tls.

Update traefik-values.yaml:

service:
  enabled: true
  type: LoadBalancer
  spec:
    loadBalancerIP: 192.168.0.98

providers:
  kubernetesIngress:
    enabled: true
    ingressClass: traefik

ingressClass:
  enabled: true
  isDefaultClass: true

# HTTP → HTTPS redirect only (keep default 8000/8443 inside the pod)
additionalArguments:
  - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
  - "--entrypoints.web.http.redirections.entrypoint.scheme=https"

deployment:
  replicas: 3

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: traefik
          topologyKey: kubernetes.io/hostname

# Default TLS store using the wildcard Secret in the traefik namespace
tlsStore:
  default:
    defaultCertificate:
      secretName: wildcard-maksonlee-com-tls

Key points:

  • We don’t override the internal ports; the chart continues to expose:
    • web on 8000/tcp
    • websecure on 8443/tcp
      mapped to Service ports 80 and 443 respectively.
  • additionalArguments enables a global HTTP→HTTPS redirect.
  • tlsStore.default.defaultCertificate.secretName tells Traefik to use wildcard-maksonlee-com-tls whenever a router on websecure doesn’t specify its own TLS secret.

Apply with Helm:

helm upgrade traefik traefik/traefik \
  -n traefik \
  -f traefik-values.yaml

kubectl rollout status deploy/traefik -n traefik
kubectl get pods -n traefik -o wide
kubectl get svc traefik -n traefik

If you had old ReplicaSets with required anti-affinity and want to ensure a clean restart:

kubectl scale deploy traefik -n traefik --replicas=0
kubectl scale deploy traefik -n traefik --replicas=3

After rollout:

  • EXTERNAL-IP on traefik Service remains 192.168.0.98.
  • All HTTP requests are redirected to HTTPS.
  • TLS is terminated with the wildcard *.maksonlee.com certificate by default.

  1. Verify Existing Apps Over HTTPS

From the previous cluster post you already have:

apps-ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: apps-ingress
  namespace: apps
spec:
  ingressClassName: traefik
  rules:
  - host: app1.maksonlee.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app1-svc
            port:
              number: 80
  - host: app2.maksonlee.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app2-svc
            port:
              number: 80

There’s no tls: block — intentionally.

Because:

  • Traefik is the default ingress class.
  • The default TLS store uses wildcard-maksonlee-com-tls.
  • HTTP is redirected to HTTPS.

Your existing apps-ingress now automatically benefits from the wildcard certificate.

From a client that resolves app1.maksonlee.com and app2.maksonlee.com to 192.168.0.98:

curl -v http://app1.maksonlee.com
curl -v https://app1.maksonlee.com

curl -v http://app2.maksonlee.com
curl -v https://app2.maksonlee.com

You should see:

  • http://…301 redirect to https://…
  • https://… → successful TLS handshake using a certificate whose SAN includes *.maksonlee.com

In a browser, open:

  • https://app1.maksonlee.com
  • https://app2.maksonlee.com

and verify:

  • Issuer: Let’s Encrypt
  • Subject/SAN: *.maksonlee.com

Any new app that:

  • Uses ingressClassName: traefik, and
  • Has a host under *.maksonlee.com

will automatically be served over HTTPS with this wildcard certificate, without adding:

  • A tls: section to its Ingress, or
  • A per-namespace Certificate resource.

  1. Summary

On your 3-node bare-metal HA cluster, you now have:

  • Traefik exposed via MetalLB at 192.168.0.98 on ports 80/443.
  • cert-manager managing Let’s Encrypt staging + production issuers.
  • A production wildcard *.maksonlee.com certificate stored in traefik/wildcard-maksonlee-com-tls.
  • Traefik configured to:
    • Redirect HTTP→HTTPS globally.
    • Use the wildcard certificate as the default TLS certificate.

Result:

  • https://app1.maksonlee.com
  • https://app2.maksonlee.com
  • and any future https://*.maksonlee.com fronted by Traefik

are automatically secured by the same Let’s Encrypt wildcard certificate, with cert-manager handling renewals and DNS-01 challenges via Cloudflare in the background.

Did this guide save you time?

Support this site

Leave a Comment

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

Scroll to Top