Kubernetes generate certificate using certmanager with dns authenticator using cloudflare

Now that I have installed a haproxy ingress controller it is time to add ssl to our website. For this I will be using an ssl certificate generated on cloudflare by certmanager. Since I am hosting the DNS on cloudflare and mostly using wildcard certificates I am kind of forced to use dns authenticator on the cloudflare side to authenticate the ACME challenge. For this I need to go to the cloudflare account and create an API token that has access to the dns zones. For this after you logged in to cloudflare account and clicked on the domain a bit lower on the right side you will see a link with Get your API token.

You click on it and on the next pannel you click on Create Token you give it a name and configure it with the following rights:

Save the token for now and we will be using it a bit later. For instaling the certmanager in the kubernetes cluster you can simply run the following command:

#kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.6.1/cert-manager.yaml

This will install all the required resources into the kubernetes in a namespace called cert-manager. Once you make sure all the pods are up and running you can move forward. To make sure that everything is up and running you can run:

#kubectl -n cert-manager get pods
NAME                                      READY   STATUS    RESTARTS   AGE
cert-manager-55658cdf68-7vwrf             1/1     Running   1          39h
cert-manager-cainjector-967788869-7k4xl   1/1     Running   6          39h
cert-manager-webhook-7b86bc6578-pwcr7     1/1     Running   0          39h

Now we need to create a secret with the api token generated from cloudflare.

#cat secret.yml
---
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token-secret
  namespace: cert-manager
type: Opaque
stringData:
  api-token: <plain text format of the token>

Note: If you configure a cluster wide certmanager then the secret needs to be created in the cert-manager namespace (For this you need to make sure you have access to the whole cluster and not only to a namespace). Otherwise you can create the secret on the namespace you are controlling and have access to.

Next step is to create an issuer. Again if you have access to the whole cluster then the issuer kind can ClusterIssuer and that is created cluster wide (not tied to a namespace). If on the other hand you have access to one namespace only then the issue kind will be Issuer.  In this case I will be using ACME, DNS01 type of issuer. There are lots of different kind of issuers that can be used. For more information check this link.

#cat issuer.yml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: le-global-issuer
spec:
  acme:
    email: user@email.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-key
    solvers:
    - dns01:
        cloudflare:
          email: user@email.com
          apiTokenSecretRef:
            name: cloudflare-api-token-secret
            key: api-token

As you can see the acme.email has to be any e-mail you want to register your ACME account on. The letsencryt-key is a secret your issuer key will be stored on (can have any name). As you can see the solvers are set for dns01 type and it's using cloudflare. for the e-mail in here you need to use the e-mail address your cloudflare account is configured for. and the apiTokenSecretRef has the name and key configured in the secret a bit earlier. I can check if the issuer/clusterissuer was created correctly by running:

#kubectl get clusterIssuers 
kubectl get clusterIssuers
NAME               READY   AGE
le-global-issuer   True    40h
#kubectl describe clusterissuer le-global-issuer
Name:         le-global-issuer
Namespace:  
...
Status:
  Acme:
    Last Registered Email:  user@email.com
    Uri:                    https://acme-v02.api.letsencrypt.org/acme/acct/362899090
  Conditions:
    Last Transition Time:  2022-01-13T19:44:53Z
    Message:               The ACME account was registered with the ACME server
    Observed Generation:   1
    Reason:                ACMEAccountRegistered
    Status:                True
    Type:                  Ready
Events:                    <none>

By running describe you will see lots of information however the most important is the Status section where you can see if the ACME account was registered and it is in Ready state.

Note: Everything works the same with Issuer with the difference that you need to have the namespace specified and the type would be Issuer instead of ClusterIssuer .

Now it is time to generate the certificates. For this need to create a certificate type of manifest file.

#cat certificate.yml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: certificate
  namespace: default
spec:
  dnsNames:
    - "*.example.com"
  secretName: example
  issuerRef:
    name: le-global-issuer
    kind: ClusterIssuer

Note: The certificate needs to be created on the namespace your application is running at, and the issuerRef needs to reference the type of your issuer.

You can check the status of your certificate by running:

# kubectl get cr -n default
NAME              APPROVED   DENIED   READY   ISSUER             REQUESTOR                                         AGE
certificate-<random-string>      True                True    le-global-issuer   system:serviceaccount:cert-manager:cert-manager   40h

After some time you will see that the Custom Resource will have the Approved state as True. That mean the certificate was successfully generated. And if you check the secrets on the namespace you created it there will be a secret created with the certificate name of type TLS.

Now we go back to the Ingestor operator created earlier to add the TLS reference to it and add some more annotations for example for automatic redirect from http to https.

#cat ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  namespace: default
  annotations:
    ingress.class: "haproxy"
    haproxy.org/ssl-redirect: "true"
    haproxy.org/ssl-redirect-code: "301
spec:
  tls:
    - hosts:
      - "*.redcapcloud.com"
      secretName: certificate
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        backend:
          name: web
          port:
            number: 80

After deploying the updated ingress manifest we can check if it's working:

#curl -H "Host: example.com" http://<public ip address> -ILk
HTTP/1.1 301 Moved Permanently
content-length: 0
location: https://example.com:443/

HTTP/2 200 
date: Sat, 15 Jan 2022 12:09:42 GMT
content-type: text/html;charset=utf-8
set-cookie: JSESSIONID=node0948mbyovct2p16qpwflu3mmyw99083.node0; Path=/; HttpOnly; SameSite=None
expires: Thu, 01 Jan 1970 00:00:00 GMT
content-length: 3259
server: Jetty(9.4.38.v20210224)
set-cookie: SERVERID=SRV_1; path=/
cache-control: private

As you can see everything works as expected the http traffic is redirected to https and if you open the domain in a browser you can see it has a valid certificate deployed.

Valid Certificate