I can bet almost any IT technician have heard of Letsencrypt certificates. They are completely free and fully supported by big corps such as Google, Facebook, Microsoft, and many others, to have a more secure and privacy-respecting Web. A lot of public websites are using those certificates. Of course, as for any free stuff, there are some limitations. For example how many certificates you can generate. Pay attention that Let’s Encrypt certificates are valid for 90 days. So setting up automatic renewal of Letsencrypt certificates is absolutely necessary.
When using an Azure Application Gateway, one of the things you need to do is to install the SSL certificate on the gateway to secure resources behind.
In this article I’ll show you how to set up Letsencrypt certificates automatic renewal in Azure Key Vault and Azure Application Gateway.
The renewal process explained
The idea behind this implementation is to avoid any modification on whatever infrastructure is behind the Application Gateway, to complete the renewal checks and validations made by Let’s Encrypt process. In summary:
- an Azure Automation runbook will be executed in a schedule (i.e. once every two weeks) to renew and install the current Let’s Encrypt certificate. Let’s Encrypt needs to validate the domain ownership, so it returns a challenge code which is stored by the runbook on a storage account behind the application gateway;
- a special rule on the Application Gateway redirects the validation check coming from Let’s Encrypt to the storage account, so the domain ownership check is successful
- the Azure Automation runbook finally downloads the new certificate and install it on the Application Gateway
Depending on your needs it’s possible:
- Renew and install Letsencrypt cert directly on Azure Application Gateway or
- Renew Letsencrypt certificates in the Azure Key Vault and bind to necessary certificates in properties of the respective App Gateway listener
Both options will be described below. I’ll mark the steps specific for a particular option with the respective images above.
Note that with this implementation, there is no need to manipulate any other infrastructure behind the Application Gateway.
Let’s go!
Creating container to process ACME challenges
I wanted to issue and automate the renewals of Let’s Encrypt certificates for “api.app.com”. Note that I had already a DNS record of Type A targeting my Application Gateway.
C:\>nslookup api.app.com
Server: google-public-dns-a.google.com
Address: 8.8.8.8
Authoritative response:
Domain: api.app.com
Address: 23.102.37.253
To implement the Let’s Encrypt renewal process to issue new SSL certificates on the Application Gateway, follow these steps:
Create a Storage Account
1. Create an Azure Storage account that will be used to host the challenge requests for the DNS domain ownership check. Use the cheapest parameters such as “Standard performance” and LRS.
2. Once the storage account is ready, create a “public” container with “public blob” permissions
3. Create the virtual directory “\.well-known\acme-challenge” using the Storage Explorer tool.
Modify the Application Gateway to redirect ACME challenge requests to the storage account
4. When you created the Azure Application Gateway, you probably specified a HTTP rule that was associated to an http listener. In this case, you need to delete that rule that will be replaced by a Path-based rule as shown in the next step
5. Create a new path-based rule that redirects the requests that will be made by Let’s Encrypt on the renewal process with the following configuration:
6. Set the parameters you had on the http rule, and click on “Add Configuration”
7. Specify the configuration parameters with the path “/.well-known/acme-challenge/*” with a redirection (Permanent), targeting an external site with the storage account container URL you created before:
9. Test the rule by creating a file called “test.html” on the storage account and browsing the URL http://<yourdomain>/.well-known/acme-challenge/test.html
If everything was setup correctly, when browsing the URL, the application gateway should redirect your browser to the storage account as shown below. Don’t continue until you have successfully setup the redirection rule.
This step is common for both options.
Issue Letsencrypt certificate for the first time
There are several ways to issue the certificate, but the easiest one is to use Certbot for Linux environment, a tool available on GitHub and built on Python that allows you to obtain certs from Let’s Encrypt and Win-ACME for Windows. There is also Powershell extension ACME-PS that you can use. There are other clients, so you can probably share better ideas on the comments area of this post.
I used the third option and the following cmdlet to get the certificate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
####################################################################################### # Based on https://intelequia.com/blog/post/1012/automating-azure-application-gateway-ssl-certificate-renewals-with-let-s-encrypt-and-azure-automation # Script that gets Let's Encrypt certificate and uploads it to Azure Key Vault (that are used on Application gateway) # Pre-requirements: # - Have a storage account in which the folder path has been created: # '/.well-known/acme-challenge/', to put here the Let's Encrypt DNS check files # - Add "Path-based" rule in the Application Gateway with this configuration: # - Path: '/.well-known/acme-challenge/*' # - Check the configure redirection option # - Choose redirection type: permanent # - Choose redirection target: External site # - Target URL: <Blob public path of the previously created storage account> # - Example: 'https://test.blob.core.windows.net/public' # # # Following modules are needed now: Az.Accounts, Az.Network, Az.Storage, Az.KeyVault, ACME-PS # ####################################################################################### [CmdletBinding()] param( # Domain to get the certificate for [string]$Domain, [string]$EmailAddress, # Resource group name where blob starage resides [string]$STResourceGroupName, # Blob storage name [string]$storageName, [string]$KeyVaultName, [string]$CertNameinKeyVault ) # Ensures that no login info is saved after the runbook is done Disable-AzContextAutosave # Log in as the service principal from the Runbook $connection = Get-AutomationConnection -Name AzureRunAsConnection Login-AzAccount -ServicePrincipal -Tenant $connection.TenantID -ApplicationId $connection.ApplicationID -CertificateThumbprint $connection.CertificateThumbprint Write-Output "Trying to get a new certificate for $domain" # Create a state object and save it to the harddrive $tempFolderPath = $env:TEMP + "\" + $domain # Preparing folder for certificate renewal # Remove folder used for certificate renewal if existing if(Test-Path $tempFolderPath -PathType Container) { Get-ChildItem -Path $tempFolderPath -Recurse | Remove-Item -force -recurse Remove-Item $tempFolderPath -Force } $tempFolder = New-Item -Path $tempFolderPath -ItemType "directory" $state = New-ACMEState -Path $tempFolder $serviceName = 'LetsEncrypt' # Fetch the service directory and save it in the state Get-ACMEServiceDirectory $state -ServiceName $serviceName -PassThru; # Get the first anti-replay nonce New-ACMENonce $state; # Create an account key. The state will make sure it's stored. New-ACMEAccountKey $state -PassThru; # Register the account key with the acme service. The account key will automatically be read from the state New-ACMEAccount $state -EmailAddresses $EmailAddress -AcceptTOS; # Load an state object to have service directory and account keys available $state = Get-ACMEState -Path $tempFolder; # It might be neccessary to acquire a new nonce, so we'll just do it for the sake of the example. New-ACMENonce $state -PassThru; # Create the identifier for the DNS name $identifier = New-ACMEIdentifier $domain; # Create the order object at the ACME service. $order = New-ACMEOrder $state -Identifiers $identifier; # Fetch the authorizations for that order $authZ = Get-ACMEAuthorization -State $state -Order $order; # Select a challenge to fullfill $challenge = Get-ACMEChallenge $state $authZ "http-01"; # Inspect the challenge data $challenge.Data; # Create the file requested by the challenge $fileName = $tempFolderPath + '\' + $challenge.Token; Set-Content -Path $fileName -Value $challenge.Data.Content -NoNewline; $blobName = ".well-known/acme-challenge/" + $challenge.Token $storageAccount = Get-AzStorageAccount -ResourceGroupName $STResourceGroupName -Name $StorageName $ctx = $storageAccount.Context Set-AzStorageBlobContent -File $fileName -Container "public" -Context $ctx -Blob $blobName # Signal the ACME server that the challenge is ready $challenge | Complete-ACMEChallenge $state; # Wait a little bit and update the order, until we see the states while ($order.Status -notin ("ready", "invalid")) { Start-Sleep -Seconds 10; $order | Update-ACMEOrder $state -PassThru; } # We should have a valid order now and should be able to complete it # Therefore we need a certificate key $certKey = New-ACMECertificateKey -Path "$tempFolder\$domain.key.xml"; # Complete the order - this will issue a certificate singing request Complete-ACMEOrder $state -Order $order -CertificateKey $certKey; # Now we wait until the ACME service provides the certificate url while (-not $order.CertificateUrl) { Start-Sleep -Seconds 15 $order | Update-Order $state -PassThru } # As soon as the url shows up we can create the PFX $password = ConvertTo-SecureString -String "GOOD_LONG_SECRET" -Force -AsPlainText Export-ACMECertificate $state -Order $order -CertificateKey $certKey -Path "$tempFolder\$domain.pfx" -Password $password; # Delete blob to check DNS Remove-AzStorageBlob -Container "public" -Context $ctx -Blob $blobName ### Upload new Certificate version to KeyVault Import-AzKeyVaultCertificate -VaultName $KeyVaultName -Name $CertNameinKeyVault -FilePath "$tempFolder\$domain.pfx" -Password $password Write-Output "Completed. Check the certificates in Key Vault. there should be the new one named $CertNameinKeyVault" |
Now check your Key Vault. There should be the new certificate there.
At this step you have certificate.pfx ready for App Gateway or Key Vault .
If you need a Letsencrypt certificate with multiple domains (Subject Alternative Names) in Azure use the improved version of the script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
####################################################################################### # Based on https://intelequia.com/blog/post/1012/automating-azure-application-gateway-ssl-certificate-renewals-with-let-s-encrypt-and-azure-automation # Script that renews a Let's Encrypt certificate in Azure Key Vault (that are used on Application gateway) # Pre-requirements: # - Have a storage account in which the folder path has been created: # '/.well-known/acme-challenge/', to put here the Let's Encrypt DNS check files # - Add "Path-based" rule in the Application Gateway with this configuration: # - Path: '/.well-known/acme-challenge/*' # - Check the configure redirection option # - Choose redirection type: permanent # - Choose redirection target: External site # - Target URL: # - Example: 'https://test.blob.core.windows.net/public' # # # Following modules are needed now: Az.Accounts, Az.Network, Az.Storage, Az.KeyVault, ACME-PS # ####################################################################################### [CmdletBinding()] param( # Domain to get the certificate for [string[]]$Domains = @("domain1.com","domain1.com","domain3.com","domain4.com"), [string]$CertNameinKeyVault = "Letsencrypt-multiple-SAN", [string]$EmailAddress = "youremail@domain.com", # Resource group name where blob starage resides [string]$STResourceGroupName = "azure-resource-group", # Blob storage name [string]$storageName = "storage", [string]$KeyVaultName = "MyKeyVault" ) # Ensures that no login info is saved after the runbook is done Disable-AzContextAutosave # Log in as the service principal from the Runbook $connection = Get-AutomationConnection -Name AzureRunAsConnection Login-AzAccount -ServicePrincipal -Tenant $connection.TenantID -ApplicationId $connection.ApplicationID -CertificateThumbprint $connection.CertificateThumbprint $daysFromNow = (Get-Date).AddDays($ExpiresInDays) # Get all certificates from Azure Key Vault # $sslCerts = Get-AzKeyVaultCertificate -VaultName $KeyVaultName Write-Output "Trying to get a new certificate for $domain" # Create a state object and save it to the harddrive $tempFolderPath = $env:TEMP + "\" + $domain # Preparing folder for certificate renewal # Remove folder used for certificate renewal if existing if(Test-Path $tempFolderPath -PathType Container) { Get-ChildItem -Path $tempFolderPath -Recurse | Remove-Item -force -recurse Remove-Item $tempFolderPath -Force } $tempFolder = New-Item -Path $tempFolderPath -ItemType "directory" $state = New-ACMEState -Path $tempFolder $serviceName = 'LetsEncrypt' # Fetch the service directory and save it in the state Get-ACMEServiceDirectory $state -ServiceName $serviceName -PassThru; # Get the first anti-replay nonce New-ACMENonce $state; # Create an account key. The state will make sure it's stored. New-ACMEAccountKey $state -PassThru; # Register the account key with the acme service. The account key will automatically be read from the state New-ACMEAccount $state -EmailAddresses $EmailAddress -AcceptTOS; # Load an state object to have service directory and account keys available $state = Get-ACMEState -Path $tempFolder; # It might be neccessary to acquire a new nonce, so we'll just do it for the sake of the example. New-ACMENonce $state -PassThru; # Create the identifier for the DNS name # $identifier = New-ACMEIdentifier $domain; # Create the order object at the ACME service. # $order = New-ACMEOrder $state -Identifiers $identifier; $dnsIdentifiers = $Domains | ForEach-Object { New-ACMEIdentifier $_ }; # Create a new order $order = New-ACMEOrder -State $state -Identifiers $dnsIdentifiers; # Fetch the authorizations for that order $authZ = @(Get-ACMEAuthorization -State $state -Order $order); foreach($auth in $authZ) { # Select a challenge to fullfill $challenge = Get-ACMEChallenge $state $auth "http-01"; # Inspect the challenge data $challenge.Data; # Create the file requested by the challenge $fileName = $tempFolderPath + '\' + $challenge.Token; Set-Content -Path $fileName -Value $challenge.Data.Content -NoNewline; $blobName = ".well-known/acme-challenge/" + $challenge.Token $storageAccount = Get-AzStorageAccount -ResourceGroupName $STResourceGroupName -Name $StorageName $ctx = $storageAccount.Context Set-AzStorageBlobContent -File $fileName -Container "ContainerInsideStorage" -Context $ctx -Blob $blobName # Signal the ACME server that the challenge is ready $challenge | Complete-ACMEChallenge $state; } # Wait a little bit and update the order, until we see the states while ($order.Status -notin ("ready", "invalid")) { Start-Sleep -Seconds 10; $order | Update-ACMEOrder $state -PassThru; } # Should the order get invalid, use Get-ACMEAuthorizationError to list error details. if($order.Status -ieq ("invalid")) { $order | Get-ACMEAuthorizationError -State $AcmeStateDir; throw "Order was invalid"; } # We should have a valid order now and should be able to complete it # Therefore we need a certificate key $certKey = New-ACMECertificateKey -Path "$tempFolder\$domain.key.xml"; # Complete the order - this will issue a certificate singing request Complete-ACMEOrder $state -Order $order -CertificateKey $certKey; # Now we wait until the ACME service provides the certificate url while (-not $order.CertificateUrl) { Start-Sleep -Seconds 15 $order | Update-Order $state -PassThru } # As soon as the url shows up we can create the PFX $password = ConvertTo-SecureString -String "password" -Force -AsPlainText Export-ACMECertificate $state -Order $order -CertificateKey $certKey -Path "$tempFolder\$domain.pfx" -Password $password; # Delete blob to check DNS Remove-AzStorageBlob -Container "ContainerInsideStorage" -Context $ctx -Blob $blobName ### Upload new Certificate version to KeyVault Import-AzKeyVaultCertificate -VaultName $KeyVaultName -Name $CertNameinKeyVault -FilePath "$tempFolder\$domain.pfx" -Password $password Write-Output "Completed. Check the certificates in Key Vault. there should be the new one named $CertNameinKeyVault" |
Here is the commands to convert .PEM to .PFX for Linux and Windows.
Linux:
1 |
# openssl pkcs12 -export -out certificate.pfx -inkey privkey.pem -in certificate.pem |
Windows:
1 2 |
$pwd = ConvertTo-SecureString -String P@ssword -Force -AsPlainText Export-PfxCertificate -cert cert:\LocalMachine\WebHosting\4F4135541B7CEB38F937A75EDB9CDE5333237885 -FilePath c:\certificate.pfx -Password $pwd |
where “4F4135541B7CEB38F937A75EDB9CDE5333237885 ” is the certificate’s thumbprint.
Next , I’ll show how to install the certificate on App Gateway and configure automatic renewal.
If you need integration with Key Vault please skip this step .
Install Letsencrypt certificate on Azure App Gateway for the first time
This should be simple step : in listener’s properties upload the certificate.
IMPORTANT: remember the name you are going to give to this certificate, since you will need to specify it as a parameter on the renewal process later
After applying the changes, you can check that the LetsEncrypt SSL certificate is working properly just by browsing a resource via HTTPS.
Upload Letsencrypt certificate on Azure Key Vault for the first time
Enable Application Gateway to get certificates from Key vault
Configure Application Gateway listener to use the certificate from Key Vault
Setting up renewal process
Create an Automation Account
1. On the Azure Portal, create an Azure Automation account (or use an existing one) to host the runbook. Note that you can create this automation account and run up to 500 minutes per month for free.
2. Inside the Automation resource, open the Modules and browse the gallery to import the following modules: ‘Az.Accounts’, ‘Az.KeyVault’ , ‘Az.Network’, ‘Az.Storage’ and ‘ACME-PS’. .
3. On the Azure Automation account, create a PowerShell runbook called LetsEncryptCertificateRenewal
4. Edit the powershell runbook and paste the following contents for
automatic certificate renewal directly on App Gateway:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
####################################################################################### # Based on https://intelequia.com/blog/post/1012/automating-azure-application-gateway-ssl-certificate-renewals-with-let-s-encrypt-and-azure-automation # Script that renews a Let's Encrypt certificate for an Azure Application Gateway # Pre-requirements: # - Have a storage account in which the folder path has been created: # '/.well-known/acme-challenge/', to put here the Let's Encrypt DNS check files # - Add "Path-based" rule in the Application Gateway with this configuration: # - Path: '/.well-known/acme-challenge/*' # - Check the configure redirection option # - Choose redirection type: permanent # - Choose redirection target: External site # - Target URL: <Blob public path of the previously created storage account> # - Example: 'https://test.blob.core.windows.net/public' # # # Following modules are needed now: Az.Accounts, Az.Network, Az.Storage, ACME-PS # ####################################################################################### [CmdletBinding()] param( $ExpiresInDays = 110, [string]$EmailAddress, [string]$STResourceGroupName, [string]$storageName, [string]$AGResourceGroupName, [string]$AGName, ) # Ensures that no login info is saved after the runbook is done Disable-AzContextAutosave # Log in as the service principal from the Runbook $connection = Get-AutomationConnection -Name AzureRunAsConnection Login-AzAccount -ServicePrincipal -Tenant $connection.TenantID -ApplicationId $connection.ApplicationID -CertificateThumbprint $connection.CertificateThumbprint $daysFromNow = (Get-Date).AddDays($ExpiresInDays) $AppGw = Get-AzApplicationGateway -Name $AGName -ResourceGroupName $AGResourceGroupName $sslCerts = Get-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGw $sslCerts = Get-AzKeyVaultCertificate -VaultName $KeyVaultName $sslCerts | ForEach-Object { $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($_.PublicCertData.Substring(60, $_.PublicCertData.Length - 60))) $_.Name $cert.NotAfter # Check if the certificate corresponds naming agreement (starts with 'LetsEncrypt' and whether it is expiring) if ($_.Name -like "LetsEncrypt-*" -and $cert.NotAfter -le $daysFromNow) { $domain = $cert.Subject.Replace("CN=", "") $AGOldCertName = $_.Name Write-Output "Start renewing of the certificate: $AGOldCertName for $domain" # Create a state object and save it to the harddrive $tempFolderPath = $env:TEMP + "\" + $domain # Preparing folder for certificate renewal # Remove folder used for certificate renewal if existing if(Test-Path $tempFolderPath -PathType Container) { Get-ChildItem -Path $tempFolderPath -Recurse | Remove-Item -force -recurse Remove-Item $tempFolderPath -Force } $tempFolder = New-Item -Path $tempFolderPath -ItemType "directory" $state = New-ACMEState -Path $tempFolder $serviceName = 'LetsEncrypt' # Fetch the service directory and save it in the state Get-ACMEServiceDirectory $state -ServiceName $serviceName -PassThru; # Get the first anti-replay nonce New-ACMENonce $state; # Create an account key. The state will make sure it's stored. New-ACMEAccountKey $state -PassThru; # Register the account key with the acme service. The account key will automatically be read from the state New-ACMEAccount $state -EmailAddresses $EmailAddress -AcceptTOS; # Load an state object to have service directory and account keys available $state = Get-ACMEState -Path $tempFolder; # It might be neccessary to acquire a new nonce, so we'll just do it for the sake of the example. New-ACMENonce $state -PassThru; # Create the identifier for the DNS name $identifier = New-ACMEIdentifier $domain; # Create the order object at the ACME service. $order = New-ACMEOrder $state -Identifiers $identifier; # Fetch the authorizations for that order $authZ = Get-ACMEAuthorization -State $state -Order $order; # Select a challenge to fullfill $challenge = Get-ACMEChallenge $state $authZ "http-01"; # Inspect the challenge data $challenge.Data; # Create the file requested by the challenge $fileName = $tempFolderPath + '\' + $challenge.Token; Set-Content -Path $fileName -Value $challenge.Data.Content -NoNewline; $blobName = ".well-known/acme-challenge/" + $challenge.Token $storageAccount = Get-AzStorageAccount -ResourceGroupName $STResourceGroupName -Name $storageName $ctx = $storageAccount.Context Set-AzStorageBlobContent -File $fileName -Container "public" -Context $ctx -Blob $blobName # Signal the ACME server that the challenge is ready $challenge | Complete-ACMEChallenge $state; # Wait a little bit and update the order, until we see the states while ($order.Status -notin ("ready", "invalid")) { Start-Sleep -Seconds 10; $order | Update-ACMEOrder $state -PassThru; } # We should have a valid order now and should be able to complete it # Therefore we need a certificate key $certKey = New-ACMECertificateKey -Path "$tempFolder\$domain.key.xml"; # Complete the order - this will issue a certificate singing request Complete-ACMEOrder $state -Order $order -CertificateKey $certKey; # Now we wait until the ACME service provides the certificate url while (-not $order.CertificateUrl) { Start-Sleep -Seconds 15 $order | Update-Order $state -PassThru } # As soon as the url shows up we can create the PFX $password = ConvertTo-SecureString -String "Passw@rd123***" -Force -AsPlainText Export-ACMECertificate $state -Order $order -CertificateKey $certKey -Path "$tempFolder\$domain.pfx" -Password $password; # Delete blob to check DNS Remove-AzStorageBlob -Container "public" -Context $ctx -Blob $blobName ### RENEW APPLICATION GATEWAY CERTIFICATE ### $appgw = Get-AzApplicationGateway -ResourceGroupName $AGResourceGroupName -Name $AGName Set-AzApplicationGatewaySSLCertificate -Name $AGOldCertName -ApplicationGateway $appgw -CertificateFile "$tempFolder\$domain.pfx" -Password $password Set-AzApplicationGateway -ApplicationGateway $appgw } } |
For automatic certificates renewal in the Key Vault the script will be as following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
####################################################################################### # Based on https://intelequia.com/blog/post/1012/automating-azure-application-gateway-ssl-certificate-renewals-with-let-s-encrypt-and-azure-automation # Script that renews a Let's Encrypt certificate in Azure Key Vault (that are used on Application gateway) # Pre-requirements: # - Have a storage account in which the folder path has been created: # '/.well-known/acme-challenge/', to put here the Let's Encrypt DNS check files # - Add "Path-based" rule in the Application Gateway with this configuration: # - Path: '/.well-known/acme-challenge/*' # - Check the configure redirection option # - Choose redirection type: permanent # - Choose redirection target: External site # - Target URL: <Blob public path of the previously created storage account> # - Example: 'https://test.blob.core.windows.net/public' # # # Following modules are needed now: Az.Accounts, Az.Network, Az.Storage, Az.KeyVault, ACME-PS # ####################################################################################### [CmdletBinding()] param( $ExpiresInDays = 110, [string]$EmailAddress, [string]$STResourceGroupName, [string]$storageName, [string]$KeyVaultName ) # Ensures that no login info is saved after the runbook is done Disable-AzContextAutosave # Log in as the service principal from the Runbook $connection = Get-AutomationConnection -Name AzureRunAsConnection Login-AzAccount -ServicePrincipal -Tenant $connection.TenantID -ApplicationId $connection.ApplicationID -CertificateThumbprint $connection.CertificateThumbprint $daysFromNow = (Get-Date).AddDays($ExpiresInDays) # Get all certificates form Azure Key Vault $sslCerts = Get-AzKeyVaultCertificate -VaultName $KeyVaultName Write-Output "Check for certificates that expire in $ExpiresInDays days" $sslCerts | ForEach-Object { $cert = Get-AzKeyVaultCertificate -VaultName $KeyVaultName -Name $_.name # Save ID of the current certificate versions, that will be used for disabling later $certificateOldVersion = $cert.Version # Check if the certificate corresponds naming agreement (starts with 'LetsEncrypt' and whether it is expiring) if ($cert.Name -like "LetsEncrypt-*" -and $cert.Expires -le $daysFromNow) { # Extract domain name from the Subject $domain = $cert.Certificate.Subject.Replace("CN=", "") $AGOldCertName = $cert.Name Write-Output "Start renewing of the certificate: $AGOldCertName for $domain" # Create a state object and save it to the harddrive $tempFolderPath = $env:TEMP + "\" + $domain # Preparing folder for certificate renewal # Remove folder used for certificate renewal if existing if(Test-Path $tempFolderPath -PathType Container) { Get-ChildItem -Path $tempFolderPath -Recurse | Remove-Item -force -recurse Remove-Item $tempFolderPath -Force } $tempFolder = New-Item -Path $tempFolderPath -ItemType "directory" $state = New-ACMEState -Path $tempFolder $serviceName = 'LetsEncrypt' # Fetch the service directory and save it in the state Get-ACMEServiceDirectory $state -ServiceName $serviceName -PassThru; # Get the first anti-replay nonce New-ACMENonce $state; # Create an account key. The state will make sure it's stored. New-ACMEAccountKey $state -PassThru; # Register the account key with the acme service. The account key will automatically be read from the state New-ACMEAccount $state -EmailAddresses $EmailAddress -AcceptTOS; # Load an state object to have service directory and account keys available $state = Get-ACMEState -Path $tempFolder; # It might be neccessary to acquire a new nonce, so we'll just do it for the sake of the example. New-ACMENonce $state -PassThru; # Create the identifier for the DNS name $identifier = New-ACMEIdentifier $domain; # Create the order object at the ACME service. $order = New-ACMEOrder $state -Identifiers $identifier; # Fetch the authorizations for that order $authZ = Get-ACMEAuthorization -State $state -Order $order; # Select a challenge to fullfill $challenge = Get-ACMEChallenge $state $authZ "http-01"; # Inspect the challenge data $challenge.Data; # Create the file requested by the challenge $fileName = $tempFolderPath + '\' + $challenge.Token; Set-Content -Path $fileName -Value $challenge.Data.Content -NoNewline; $blobName = ".well-known/acme-challenge/" + $challenge.Token $storageAccount = Get-AzStorageAccount -ResourceGroupName $StorageResourceGroupName -Name $StorageName $ctx = $storageAccount.Context Set-AzStorageBlobContent -File $fileName -Container "public" -Context $ctx -Blob $blobName # Signal the ACME server that the challenge is ready $challenge | Complete-ACMEChallenge $state; # Wait a little bit and update the order, until we see the states while ($order.Status -notin ("ready", "invalid")) { Start-Sleep -Seconds 10; $order | Update-ACMEOrder $state -PassThru; } # We should have a valid order now and should be able to complete it # Therefore we need a certificate key $certKey = New-ACMECertificateKey -Path "$tempFolder\$domain.key.xml"; # Complete the order - this will issue a certificate singing request Complete-ACMEOrder $state -Order $order -CertificateKey $certKey; # Now we wait until the ACME service provides the certificate url while (-not $order.CertificateUrl) { Start-Sleep -Seconds 15 $order | Update-Order $state -PassThru } # As soon as the url shows up we can create the PFX $password = ConvertTo-SecureString -String "Passw@rd123***" -Force -AsPlainText Export-ACMECertificate $state -Order $order -CertificateKey $certKey -Path "$tempFolder\$domain.pfx" -Password $password; # Delete blob to check DNS Remove-AzStorageBlob -Container "public" -Context $ctx -Blob $blobName ### Upload new Certificate version to KeyVault Import-AzKeyVaultCertificate -VaultName $KeyVaultName -Name $AGOldCertName -FilePath "$tempFolder\$domain.pfx" -Password $password # Disable older certificate version Update-AzKeyVaultCertificate -VaultName $KeyVaultName -Name $AGOldCertName -Version $certificateOldVersion -Enable $false Write-Output "Older version ID: '$certificateOldVersion' for certificate $AGOldCertName is disabled" Write-Output "Completed renewing of the certificate: $AGOldCertName for $domain. New certificate version is uploaded." } } Write-Output "Completed" |
The syncronization between App Gateway and Key Vault occurs every 4 hours.
You can test the runbook on the Test pane, and passing the required parameters (domain name, email address used on LetsEncrypt, resource group names, storage account name, application gateway name and the name of the certificate you used when setting up the https listener). It takes around a minute to complete. When browsing the site again with https, you will notice that the certificate was updated correctly.
IMPORTANT: LetsEncrypt has its own weekly limits when issuing certificates for a specific domain in production (50 per week), so be aware when testing the PowerShell script.
5. Create an Azure Automation Schedule to renew the SSL certificate. In my case, I created a schedule for renewing it every 2 weeks
6. Setup the parameters to schedule the runbook with the schedule you created before.
And that’s it. You have successfully configured autorenewal of Letsencrypt certificates for Azure Application Gateway!
Good luck!