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.
Subzone
In this example, an additional subzone “levalidation.cloudbrothers.info” has been created below the DNS zone “cloudbrothers.info”. The name server record for this zone was changed to the Azure DNS.
A certificate should be created for the domain “test.cloudbrothers.info”. During validation Let’s Encrypt will check if the challenge in the DNS TXT record “_acme-challenge.test.cloudbrothers.info” is correct.
The DNS record is redirected to “_acme-challenge.test.cloudbrothers.info.levalidation.cloudbrothers.info”, 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. https://github.com/f-bader/AzAutomation-PoshACME
git clone https://github.com/f-bader/AzAutomation-PoshACME
cd .\AzAutomation-PoshACME\
code .
Attenttion
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.
Connect-AzAccount
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
#endregion
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)
#endregion
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)
#endregion
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"
#endregion
The Automation Account is created with this command
#region Create automation account
New-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName -Location $Location
#endregion
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
#endregion
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
#endregion
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
#endregion
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
#endregion
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
#endregion
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-AzResource
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
}
#endregion
The next part of this blog series will discuss the two runbooks “New-LetsEncryptCertificate” and “Update-LetsEncryptCertificates”.