Lets encrypt certificates and Kubernetes

A simple guide listing how you generate AWS SSL certs for use in your Kubernetes cluster.
Updated 6 months ago Published 9 months ago
I've recently started to migrate my home network away from Pfsense and over a shiny new Ubiquity Dream machine pro, I can hear the screams of disgust from some of the networking folk already.
Over the past few years I have been running Pfsense at the core of my home network and It's served me extremely well and i've learnt a hell of a lot along the way.
But I'll admin that whilst I loved the feature set provided the pure power and occasional complexity of the features provided was a lot of overhead and simple updates were often a more hassle than I had time for being a new parent as such I decided it as time to bite the bullet and move to something a bit easier to manage. I already use a number of Ubiquiti switches and access points at home so the decison to move over to an entirely Ubiquity based setup was a pretty easy decision.
But I'll openly admit the feature gap between Pfsense and the Ubiquiti Dream machine pro was something I thought that could be easily mitigated, some things were easy to migrate, other features I decided I could live without. But some features I really missed.
One of the aforementioned nifty features provided by Pfsense was it's built in HaProxy plugin which I previously used to hook up the external pod IP's provisioned from load balanced Kubernetes services, it even included automated ACME certificate provisioning.
Alas now that I have moved over to a Ubiquiti Dream machine pro these nifty features are no longer avaliable.
Introducing [cert-manager](https://cert-manager.io/docs/configuration/acme/)
cert-manager is a native Kubernetes certificate management controller. It can help with issuing certificates from a variety of sources, such as Let's Encrypt, HashiCorp Vault, Venafi, a simple signing key pair, or self signed.
Cert manager allows you to add predefined metadata to a Kubernetes ingress in conjunction with a secondary proxy deployed on your cluster, in this case the proxy of choice is Trafiek.
I will only cover the generation of a new certificate in this article and not the configuration of Trafiek but I may cover this in a follow up article when I get some more time.
I generally use "Lets encrypt' for all of my certificates, I would to use AWS Private certificate Authority but that's a pretty hefty chunk of change and it's hard to beat free.
I have attached a script which can be used to add a new A(AAA records still need to be added) record to a pre-existing AWS R53 hosted zone (with the assumption this is also configured correctly), This is required given we need a chain of trust in order for ACME to provision the certificate.
When cert-manager provisions a new certificate it need's to ensure you are the legitimate owner of the domain as such you can validate your ownership of the domain via either a HTTP get endpoint hosted on the domain you wish to provision the certificate for or via a new DNS txt entry.
We are going to use HTTP validation as it's simpler and the process will automatically clean up and junk you don't need after the certificate has been signed.
The below script will simply (as mentioned above) create a new R53 A record for you, then it will create a new issuer resource which will be used to request a new certificate and then finally create a new certificate using your provided DNS name.
With "lets encrypt" you can also test your changes via their dedicated test infrastructure as requests against the production endpoints are rate limited and repeated attempts against the same domain will result in an 36 hour lockout.
#!/bin/sh
# This script will create a certificate for a subdomain
# ./certificate-domain.sh staging 1.1.1.1  Z03501612HD7E79ETXM8B superdomain.com
# ./certificate-domain.sh production 1.1.1.1 superdomain.com
staging="https://acme-staging-v02.api.letsencrypt.org/directory"
production="https://acme-v02.api.letsencrypt.org/directory"
echo $staging
echo $production
if [ $1 == "staging" ]
then
  domain=$staging
else
  domain=$production
fi
if [ -z $4 ]
then 
  fulldomain=$3
  fulldomainString=$3
else
  fulldomain=$4.$3
  fulldomainString=$4-$3
fi
echo "full domain is $fulldomain"
echo "We will be creating the following certificate $domain"
echo "Curate R53 config for $fulldomain"
recordSet=$(cat <<EOF
{
  "Comment": "A new record set for the zone.",
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "$fulldomain.",
        "Type": "A",
        "TTL": 600,
        "ResourceRecords": [
          {
            "Value": "$2"
          }
        ]
      }
    }
  ]
}
EOF
)
cat <<< $recordSet > r53-staging-request.json
echo "Generate a certificate for $fulldomain"
aws route53 change-resource-record-sets --hosted-zone-id Z03501612HD7E79ETXM8B --change-batch file://r53-staging-request.json
echo "Generate certificate issuer for $fulldomain"
cat <<EOF | kubectl create -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: letsencrypt-$fulldomainString-$1-issuer
  namespace: default
spec:
  acme:
    # The ACME server URL
    server: $domain
    # Email address used for ACME registration
    email: domain@name.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-$1-issuer
    # Enable the HTTP-01 challenge provider
    solvers:
    - http01:
        ingress:
          class: traefik
EOF
echo "Generate certificate for $fulldomain"
cat <<EOF | kubectl create -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: $fulldomainString-$1
  namespace: default
spec:
  secretName: $fulldomainString-$1-certificate
  issuerRef:
    name: letsencrypt-$fulldomainString-$1-issuer
  commonName: $fulldomain
  dnsNames:
  - $fulldomain
EOF
I have also spun up a new Github repo with the same code