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.