Free Wildcard Certificates using Azure DNS, Let’s Encrypt and acme.sh

     

Prelude

Goal

We want to obtain wildcard certificates from Let’s Encrypt ACME v2. We want to verify ourselves using DNS, specifically the dns-01 method, because DNS verification doesn’t interrupt your web server and it works even if your server is unreachable from the outside world. The DNS provider is Azure DNS.

Ingredients

Outline

  1. You log in via Azure CLI.
  2. You create a new Role Definition which lets identities set TXT records, which are used by dns-01.
  3. You create a new Service Principal and Application with this new role assigned, and set its scope to your DNS Zone or Resource Group.
  4. You obtain acme.sh and set it up using the Subscription ID, Tenant ID, Application ID, and Application Password.
  5. You obtain a staging certificate, then a production certificate.
  6. You set up a cronjob to make the renewal happen periodically, automatically.

Walkthrough

Azure

Login

First off, install PowerShell and Azure CLI. Then open PowerShell and log in to Azure:

az login

This will instruct you to open the Device Login page and enter the code you received in the PowerShell window. Then return something like this:

To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code BXYG7SP2V to authenticate.
[
  {
    "cloudName": "AzureCloud",
    "id": "d70e649c-eafb-4719-ae43-22da51ad9249",
    "isDefault": true,
    "name": "Pay-As-You-Go",
    "state": "Enabled",
    "tenantId": "77e33597-ab77-4314-ba03-30b98340a715",
    "user": {
      "name": "[email protected]",
      "type": "user"
    }
  }
]

Take note of id, which is your subscription ID, and tenantId, which is, of course, your tenant ID. These IDs are UUIDs, as most credentials in this guide.

An alternate method to find your subscription ID is going to the Azure Portal / Subscriptions blade. For tenant ID, go to the Azure Active Directory blade, select Properties, your tenant ID is listed under Directory ID.

Role Definition

Once logged in, we need to define a role that allows for setting TXT records. First we might as well get the current list:

az role definition list | ConvertFrom-Json | Format-Table -Property roleName,name

To create a new role, we gotta create a JSON file. In this case, something like this will suffice:

{
  "Name":"DNS TXT Contributor",
  "Id":"",
  "IsCustom":true,
  "Description":"Can manage DNS TXT records only.",
  "Actions":[
    "Microsoft.Network/dnsZones/TXT/*",
    "Microsoft.Network/dnsZones/read",
    "Microsoft.Authorization/*/read",
    "Microsoft.Insights/alertRules/*",
    "Microsoft.ResourceHealth/availabilityStatuses/read",
    "Microsoft.Resources/deployments/read",
    "Microsoft.Resources/subscriptions/resourceGroups/read"
  ],
  "NotActions":[

  ],
  "AssignableScopes":[
    "/subscriptions/d70e649c-eafb-4719-ae43-22da51ad9249"
  ]
}

Save it as role.json. Make sure to update the AssignableScopes key with your subscription ID! Once done, add this role definition to your Azure subscription:

az role definition create --role-definition role.json

If something goes wrong, you can delete it like this:

az role definition delete --name "DNS TXT Contributor"

Role Assignment

Now we gotta decide how we want to assign the role. There are 2 options:

  • Assign it to a single DNS Zone
  • Assign it to a Resource Group that contains any number of DNS zones

I prefer the latter, because it’s easier to maintain in the long run: when you want SSL for a new domain, you can just add it to this resource group, instead of messing around in PowerShell every time.

To query the available

  • DNS zones:
az network dns zone list | ConvertFrom-Json | Format-Table -Property id
  • Resource groups:
az group list | ConvertFrom-Json | Format-Table -Property id

Now create a role assignment.

  • DNS zone:
az ad sp create-for-rbac --name "Acme2DnsValidator" --role "DNS TXT Contributor" --scopes "/subscriptions/d70e649c-eafb-4719-ae43-22da51ad9249/resourceGroups/FooDNSGroup/providers/Microsoft.Network/dnszones/foobar.com"
  • Resource group:
az ad sp create-for-rbac --name "Acme2DnsValidator" --role "DNS TXT Contributor" --scopes "/subscriptions/d70e649c-eafb-4719-ae43-22da51ad9249/resourceGroups/FooDNSGroup"

Either command will return something similar to this:

Retrying role assignment creation: 1/36
Retrying role assignment creation: 2/36
{
    "appId": "c236f357-cd55-4b01-ae94-0ac56107ecd0",
    "displayName": "Acme2DnsValidator",
    "name": "http://Acme2DnsValidator",
    "password": "9dd5ac78-84b1-42bf-9ae6-f8b202fa17d7",
    "tenant": "77e33597-ab77-4314-ba03-30b98340a715"
}

Make sure to take proper note of appId, i.e. your Application ID , and password, which is your Application Password.

This command will create 3 things at once:

  • a new Service Principal
  • a new Application that corresponds to this service principal
  • a Role Assignment between the application, and the role definition that we created in the previous step

As always, it might be needed to query or delete these entities. Here’s the cheat sheet:

  • Role assignment:
az role assignment list --all | ConvertFrom-Json | Format-Table -Property principalName,roleDefinitionName,scope
az role assignment delete --assignee "http://Acme2DnsValidator" --role "DNS TXT Contributor" --scope "/subscriptions/d70e649c-eafb-4719-ae43-22da51ad9249/resourceGroups/FooDNSGroup"
  • Service principal:
az ad sp list --all | ConvertFrom-Json | Format-Table -Property displayName,appId
az ad sp delete --id c2a5775c-486e-4d55-a92e-5b65845c4bd1
  • Application (AFAIK deleting the service principal will delete the corresponding application, but just in case):
az ad app list | ConvertFrom-Json | Format-Table -Property displayName,appId
az ad app delete --id c236f357-cd55-4b01-ae94-0ac56107ecd0

acme.sh

Setup

acme.sh is one of the many Let’s Encrypt clients. It has built-in support for Azure DNS, and it is written in pure Bash, so it seems the obvious choice for our case. Most importantly, it supports ACME v2, which allows for wildcard certificates. To obtain it:

git clone https://github.com/Neilpang/acme.sh.git /opt/acme.sh

Now set up the credentials we collected in the previous steps:

export AZUREDNS_SUBSCRIPTIONID="d70e649c-eafb-4719-ae43-22da51ad9249"
export AZUREDNS_TENANTID="77e33597-ab77-4314-ba03-30b98340a715"
export AZUREDNS_APPID="c236f357-cd55-4b01-ae94-0ac56107ecd0"
export AZUREDNS_CLIENTSECRET="9dd5ac78-84b1-42bf-9ae6-f8b202fa17d7"

Don’t worry, we only need to set these once, then acme.sh will save those in its config folder. Now acquire a staging cert for foobar.com and all its subdomains:

/opt/acme.sh/acme.sh --issue --dns dns_azure --dnssleep 10 --force -d foobar.com -d *.foobar.com --staging

If it works, you can try doing the same for a production cert:

/opt/acme.sh/acme.sh --issue --dns dns_azure --dnssleep 10 --force -d foobar.com -d *.foobar.com

To actually deploy the cert in nginx:

/opt/acme.sh/acme.sh --install-cert -d foobar.com -d *.foobar.com --fullchain-file /etc/nginx/certs/fullchain.pem --key-file /etc/nginx/certs/privkey.pem --reloadcmd "nginx -t && nginx -s reload"

Then your host config should contain something like this:

ssl_certificate certs/fullchain.pem;
ssl_certificate_key certs/privkey.pem;

Renewal

A cronjob like this should suffice:

00 07 1 * * root /opt/acme.sh/acme.sh "--renew" "--dns" "dns_azure" "--dnssleep" "10" "--force" "-d" "foobar.com" "-d" "*.foobar.com" >> /var/log/acme.sh 2>&1

Cheers folks!