Following my previous post on generating self-signed certificates with Terraform, this one is the second post of the series.
This time we are going to use Let's Encrypt as the certificate authority (CA) instead of our own machine. As a result we will get trusted certificates that can be used in production, for free.
Previously in the "TLS with Terraform and Azure" series...
If you haven't read my previous post you can check out this section to get the common context for the whole series.
There is also a GitHub repository that contains all the code from this series of posts:
A repository showing how to generate certificates using Terraform
Certificate generation with Terraform for Azure App Service
This repository contains sample code to generate TLS certificates using Terraform.
It uses an Azure App Service as an example of a website to secure.
The certificates are generated in 3 ways:
By creating a self-signed certificate
By requesting a certificate from Let's Encrypt
By creating an Azure App Service managed certificate
Obviously the third example can only work with Azure. The two others are written to work with Azure as well but can be adapted or used as an inspiration to work on other platforms.
Getting started
Set the variables of the root module
If you want to run this against your Azure infrastructure you will need to provide values for the variables of the root module:
The dns_zone_name should contain the name of your DNS Zone managed in Azure
The dns_zone_rg_name should contain the name of the resource group containing…
Level 2: requesting a certificate from Let's Encrypt
If you don't know what Let's Encrypt is, in a few words it's a free, automated and open certificate authority (CA), allowing everyone to get certificates trusted by browsers at no charge.
As the Let's Encrypt certificates are valid for 90 days (instead of one year for commercial authorities), automation is strongly recommended.
So how can we automate this with Terraform ? Using the third-party provider ACME. ACME stands for Automated Certificate Management Environment, the protocol used by Let's Encrypt. The Terraform ACME provider supports any ACME CA, so we need to configure Let's Encrypt's endpoint in the provider configuration:
terraform{required_providers{# The provider is declared here just like any provider...acme={source="vancluever/acme"version="~> 2.0"}}}# ...and is configured here, with the Let's Encrypt production endpoint.provider"acme"{server_url="https://acme-v02.api.letsencrypt.org/directory"}
Let's Encrypts provides a staging endpoint you should use for testing: https://acme-staging-v02.api.letsencrypt.org/directory
There is a limit of 50 certificates per domain and per week on the production environment, on the staging it's 30,000 (but the certificates will not be trusted by the browser)
{: .prompt-tip }
Once the provider properly configured, we can start creating resources. As for self-signed certificates, it starts here with a private key, and using this private key and an email we create a registration which is an account on the ACME server:
# Creates a private key in PEM formatresource"tls_private_key""private_key"{algorithm="RSA"}# Creates an account on the ACME server using the private key and an emailresource"acme_registration""reg"{account_key_pem=tls_private_key.private_key.private_key_pememail_address=var.email}
Then we can request a certificate using our account, this is where the real magic happens:
# As the certificate will be generated in PFX a password is requiredresource"random_password""cert"{length=24special=true}# Gets a certificate from the ACME serverresource"acme_certificate""cert"{account_key_pem=acme_registration.reg.account_key_pemcommon_name=var.common_name# The hostname goes herecertificate_p12_password=random_password.cert.resultdns_challenge{# Many providers are supported for the DNS challenge, we are using Azure DNS hereprovider="azure"config={# Some arguments are passed here but it's not enough to let the provider access the zone in Azure DNS.# Other arguments (tenant id, subscription id, and cient id/secret) must be set through environment variables.AZURE_RESOURCE_GROUP=var.dns.zone_rg_nameAZURE_ZONE_NAME=var.dns.zone_nameAZURE_TTL=300}}}
The key part is in the dns_challenge block of the acme_certificate resource.
Before generating a certificate for our domain, Let's Encrypt checks that we own that domain. To do that it proceeds with a DNS challenge, basically it generates a random string and will not generate the certificate unless that random string is in a specific TXT record of the DNS zone.
Obviously the ACME provider does that for us, we just need to let it access our DNS zone. The provider supports many DNS providers, in this case we are using Azure DNS.
So we need to tell the ACME provider where our DNS zone is, so we specify its name and resource group name in the config block above. Unfortunately the provider cannot use the Azure CLI authentication so we need to set additional arguments for authentication, so that the provider knows the subscription we are using, and a client id/secret to access it.
This is the kind of information that should not be pushed to a git repository, so I prefer to put these as environment variables in a .env git-ignored file like this:
export ARM_TENANT_ID="<YOUR AZURE TENAND ID (a guid)>"export ARM_SUBSCRIPTION_ID="<YOUR AZURE SUBSCRIPTION ID (another guid)>"export ARM_CLIENT_ID="<AN APP REGISTRATION ID (yep it's a guid too)>"export ARM_CLIENT_SECRET="<THE APP REGISTRATION SECRET (not a guid this time)>"
Then I use the source .env command and the ACME provider can use the environment variables to authenticate to Azure and add/remove records in my DNS zone.
If you are using Terraform Cloud or running Terraform in a CI/CD context you will not need to do this as the environment variables are already set
Once the full code has been applied you can check out the Activity Log of your DNS zone.
You should see that the service principal corresponding to the environment variables has created and deleted a TXT record in the zone : The creation and deletion of the TXT record should occur within a minute
And more importantly you can browse your App Service from its custom hostname and see how your browser enjoys this fresh trusted certificate:
Note the R3 issuer which is Let's Encrypt, and the absence of security warning ✅
Wrapping up
That's if for this post, which is the one why I have started this series. At first I had trouble understanding how this Let's Encrypt/ACME provider works and didn't find any content showing a full example.
So I hope this post makes sense and helps other to issue trusted certificates using Terraform.