DNS01 Challenge Provider for Let’s Encrypt Issuer using Google CloudDNS

- 6 mins

Introduction

This article explains how to set up a ClusterIssuer to use Google CloudDNS to solve DNS01 ACME challenge. It assumes that your cluster is hosted on Google Cloud Platform (GCP) and that you already have a domain set up with CloudDNS. It also assumes that you have cert-manager installed on your cluster.

This was written based on GKE v1.17.17-gke.3000 and cert-manager v1.20.

Create a Service Account

Create a service account with dns.admin role. This is required for cert-manager to be able to add records to CloudDNS in order to solve the DNS01 challenge.

Use Service Account

To use this service account, you can either create a service account secret, or use GKE workload identity.

Create a Service Account secret

To access the service account you created in the previous step, cert-manager uses a key stored in a Kubernetes Secret. First, create a key for the service account and download it as a JSON file, then create a Secret from this file.

$ gcloud iam service-accounts keys create service-account.json \
  --iam-account dns01-solver@$PROJECT_ID.iam.gserviceaccount.com

$ kubectl create secret generic clouddns-dns01-solver-svc-acct \
  --from-file=service-account.json

Create ClusterIssuer (using Service Account secret setup)

Here is a sample manifest

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: you@youremail.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
    - dns01:
        cloudDNS:
          # The ID of the GCP project
          project: $PROJECT_ID
          # Secret key reference to the service account created above
          serviceAccountSecretRef:
            name: clouddns-dns01-solver-svc-acct
            key: service-account.json

GKE Workload Identity

If your GKE cluster already has workload identity enabled, you can leverage workload identity to avoid creating and managing static service account credentials. Follow these steps to do this:

Create ClusterIssuer (using Workload Identity setup)

Here is a sample manifest

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: you@youremail.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
    - dns01:
        cloudDNS:
          # The ID of the GCP project
          project: $PROJECT_ID

Note that the issuer does not include a serviceAccountSecretRef property. Excluding this instructs cert-manager to use the default credentials provided by GKE workload identity.

Verify

View the clusterissuer object:

$ kubectl get clusterissuer
NAME               READY   AGE
letsencrypt-prod   True    9s

$ kubectl describe clusterissuer
Name:         letsencrypt-prod
...
Status:
  Acme:
    Last Registered Email:  you@youremail.com
    Uri:                    https://acme-v02.api.letsencrypt.org/acme/acct/12345678
  Conditions:
    Last Transition Time:  2020-03-18T15:05:26Z
    Message:               The ACME account was registered with the ACME server
    Reason:                ACMEAccountRegistered
    Status:                True
    Type:                  Ready
Events:                    <none>

Create test certificate

After successful creation of the ClusterIssuer, you can create a test Certificate to verify that everything works. Here is a sample manifest:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
  namespace: default
spec:
  secretName: example-com-tls
  issuerRef:
    # The issuer created previously
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - example.com

It may take a few minutes for the certificate to be issued. While you are waiting, you can view the Certificate object and the associated resources being created:

$ kubectl get certificate
NAME          READY   SECRET            AGE
example-com   False   example-com-tls   65s
$ kubectl describe certificate
Name:         example-com
...
Status:
  Conditions:
    Last Transition Time:  2020-03-18T15:19:42Z
    Message:               Waiting for CertificateRequest "example-com-123456787" to complete
    Reason:                InProgress
    Status:                False
    Type:                  Ready
Events:
  Type    Reason     Age   From          Message
  ----    ------     ----  ----          -------
  Normal  Requested  71s   cert-manager  Created new CertificateRequest resource "example-com-123456787"
$ kubectl get certificaterequest
NAME                     READY   AGE
example-com-123456787   False   88s
$ kubectl describe certificaterequest example-com-123456787
Name:         example-com-123456787
...
Status:
  Conditions:
    Last Transition Time:  2020-03-18T15:19:42Z
    Message:               Waiting on certificate issuance from order default/example-com-123456787-123456787: "pending"
    Reason:                Pending
    Status:                False
    Type:                  Ready
Events:
  Type    Reason        Age   From          Message
  ----    ------        ----  ----          -------
  Normal  OrderCreated  96s   cert-manager  Created Order resource default/example-com-123456787-123456787
$ kubectl get order
NAME                                STATE     AGE
example-com-123456787-123456787   pending   114s
$ kubectl describe order example-com-123456787-123456787
Name:         example-com-123456787-123456787
...
Events:
  Type    Reason   Age   From          Message
  ----    ------   ----  ----          -------
  Normal  Created  118s  cert-manager  Created Challenge resource "example-com-123456787-123456787-123456787" for domain "example.com"

From the order event, we can see that the challenge resource has been created. When the order creation is complete, you will see this in its event.

$ kubectl describe order example-com-123456787-123456787
...
Events:
Type    Reason   Age   From          Message
----    ------   ----  ----          -------
...
Normal  Complete  4m2s   cert-manager  Order completed successfully

The state of order should change to valid.

$ kubectl get order
NAME                                STATE   AGE
example-com-123456787-123456787   valid   7m59s

CertificateRequest should have READY status as True.

$ kubectl get CertificateRequest
NAME                     READY   AGE
example-com-123456787   True    8m14s
$ kubectl describe CertificateRequest
...
Events:
  Type    Reason             Age    From          Message
  ----    ------             ----   ----          -------
  Normal  OrderCreated       8m37s  cert-manager  Created Order resource default/example-com-123456787-123456787
  Normal  CertificateIssued  5m49s  cert-manager  Certificate fetched from issuer successfully

Certificate should also have READY status as True.

$ kubectl get certificate
NAME          READY   SECRET            AGE
example-com   True    example-com-tls   9m53s
$ kubectl describe certificate example-com
...
Events:
  Type    Reason     Age    From          Message
  ----    ------     ----   ----          -------
  Normal  Requested  10m    cert-manager  Created new CertificateRequest resource "example-com-123456787"
  Normal  Issued     7m38s  cert-manager  Certificate issued successfully
Kosy Anyanwu

Kosy Anyanwu

The lady who travels the world and loves karaoke

comments powered by Disqus
rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora