Published on

How to use Cert-Manager DNS Challenge with Cloudflare on Kubernetes with Helm


In this tutorial, we will be issuing Let's Encrypt certificates using cert-manager on Kubernetes and we will be using the DNS Challenge with Cloudflare.

The reason I am using DNS Challenge instead of HTTP Challenge is because the Kubernetes environment is local on my laptop and there isn't a direct HTTP route into my environment from the internet and I would like to not expose the endpoints to the public internet.

Summary of what we will be doing

We would like to have Let's Encrypt Certificates on our web application that will be issued by Cert-Manager using the DNS Challenge from CloudFlare.

Our ingress controller will be ingress-nginx and our endpoints will be private, as they will resolve to private IP addresses, hence the reason why we are using DNS validation instead of HTTP.


To follow along in this tutorial you will need the following

Install a Kubernetes Cluster

If you already have a Kubernetes Cluster, you can skip this step.

Define the kind-config.yaml

kind: Cluster
- role: control-plane
  image: kindest/node:v1.26.6@sha256:6e2d8b28a5b601defe327b98bd1c2d1930b49e5d8c512e1895099e4504007adb
  - containerPort: 80
    hostPort: 80
    protocol: TCP
    listenAddress: ""
  - containerPort: 443
    hostPort: 443
    protocol: TCP

Then create the cluster with kind:

kind create cluster --name example --config kind-config.yaml

Nginx Ingress Controller

First we need to install a ingress controller and I am opting in to use ingress-nginx, so first we need to add the helm repository to our local repositories:

helm repo add ingress-nginx

Then we need to update our repositories:

helm repo update

Then we can install the helm release:

helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.kind=DaemonSet \
  --set controller.hostPort.enabled=true \
  --set controller.ingressClass=nginx

You can view all the default values from their GitHub repository where the chart is hosted:

Once the release has been deployed, you should see the ingress-nginx pod running under the ingress-nginx namespace:

kubectl get pods -n ingress-nginx


The next step is to install cert-manager using helm, first add the repository:

helm repo add jetstack

Update the repositories:

helm repo update

Then install the cert-manager release:

helm upgrade --install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.13.1 \
  --set installCRDs=true

Cloudflare API Token

We need to grant Cert-Manager access to make DNS changes on our Cloudflare account for DNS validation on our behalf, and in order to do that, we need to create a Cloudflare API Token.

As per the cert-manager documentation, from your profile select API Tokens, create an API Token and select Edit Zone DNS template.

Then select the following:

  • Permissions:

    • Zone: DNS -> Edit

    • Zone: Zone -> Read

  • Zone Resources:

    • Include -> All Zones

![]( align="left")

Then create the token and save the value somewhere safe, as we will be using it in the next step.

Cert-Manager ClusterIssuer

First, we need to create a Kubernetes secret with the API Token that we created in the previous step.

kubectl create secret generic cloudflare-api-key-secret \

Then create the clusterissuer.yaml

kind: ClusterIssuer
  name: letsencrypt-dns01-issuer
    email:  # your email address for updates
      name: letsencrypt-dns01-private-key
    - dns01:
          email: # your cloudflare account email address
            name: cloudflare-api-key-secret
            key: api-key

Then create the cluster issuer:

kubectl apply -f clusterissuer.yaml

Request a Certificate

Now that we have our ClusterIssuer created, we can request a certificate. In my scenario, I have a domain which is hosted on CloudFlare and I would like to create a wildcard certificate on the sub-domain *

Certificates are scoped on a namespace level, and ClusterIssuer's are cluster-wide, therefore I am prefixing my certificate with the namespace (just my personal preference).

kind: Certificate
  name: default-workshop-certificate
  namespace: default
  secretName: default-workshop-example-tls
    name: letsencrypt-dns01-issuer
    kind: ClusterIssuer
  - '*'

Before we create the certificate on CloudFlare, I have created private DNS to the names mentioned in the manifest above like the following:

- -> A Record ->
- * -> CNAME ->

In the DNS configuration mentioned above, to explain why I am creating 2 entries:

  • - This is my LoadBalancer IP Address

  • I have a static DNS entry to the name so if my LoadBalancer IP Address ever change, I can just change this address

  • I am creating a wildcard DNS entry for * and I am creating a CNAME record for it to resolve to so it will essentially respond to the LoadBalancer IP.

  • So lets say I create and then it will resolve to the LoadBalancer IP in and as mentioned before, if the LoadBalancer IP ever changes, I only have to update the A Record of

Then after DNS was created, I went ahead and created the certificate:

kubectl apply -f certificate.yaml

You can view the progress by viewing the certificate status by running:

kubectl get certificate -n default

Specify the Certificate in your Ingress

Let's deploy a nginx web server deployment and I have concatenated the following in one manifest called deployment.yaml:

  • Deployment

  • Service

  • Ingress

apiVersion: apps/v1
kind: Deployment
  name: nginx-web
  namespace: default
    app: nginx-web
  replicas: 2
      app: nginx-web
        app: nginx-web
      - name: nginx
        image: nginx:1.19
        - containerPort: 80
apiVersion: v1
kind: Service
  name: nginx-web-service
  namespace: default
    app: nginx-web
  type: ClusterIP
  - port: 80
    targetPort: 80
    app: nginx-web
kind: Ingress
  name: nginx-web-ingress
  namespace: default
  annotations: "nginx"
  - host:
      - path: /
        pathType: Prefix
            name: nginx-web-service
              number: 80
  - hosts:
    secretName: default-workshop-example-tls

A few important things to notice on the ingress resource:

  • host the host needs to match the certificate

  • secretName the secret needs to match the secret defined in the certificate

Then create the deployment:

kubectl apply -f deployment.yaml

Ensure DNS Challenges are successful

Ensure that cert-manager can set DNS-01 challenge records correctly, if you encounter issues, you can inspect the cert-manager pod logs.

To view the pods for cert-manager:

kubectl get pods -n cert-manager

Then view the logs using:

kubectl logs -f pod <pod-id> -n cert-manager


You can open up a browser and access the ingress on your browser, in my case it would be and verify that you have a certificate issued from Lets Encrypt.

Thank You

Thanks for reading, if you enjoy my content please feel free to follow me on Twitter - @@ruanbekker or visit me on my website -

Buy Me A Coffee