Certificate management with Azure Automation and Let's Encrypt

The Let’s Encrypt project has had a lasting impact on the Internet landscape. Free SSL certificates for everyone can be created automatically and signed by Let’s Encrypt. Due to the broad acceptance by browser manufacturers and cross-signing, the certificates are valid almost everywhere.
However, an automated solution is absolutely necessary due to the short validity period of 90 days. Using PowerShell in Azure Automation, a workflow can be created that takes care of certificate renewal and secure central storage. This workflow is the optimal basis for distributing the certificates to the actual service.

The basis for such an automation solution is provided by AzAutomation-PoshACME

AzAutomation-PoshACME builds on several existing components and uses them for a completely automated certificate workflow.

  • Let’s Encrypt
    Signs the created certificates
  • Azure Automation
    The basis for automation in a server-less environment
  • Posh-ACME
    The incredible implementation of the ACME protocol by Ryan Bolger @rmbolger
  • Azure DNS
    Without a DNS service that offers API support, this project cannot be realized

DNS infrastrucuture

Unfortunately in the so-called enterprise IT there are still many reasons why a DNS zone cannot be changed via API.

These range from organizational hurdles who administers the zone, to technical problems when the DNS provider does not offer an API.

To avoid these problems from the start, AzAutomation-PoshACME has built in full support for CNAME redirection.

CNAME Redirection

CNAME redirection in ACME validation is the redirection of the validation request to another DNS zone. This can be a subzone of the existing DNS zone or a completely different DNS zone.


In this example, an additional subzone “” has been created below the DNS zone “”. The name server record for this zone was changed to the Azure DNS.

A certificate should be created for the domain “”. During validation Let’s Encrypt will check if the challenge in the DNS TXT record “” is correct.

The DNS record is redirected to “”, a record in the subzone managed by Azure, using a CNAME record.


This behavior allows certificates to be issued for individual records without putting the entire Root DNS zone of the company under the control of the certificate issuer. However, a CNAME record for the ACME challenge must be created manually for each domain that is to receive a certificate. This is a one-time action.

Separate DNS Zone

In this case, not a subzone is placed under the control of the Azure DNS, but a completely separate domain.


The functionality is the same, but this prevents the creation of further DNS entries below the root zone of the company.

Azure components

The Azure components are kept to a minimum:

  • Resource Group
  • DNS Zone
  • Storage Account
  • Azure Automation Account
  • Runbooks

Since Azure Automation is used for automation, no separate compute resources are required.

Building the environment

All necessary resources can be obtained from the project’s Git repository.

git clone
cd .\AzAutomation-PoshACME\
code .


Currently only Windows PowerShell is supported due to the use module to create self signed certificates!

Deploy Ressources

The script “DeployRessources.ps1” contains all necessary commands to create the necessary resources. The script has a workshop character and is to be executed step by step and not in one go.

Environment variables

In the upper section, the environment variables must be adapted to your environment.

  • ResourceGroupName
    The name of the Resource Group to be created
  • Location
    In which Azure region the resources should be created. The default is Western Europe
  • DNSZoneRootDomain
    Which DNS zone should be used for validation. An Azure DNS zone is created for this zone
  • MailContact
    To which e-mail address should Let’s Encrypt send information about expiring certificates.
  • BlobStorageName
    The name of the Storage Account. It may only contain small letters and numbers and must be unique worldwide.
  • AutomationAccountName
    The name of the Azure Automation account. The default is “LetsEncryptAutomation”.
  • PfxPass
    What password should be used for the exported PFX files? This value is stored encrypted in the Azure Automation Account.

Let’s start creating stuff


After the environment variables are initialized and the block PowerShell code is executed with F8 it is time to connect to Azure.


The next command creates the necessary resource group

# Create resource group
$ResourceGroup = New-AzResourceGroup -Name $ResourceGroupName -Location $Location

In the next step the DNS zone is created.

#region Create DNS Zone
$DNSZone = New-AzDnsZone -Name $DNSZoneRootDomain -ResourceGroupName $ResourceGroupName
# Retrieve DNS server names for the NS records
$DNSZone | Select-Object -ExpandProperty NameServers
# Add those to your custom DNS zone

It is important that the DNS name servers for this zone are also configured at the registrar of the zone or in the subzone. Only then Let’s Encrypt knows that Azure DNS manages this zone.


Insgesamt werden vier Nameserver ausgegeben

For the storage of the certificates a Storage Account is now created and access is restricted to HTTPS only. In addition, a SA token is created for Azure Automation to later access the storage account.

#region BLOB Storage to store the Posh-ACME configuration data
New-AzStorageAccount -Name $BlobStorageName -ResourceGroupName $ResourceGroupName -Location $Location -Kind StorageV2 -SkuName Standard_LRS -EnableHttpsTrafficOnly $true
$storageAccountKey = Get-AzStorageAccountKey -Name $BlobStorageName -ResourceGroupName $ResourceGroupName | Where-Object KeyName -eq "key1" | Select-Object -ExpandProperty Value
$storageContext = New-AzStorageContext -StorageAccountName $BlobStorageName -StorageAccountKey $storageAccountKey
New-AzStorageContainer -Name "posh-acme" -Context $storageContext

#SAS Token for blob access
$SASToken = New-AzStorageContainerSASToken -Name "posh-acme" -Permission rwdl -Context $storageContext -ExpiryTime (Get-Date).AddYears(5)  -StartTime (Get-Date)

The script creates an Entra ID (Azure AD) application and a Service Principal so that the runbooks of the Automation Account can later manage the DNS zone.

#region Create a service principal without any permissions assigned
$application = New-AzADApplication -DisplayName "Let's Encrypt Certificate Automation" -IdentifierUris "http://localhost"
$spPrincipal = New-AzADServicePrincipal -ApplicationId $application.ApplicationId -Role $null -Scope $null
$spCredential = New-AzADSpCredential -ServicePrincipalObject $spPrincipal -EndDate (Get-Date).AddYears(5)


Im Azure Portal wird diese Applikation als “Let’s Encrypt Certificate Automation” geführt

The Service Principal is assigned the role “DNS Zone Contributor” to the created DNS zone.

#region Grant service principal "DNS Zone Contributor" permissions to DNS Zone
New-AzRoleAssignment -ObjectId $spPrincipal.Id -ResourceGroupName $ResourceGroupName -ResourceName $DNSZoneRootDomain -ResourceType "Microsoft.Network/dnszones" -RoleDefinitionName "DNS Zone Contributor"

The Automation Account is created with this command

#region Create automation account
New-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName -Location $Location

To authenticate Azure Automation to Azure, the script creates a self-signed certificate and stores it in the context of the currently logged on user.

#region Create certificate for Azure Automation Run As Account
$CertificateName = $AutomationAccountName + $CertificateAssetName
$param = @{
    "DnsName"           = $certificateName
    "CertStoreLocation" = "cert:\CurrentUser\My"
    "KeyExportPolicy"   = "Exportable"
    "Provider"          = "Microsoft Enhanced RSA and AES Cryptographic Provider"
    "NotAfter"          = (Get-Date).AddMonths($selfSignedCertNoOfMonthsUntilExpired)
    "HashAlgorithm"     = "SHA256"
$Cert = New-SelfSignedCertificate @param

This certificate is exported as PFX file (Private + Public Key)

#region Export certificate to temp folder
$selfSignedCertPlainPassword = $PfxPass
$CertPassword = ConvertTo-SecureString $selfSignedCertPlainPassword -AsPlainText -Force
$PfxCertPath = Join-Path $env:TEMP ($CertificateName + ".pfx")
Export-PfxCertificate -Cert ("Cert:\CurrentUser\my\" + $Cert.Thumbprint) -FilePath $PfxCertPath -Password $CertPassword -Force | Write-Verbose

The public part of the certificate is stored as part of an Entra ID (Azure AD) Application Credential in the “Let’s Encrypt Certificate Automation” application for authentication.

#region Create Application Credential to use for authentication of RunAs Account
$PfxCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($PfxCertPath, $selfSignedCertPlainPassword)
$param = @{
    "ApplicationId" = $application.ApplicationId
    "CertValue"     = ( [System.Convert]::ToBase64String($PfxCert.GetRawCertData()) )
    "StartDate"     = $PfxCert.NotBefore
    "EndDate"       = $PfxCert.NotAfter
$applicationCredential = New-AzADAppCredential @param


Now the private key is stored as Automation Certificate in the Azure Automation Account

#region Add certificate to automation account
$param = @{
    "ResourceGroupName"     = $ResourceGroupName
    "AutomationAccountName" = $AutomationAccountName
    "Name"                  = $CertificateAssetName
    "Path"                  = $PfxCertPath
    "Password"              = $CertPassword
    "Exportable"            = $false
$AutomationCertificate = New-AzAutomationCertificate @param


With this certificate Azure Automation can authenticate itself to Azure

An Azure Automation Connection is created for simplified login within the runbooks. This contains all necessary information for the login.

These are the Application Id, the TenantId, the Certificate Thumbprint and the SubscriptionId.

#region Add Run As Account Connection to automation account
$SubscriptionInformation = Get-AzContext | Select-Object -ExpandProperty Subscription
$ConnectionFieldValues = @{
    "ApplicationId"         = $application.ApplicationId
    "TenantId"              = $SubscriptionInformation.TenantId
    "CertificateThumbprint" = $AutomationCertificate.Thumbprint
    "SubscriptionId"        = $SubscriptionInformation.SubscriptionId
$param = @{
    "ResourceGroupName"     = $ResourceGroupName
    "AutomationAccountName" = $AutomationAccountName
    "Name"                  = $ConnectionAssetName
    "ConnectionTypeName"    = $ConnectionTypeName
    "ConnectionFieldValues" = $connectionFieldValues
New-AzAutomationConnection @param


The next code region, not shown here, installs the necessary modules into the Azure Automation Account.

  • Az.Accounts
  • Az.Resources
  • Az.Storage
  • Posh-ACME
# Coderegion - Deploy the necessary module, this will take a while

If desired, the following code can be copied into a runbook to check the connection and module availability.

$connection = Get-AutomationConnection -Name 'AzureRunAsConnection'
Connect-AzAccount -ServicePrincipal -Tenant $connection.TenantID -ApplicationId $connection.ApplicationID -CertificateThumbprint $connection.CertificateThumbprint
Get-Module -ListAvailable

In order to enable the runbooks to use the defined default values later, these are stored in Azure Automation variables.

  • PAServer
    This value defines which Let’s Encrypt environment should be used. The default is the Staging Environment (LE_STAGE).
    If valid certificates should be issued, this value must be changed to “LE_PROD”!
  • ACMEContact
    The defined standard e-mail contact
  • StorageContainerSASToken
    The encrypted value for access to the Storage Account
  • BlobStorageName
    The name of the Blob Storage
  • PfxPass
    The encrypted password for the PFX files
  • WriteLock
    Default is “$false”. This variable prevents more than one runbook writing access to the configuration data.
# Coderegion - Set variables

The last code region copies all runbooks from the subfolder “runbooks” into the Azure Automation account. Make sure that the PowerShell session is in the correct folder.

#region Deploy Runbooks to Azure Automation account
$Runbooks = Get-ChildItem .\runbooks -Filter *.ps1
foreach ($Runbook in $Runbooks) {
    $param = @{
        "Path"                  = $Runbook.FullName
        "Name"                  = $Runbook.BaseName
        "Type"                  = "PowerShell"
        "Published"             = $true
        "ResourceGroupName"     = $ResourceGroupName
        "AutomationAccountName" = $AutomationAccountName
    Import-AzAutomationRunbook @param

The next part of this blog series will discuss the two runbooks “New-LetsEncryptCertificate” and “Update-LetsEncryptCertificates”.