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.