Using Azure Container Instances to create Let’s Encrypt Certificates

You might have been confronted with the challenge that certificate management sometimes presents. Your website should be secure, but digital certificates can be expensive. Not only in terms of money, but they bring along a certain complexity too. Luckily there are two technologies that can help you to overcome both challenges:

  • Let’s Encrypt is a non-profit Certificate Authority (CA) that issues certificates at no cost.
  • Secret vaults such as Azure Key Vault can alleviate the overhead of certificate management: a centralized repository for your certificates, and the source where other Azure services will take their certificates from.

Azure Key Vault offers creating two types of certificates (see Azure Key Vault certificates for more details):

  • Self-signed
  • Signed by a partner Certificate Authority, but Let’s Encrypt as CA is not in the list of partner Certificate Authorities for Azure Key Vault at the time of this writing

So if you want to use the free Let’s Encrypt certs, you need to create them yourselves. You can automate the creation and renewal of certificates with Let’s Encrypt using the ACME protocol. Luckily you don’t need to understand anything of it, since many ACME clients exist out there that can help with this task. One of the most popular ones is certbot, a command-line application that allows to send certificate requests to LetsEncrypt.

When you run certbot to generate a digital certificate, LetsEncrypt will return a challenge to validate that the domain actually belongs to you. This challenge can be either HTTP-based (uploading a certain file in your web server) or DNS-based (creating a certain TXT record in your domain). Since we are trying to generate a certificate to put it into Azure Key Vault, potentially we don’t have any web site yet. So we will take the DNS challenge. The rest of this document assumes that your domain is hosted in Azure DNS.

OK, that was a lot of new words. In short, this is the sequence of events we want to achieve:

  • Step 1: Use certbot to send a certificate request to Lets Encrypt
  • Step 2: Create a TXT record in Azure DNS to fulfill the challenge and prove we own the domain
  • Step 3: Get the certificate generated by Lets Encrypt, and put it into Azure Key Vault
  • Step 4: Clean up the TXT record from Azure DNS
  • Step 5: Now you can use that certificate anywhere else in Azure

As you can see, from step 2 onwards you need to run operations on Azure, for which you can use a number of different frameworks. In this example we will use the Azure CLI. Container images offer a great way of packaging the requirements we need (check out the great ACME bot for an alternative using Azure Functions). As you can see in the Dockerfile of this repo, I am taking the image mcr.microsoft.com/azure-cli with the latest Azure CLI version, and I am adding certbot to it (along some scripts to create/delete the TXT record to Azure DNS). That’s it. Now we need to run it!

In Azure there are multiple platforms that can run Docker containers, for our purpose Azure Container Instances are ideal: When you need to generate or renew a certificate, you can spin up an ACI, and when it finishes you will have the new certificate stored in your Azure Key Vault.

There is one more hurdle we need to jump: how will the Azure Container Instance authenticate to Azure? Managed Identities. We can create a Managed Identity, and give it enough privilege for Azure DNS (to solve the LetsEncrypt challenge) and to Azure Key Vault (to create the certificate). This code will create the identity in a new resource group and assign those permissions, you only need to run it once in your subscription:

# Variables
rg=acicertbot
location=westeurope
id_name=certbotid
akv_name=your_vault_name
dns_zone=yourdomain.com
# Create RG and user identity
az group create -n $rg -l $location
id_resid=$(az identity show -n $id_name -g $rg --query id -o tsv)
if [[ -z "$id_resid" ]]
then
    echo "Creating user identity ${id_name}..."
    az identity create -n $id_name -g $rg
    id_spid=$(az identity show -n $id_name -g $rg --query principalId -o tsv)
    id_resid=$(az identity show -n $id_name -g $rg --query id -o tsv)
    # Assign permissions to AKV
    az keyvault set-policy -n $akv_name --object-id $id_spid \
        --secret-permissions get list set \
        --certificate-permissions create import list setissuers update \
        --key-permissions create get import sign verify
    # Assign permisses to Azure DNS Zone
    dns_zone_id=$(az network dns zone list --query "[?name=='$dns_zone'].id" -o tsv)
    if [[ -n "$dns_zone_id" ]]
    then
        echo "DNS zone $dns_zone found, resource ID $dns_zone_id, creating role assignment..."
        az role assignment create --scope $dns_zone_id --assignee $id_spid --role "DNS Zone Contributor"
    else
        echo "DNS zone $dns_zone not found"
    fi
else
    echo "User identity ${id_name} found, ID is $id_resid"
fi

You can build the image with the files in the Github repository, and push it to your favorite container registry:

# Build and push image
docker build -t yourdockerusername/certbot-azcli:1.0 .
docker push yourdockerusername/certbot-azcli:1.0

Or you can use my image if you prefer: erjosito/certbot-azcli:1.0.

And that’s it, you can now run the container now:

# Run ACI to generate certificate
akv_name=erjositoKeyvault
aci_name=certbot
image=erjosito/certbot-azcli:1.0
dns_hostname=certbot
domain="${dns_hostname}.${dns_zone}"
email_address=youremail@contoso.com
az container create -n $aci_name -g $rg -l $location --image $image --assign-identity $id_resid \
  -e "DOMAIN=$domain" "EMAIL=$email_address" "AKV=$akv_name"

And you are done! If you go to your Azure Key Vault, you will find your certificate there. If you want to see an end to end example of how to use this with Azure Web Apps, I have this code for you:

# Variables
rg=certtest                              # Resource group where the web app will be created
location=westeurope                      # Location where the web app will be created
akv_name=your_vault_name                 # Here is where the certificate will be stored
svcplan_name=webappplan                  # Not too original name for our service plan
app_name=web$RANDOM                      # Random name for the app
image=gcr.io/kuar-demo/kuard-amd64:blue  # I love this image for testing
tcp_port=8080                            # Port where the previous image is listening to
dns_zone_name=yourdomain.com             # You should own this DNS zone, that should be hosted in Azure DNS
app_dns_name=$app_name                   # You could have a different DNS name, but I default to the app name
domain="${app_dns_name}.${dns_zone}"     # Full domain name of our app
email_address=youremail@contoso.com      # It will be used in the cert creation
id_name=certbotid                        # Name of managed identity with permissions to AzDNS and AKV
id_rg=acicertbot                         # Resource Group of managed identity with permissions to AzDNS and AKV

# Create cert with ACI
id_resid=$(az identity show -n $id_name -g $id_rg --query id -o tsv)
az container create -n certbot -g $rg -l $location --image erjosito/certbot-azcli:1.0 --assign-identity $id_resid \
  -e "DOMAIN=$domain" "EMAIL=$email_address" "AKV=$akv_name"
cert_name=$(echo $domain | tr -d '.')  # the container will create a cert with the domain name removing the dots (.)

# Create Web App
az group create -n $rg -l $location
az appservice plan create -n $svcplan_name -g $rg --sku B1 --is-linux
az webapp create -n $app_name -g $rg -p $svcplan_name --deployment-container-image-name $image
az webapp config appsettings set -n $app_name -g $rg --settings "WEBSITES_PORT=${tcp_port}"
az keyvault set-policy -n $akv_name --spn abfa0a7c-a6b6-4736-8310-5855508787cd \
    --secret-permissions get \
    --key-permissions get \
    --certificate-permissions get
az webapp config ssl import -n $app_name -g $rg --key-vault $akv_name --key-vault-certificate-name $cert_name
cert_thumbprint=$(az webapp config ssl list -g $rg --query '[0].thumbprint' -o tsv)
az webapp restart -n $app_name -g $rg
app_hostname=$(az webapp show -n $app_name -g $rg --query defaultHostName -o tsv)

# Update DNS name
dns_zone_rg=$(az network dns zone list --query "[?name=='$dns_zone_name'].resourceGroup" -o tsv)
echo "Adding CNAME record ${app_dns_name}.${dns_zone_name} for Webapp $app_hostname"
az network dns record-set cname set-record -z $dns_zone_name -g $dns_zone_rg -n $app_dns_name -c $app_hostname
app_fqdn="${app_dns_name}.${dns_zone_name}"

# Add custom domain to web app
az webapp config hostname add --webapp-name $app_name -g $rg --hostname $app_fqdn
az webapp config ssl bind -n $app_name -g $rg --certificate-thumbprint $cert_thumbprint --ssl-type SNI
az webapp update -n $app_name -g $rg --https-only true

# Test
echo "Visit with your browser the URL https://${app_fqdn}"

Find more code related to this in my Github repository: https://github.com/erjosito/certbot-azcli. Please let me know if you find any problem using this!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: