« Back to home

As of August 2022, Azure doesn't support issuing Let's Encrypt certificates.

There is a nice feature of automatically renewed free certificates if you use Azure CDN, but that only applies to subdomains, for some reason, they do not support these free certificates with apex domains. Also, other Azure products (such as Azure App Service), do not support free certificates at all.

Personally, I am using acme.sh to issue Let's Encrypt certificates. It's written in bash i.e. it works on any Linux machine without any additional installations, doesn't need root access (unlike the certbot), requires minimum effort to configure, and supports a wide range of certificate issuing methods, including, notably, Azure DNS.

The DNS method allows to put the certificate issuing script on any VM, which is very convenient. You can get a smallest Azure VM (or reuse any of your existing VMs as it only runs once a day so it doesn't really consume any resources on the VM) and use it to issue all your certificates, for all your websites (no matter what technology they use).

Issue certificate with Acme.sh

Issuing process with acme.sh usually looks like this:

  1. First, we issue the certificate with

    acme.sh --issue -d mydomain.com -d www.mydomain.com --dns dns_azure

    (you need some env variables set up for this command to work, see the guide for details)

  2. Second, we install the certificate, i.e. move it to the correct location. acme.sh also allows to execute a "reload" command, which we can use to prepare the certificate and upload it to Azure Key Vault.

    acme.sh --install-cert -d mydomain.com \
        --fullchain-file /home/certmanager/certs/mydomain.com.crt \
        --key-file /home/certmanager/certs/mydomain.com.key \
        --reloadcmd /home/certmanager/deploy_mydomain.sh

    Here we are simply moving the certificate files to the home folder so that we can easily use them from the deployment script.

You don't need to do anything else rather than these two commands. acme.sh will take care of automatically renewing the certificate and re-uploading it to Azure Key Vault.

Preparing certificate for upload

Azure Key Vault only supports importing the certificates in PFX format. So we need to convert the certificate from acme.sh PEM format to the PFX format.

We can use openssl pkcs command for this. Openssl is typically installed on any Linux VM. The command looks something like this:

cat mydomain.com.cer mydomain.com.key > mydomain.com.pem
openssl pkcs12 -export -in mydomain.com.pem -out mydomain.com.pfx -password pass:$PASSWORD

Notice that acme.sh produces certificate and private key separately, but you would need to concatenate them together in order to generate the PFX.

Authenticating to Azure Key Vault REST API

Azure Key Vault has a well documented REST API. In order to use this API, we first need to authenticate via a fairly standard OAuth client_credentials flow. It boils down to a single curl request.

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" \
    -d client_id=$APP_ID \
    --data-urlencode scope=https://vault.azure.net/.default \
    -d client_secret=$APP_SECRET \
    -d grant_type=client_credentials \

This requires a service principal (aka Azure AD application). We must also grant the service principal rights to access our Key Vault.

Creating a service principal is very straightforward and is covered by Azure documentation.

Don't forget to generate a secret and copy it, as well as the App Id - you will need it later.

Then, you need to open your Key Vault and create a new policy under Access Policies, pointing to the newly created service principal and giving it the "Import" permission.

Using the Azure Key Vault REST API

We need to use the "Import certificate" API in order to upload the certificate.

You need to send a JSON payload: encode the PFX file into base64 and supply it in the value field, along with some additional fields such as pwd for password, etc.

We end up with the following curl command:

    curl -s -X POST -H "Authorization: Bearer ${TOKEN}" \
        -d '{"value": "'$JOINED_B64'", "pwd": "'$PASSWORD'", "policy": { "key_props": { "exportable": true, "kty": "RSA", "key_size": 2048, "reuse_key": false }, "secret_props": { "contentType": "application/x-pkcs12" } } }'
        -H "Content-Type: application/json"

That's it!

Joining it all together

The resulting deploy_mydomain.sh script will look something like this:


set -e


cat /home/certmanager/certs/mydomain.com.crt /home/certmanager/certs/mydomain.com.key > /home/certmanager/certs/mydomain.com.joined.pem

PASSWORD=$(openssl rand -hex 16)

openssl pkcs12 -export -password pass:$PASSWORD -in /home/certmanager/certs/mydomain.com.joined.pem -out /home/certmanager/certs/mydomain.com.joined.pfx

TOKEN=$(curl -s -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'client_id='$AZUREDNS_APPID'&scope=https%3A%2F%2Fvault.azure.net%2F.default&client_secret='$AZUREDNS_CLIENTSECRET'&grant_type=client_credentials' 'https://login.microsoftonline.com/'$AZUREDNS_TENANTID'/oauth2/v2.0/token' | jq -r '.access_token')

JOINED_B64=$(base64 -w 0 /home/certmanager/certs/mydomain.com.joined.pfx)

curl -s -X POST -H "Authorization: Bearer ${TOKEN}" -d '{"value": "'$JOINED_B64'", "pwd": "'$PASSWORD'", "policy": { "key_props": { "exportable": true, "kty": "RSA", "key_size": 2048, "reuse_key": false }, "secret_props": { "contentType": "application/x-pkcs12" } } }' -H "Content-Type: application/json" https://your-keyvault.vault.azure.net/certificates/mydomain-com/import?api-version=7.3

Please don't forget to replace all occurences of mydomain.com to your actual domain and also change the keyvault URL in the last command.


Let's hope Azure will add Let's Encrypt certificates at some point, but for now we can use acme.sh in combination with the script above to issue and deploy certificates.


comments powered by Disqus