Back to blog
#kubernetes #cert-manager #tls #cloudflare #devops

Wildcard TLS on Kubernetes with cert-manager and Cloudflare DNS-01

When I set up Kubernetes on my VPS for a hobby project (estreetz.com), I wanted a single wildcard certificate to cover *.apps.estreetz.com and apps.estreetz.com. The standard HTTP-01 ACME challenge cannot issue wildcard certificates. Let’s Encrypt requires DNS-01 for wildcards, because proving you control a DNS zone is the only way to prove you control *.domain.com.

Here’s the full setup using cert-manager with Cloudflare as the DNS provider.

Why DNS-01 for Wildcards

With HTTP-01, Let’s Encrypt places a token at http://your-domain/.well-known/acme-challenge/<token> and fetches it. That works for example.com or subdomain.example.com, but there’s no way to serve an HTTP challenge for every possible subdomain under *.example.com.

DNS-01 works differently: cert-manager creates a TXT record (_acme-challenge.your-domain) via the Cloudflare API. Let’s Encrypt looks up that record to confirm domain ownership. No HTTP server needed.

Install cert-manager

Add the Jetstack Helm repo and install:

helm repo add jetstack https://charts.jetstack.io
helm repo update

helm --kubeconfig /etc/rancher/k3s/k3s.yaml install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true

cert-manager v1.16.0 deploys three pods: the main controller, cainjector, and webhook.

Cloudflare API Token

Generate a Cloudflare API token with Zone:DNS:Edit permission scoped to your zone. Then create a Kubernetes secret in the cert-manager namespace:

kubectl create secret generic cloudflare-api-token-secret \
  --from-literal=api-token=<your-token> \
  -n cert-manager

ClusterIssuer

Create a ClusterIssuer that tells cert-manager to use Let’s Encrypt with the DNS-01 solver backed by Cloudflare:

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

Certificate

Request the wildcard certificate:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: estreetz-com-wildcard-tls
  namespace: default
spec:
  secretName: estreetz-com-wildcard-tls
  issuerRef:
    name: letsencrypt-cloudflare
    kind: ClusterIssuer
  dnsNames:
    - "*.apps.estreetz.com"
    - "apps.estreetz.com"

Apply with:

kubectl apply -f cluster-cr.yml

What Happens Next

cert-manager creates a Challenge resource and uses the Cloudflare API token to insert a TXT record. You can watch the progress:

kubectl get challenges
kubectl describe challenge <challenge-name>

The describe output will show something like:

Status:
  Presented: true
  Reason: Waiting for DNS-01 challenge propagation
  State: pending

DNS propagation takes 1-3 minutes for Cloudflare. Once it propagates, Let’s Encrypt validates the record, the challenge transitions to valid, and the certificate is issued.

kubectl get certificate
# NAME                       READY   SECRET                      AGE
# estreetz-com-wildcard-tls  True    estreetz-com-wildcard-tls   3m

READY: True means the TLS secret is in place. Any Ingress or Gateway can reference estreetz-com-wildcard-tls as the TLS secret and serve valid HTTPS for any subdomain under *.apps.estreetz.com.

One Gotcha

If you installed cert-manager with --set installCRDs=true, newer versions of the chart warn that this flag is deprecated in favor of --set crds.enabled=true. It still works, but worth updating when you upgrade cert-manager versions.