Obtain SSL/TLS Certificates Automatically for AWS

Mert Simsek
Beyn Technology
Published in
7 min readNov 5, 2021

I’d like to set free you from the cumbersome process of obtaining SSL/TLS certificates by this post. For this, I’d like to use a package in Go programming language supported by ACME Protocol. It’s Lego package that is a basic ACME client. I don’t want to talk about the importance of SSL/TLS certificates or how we get them by Let’s Encrypt. There are a ton of blog posts and articles regarding them on the internet. Normally we used to get certificates by CLI tool named “certbot” but it works on command-line. We want to get certificates by a package/library. They consume the ACME protocol and help to get certificates from it. Thus, we will be able to automate this process. For now, I’ll demonstrate it for AWS Route53(DNS provider) but it supports various DNS providers. You can check out them on the documentation page.

There’s been a lot of confusion about ACME, Let’s Encrypt, and this whole “free certificates” thing, so first, a few clarifications:

  • ACME is the protocol that facilitates the automatic issuance, renewal, and revocation of x.509 certificates between certificate authorities and applicants. At the time of writing, the spec is still a working draft at the IETF.
  • ISRG is the non-profit organization behind Let’s Encrypt.
  • Let’s Encrypt is the first certificate authority (CA) to implement the ACME protocol.
  • Domain Validation (DV) Certificates are issued once a CA is convinced you own the domain you are requesting a certificate for. Let’s Encrypt issues DV certs. Make no mistake: all DV certificates are technically the same. A free, automated DV cert offers no fewer benefits than one costing $10 or $20.

First, we initialize a module for Go. I give a name as “certprovider”. You can give whatever you want.

go mod init certprovider

Let’s install the required packages for these steps.

go get -u github.com/go-acme/lego/v4/cmd/lego
go get github.com/joho/godotenv

We installed the required packages and I create a .env file in the root directory of the project like the following.

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=

CA_DIR_URL=https://acme-staging-v02.api.letsencrypt.org/directory
EMAIL=mert.simsek@beyn.com.tr

You need to get AWS parameters from your cloud provider and put them. CA_DIR_URL is a provided stage URL for testing. Thus we don’t face limitation problems. After that, you need to give an e-mail address that is linked to certificates. The email address is optional but highly recommended so you can recover your account later if you lose your private key. Also, don’t lose any of the private keys we generate in this process. That means once you generate a key, you should save it for next time. How you do this is up to you.

Firstly, we load environment variables from the file.

package main

import (
"github.com/joho/godotenv"
"log"
)

func main() {
err := godotenv.Load()
if err != nil {
log.Fatalln("Error loading .env file")
}
}

Then we are supposed to have a type that implements the ACME. It’s a user interface. Before a certificate can be issued, you’ll need to register an account with the CA. The following codes are out of the main func.

type User struct {
Email string
Registration *acme.RegistrationResource
Key *rsa.PrivateKey
}

func (u User) GetEmail() string {
return u.Email
}
func (u User) GetRegistration() *acme.RegistrationResource {
return u.Registration
}
func (u User) GetPrivateKey() *rsa.PrivateKey {
return u.Key
}

In the main func, we need to generate a private key for the user. We initialize a user with the private key and the e-mail. As you remember, we defined the e-mail by .env file.

var certificates *certificate.Resource

privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatalln(err)
}

user := User{
Email: os.Getenv("EMAIL"),
Key: privateKey,
}

After that, we are supposed to pass the ACME DNS challenge. At the moment, we use AWS cloud provider and I’ll give its Route53 service as a DNS provider. Lego package reads environment variables and authenticates them to our cloud provider and it is AWS. Then, it creates a “TXT” record for the domain. So, it solves the challenge automatically, this part is awesome! If our challenges are solved successfully, that “TXT” record has been erased by Lego. Ultimately, this process is handled smoothly.

conf := lego.NewConfig(&user)
conf.CADirURL = os.Getenv("CA_DIR_URL")
conf.Certificate.KeyType = certcrypto.RSA2048

client, err := lego.NewClient(conf)
if err != nil {
log.Fatalln(err)
}

dnsProvier, err := route53.NewDNSProvider()
if err != nil {
log.Fatalln(err)
}

err = client.Challenge.SetDNS01Provider(dnsProvier)
if err != nil {
log.Fatalln(err)
}

Once we solve the challenge well, we have to register our user in ACME Protocol. Here, we give our domain and registered user. Thus, we are able to obtain certificates that are related to the domain. In addition to that, we can also give multiple domains at the same time such as “beyn.com.tr” and “*.beyn.com.tr”

// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return certificates, err
}
user.Registration = reg
//here you need to change domain variable whatever you want.
request := certificate.ObtainRequest{
Domains: []string{domain},
Bundle: true,
}
certificates, err = client.Certificate.Obtain(request)
if err != nil {
return certificates, err
}

fmt.Println(certificates)

In the end, I print obtained certificates. Well, I leave the whole main.go file like the following. We have one file to have an example and I’ll run it.

package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/providers/dns/route53"
"github.com/go-acme/lego/v4/registration"
"github.com/joho/godotenv"
"log"
"os"
"fmt"
)

func main() {
err := godotenv.Load()
if err != nil {
log.Fatalln("Error loading .env file")
}
var certificates *certificate.Resource

privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return certificates, err
}

user := User{
Email: os.Getenv("EMAIL"),
Key: privateKey,
}

conf := lego.NewConfig(&user)
conf.CADirURL = os.Getenv("CA_DIR_URL")
conf.Certificate.KeyType = certcrypto.RSA2048

client, err := lego.NewClient(conf)
if err != nil {
return certificates, err
}

dnsProvier, err := route53.NewDNSProvider()
if err != nil {
return certificates, err
}

err = client.Challenge.SetDNS01Provider(dnsProvier)
if err != nil {
return certificates, err
}

// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return certificates, err
}
user.Registration = reg
//here you need to change domain variable whatever you want.
request := certificate.ObtainRequest{
Domains: []string{domain},
Bundle: true,
}
certificates, err = client.Certificate.Obtain(request)
if err != nil {
return certificates, err
}

fmt.Println(certificates)

}

type User struct {
Email string
Registration *acme.RegistrationResource
Key *rsa.PrivateKey
}

func (u User) GetEmail() string {
return u.Email
}
func (u User) GetRegistration() *acme.RegistrationResource {
return u.Registration
}
func (u User) GetPrivateKey() *rsa.PrivateKey {
return u.Key
}

When I run this file with the following command, I need to get the certificates.

go run .

Let’s check out the output. Solving the challenge might take 20–30 seconds and we wait for finishing it.

Then, when it is solved, we are able to get our obtained certificates successfully.

Voila! Actually, it’s up to you from now on. You can store these certificates in the database and you can put an API in front of that database and provide certificates to the clients. Thus, you will have to obtain certificates automatically.

Renewal

Obviously, you can renew your certificates with a few lines. I’d like to remind you that you are supposed to certificate in a struct that Lego package waits. So, you need to arrange according to it.

newCerts, err := client.RenewCertificate(certificate, false, true)
if err != nil {
log.Fatal(err)
}

Custom DNS Challenge Solver

Perhaps you cannot find your DNS provider to solve by Lego automatically. For instance, I leave an image here to see supported DNS providers. There are a ton of DNS providers that are supported such as AWS Route53, Google Cloud, Azure, Cloudflare, Digital Ocean, and so on.

If you cannot find your provider, you can a write custom challenge solver for you. It’s really easy with the following documentation page. Basically, you need to authenticate your DNS provider and add a “TXT” record by the API manually.

To Sum Up

I think if you have various domains, it’d be an awesome way to obtain certificates for them automatically. In another case, you will struggle to get certificates for each one. You just need to have a DNS provider that has an API to reach. Then just give everything to the Lego package to handle everything. It’s very useful.

--

--

Mert Simsek
Beyn Technology

I’m a software developer who wants to learn more. First of all, I’m interested in building, testing, and deploying automatically and autonomously.