Automate certificate renewal in Azure Devops

I was given a task to automate SSL certificates renewal using Devops. We used Let’s Encrypt certificates in DEV & UAT environments for azure function and APIM. Those certificates are free and will expire after 90 days.

At first i created a release pipeline using LetsEncrypt certificate generator task.

This release pipeline runs fine and does renewal certificates, however the Schedule for classic release pipeline is not ideal. I cannot use the Schedule to automate the release pipeline to run every 90 days.

Then I created YAML pipeline with a cron scheduler and works perfectly. Below is my YAML pipeline:

trigger: none
schedules:
    - cron: "0 9 4 JAN,APR,JUL,OCT *"
      displayName: 'At 09:00 on 4th day of month in Every 3rd months, Starting from JAN'
      branches:
        include: 
        - master
      always: true

variables: 
   ad.TenentID: 'XXXXX-XXXXX-XXXXXX-XXXX-XXXXXX'
   ad.applicationID: 'XXXXX-XXXXX-XXXXXX-XXXX-XXXXXX'
   ad.vstsSpPassword: 'XXXXXXXXXXXXXXXXXXXXXXXXXX'   
   AzureRMSubscriptionDEV: 'XXXX Azure Dev/Test'
   domainDEV: '*.dev.XXXXX.net, dev.XXXXX.net'
   kvDEV: 'XXXXXXX'
   rgDEV: 'XXXXXXX'
   subscriptionIDDEV: 'XXXXX-XXXXX-XXXXXX-XXXX-XXXXXX'
   AzureRMSubscription: 'XXXXX Azure CSP'
   domainUAT: '*.uat.XXXXXX.net, uat.XXXXXX.net'
   kv: 'XXXXXXX'
   rg: 'XXXXXXX'
   subscriptionID: 'XXXXX-XXXXX-XXXXXX-XXXX-XXXXXX'
   domainProd: '*.XXXXX.net, XXXXXX.net'

stages:
- stage: DEV
  jobs:
  - job: Deploy_certificate_to_DEV
    pool:       
      vmImage: 'vs2017-win2016'     
    displayName: Create Certificate in DEV         
    steps: 
    - task: PowerShell@2
      inputs:
        targetType: 'inline'
        workingDirectory: $(System.DefaultWorkingDirectory)
        script: |
             Install-Module -Name Posh-ACME -Force -Verbose -Scope CurrentUser -MaximumVersion 3.5.0        
    - task: cboroson-VSTS-LetsEncrypt@1
      name: RunLetsEncrypt
      inputs: 
        ConnectedServiceName: $(AzureRMSubscriptionDEV)        
        Contact: devops@XXXXX.co.nz
        KeyVaultName: $(kvDEV)
        SubscriptionId: $(subscriptionIDDEV)
        TenantId: $(ad.TenentID)
        domain: $(domainDEV)
        password: $(ad.vstsSpPassword)
        resourceGroupName: $(rgDEV)
        secretFormat: certificate
        username: $(ad.applicationID)      

- stage: UAT
  jobs:
  - job: Deploy_certificate_to_UAT
    pool:       
      vmImage: 'vs2017-win2016'     
    displayName: Create Certificate in UAT         
    steps: 
    - task: PowerShell@2
      inputs:
        targetType: 'inline'
        workingDirectory: $(System.DefaultWorkingDirectory)
        script: |
             Install-Module -Name Posh-ACME -Force -Verbose -Scope CurrentUser -MaximumVersion 3.5.0        
    - task: cboroson-VSTS-LetsEncrypt@1
      name: RunLetsEncrypt
      inputs: 
        ConnectedServiceName: $(AzureRMSubscription)        
        Contact: devops@XXXXXX.co.nz
        KeyVaultName: $(kv)
        SubscriptionId: $(subscriptionID)
        TenantId: $(ad.TenentID)
        domain: $(domainUAT)
        password: $(ad.vstsSpPassword)
        resourceGroupName: $(rg)
        secretFormat: certificate
        username: $(ad.applicationID)

The certificates will be automatically renewed and in Azure Function, the custom domain binding automatically refers to the latest certificates. However i was struck by an issue in APIM instance that the certificate configured in APIM custom domain still refers to old version.

Below is the ARM template for APIM instance. I think the keyVaultId refers to secret URL with version number causing the issue. Even though the certificate is renewed but APIM binding didn’t refers to the new version.

"resources": [
    {
      "type": "Microsoft.ApiManagement/service",
      "apiVersion": "2019-01-01",
      "name": "[parameters('ApimServiceName')]",
      "location": "[parameters('api_management_location')]",
      "sku": {
        "name": "[parameters('api_management_pricing_tier')]",
        "capacity": 1
      },
      "identity": {
        "type": "SystemAssigned"
      },
      "properties": {
        "publisherEmail": "[parameters('api_management_admin_email')]",
        "publisherName": "Administrator",
        "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com",
        "hostnameConfigurations": [
          {
            "type": "Proxy",
            "hostName": "[parameters('api_custom_hostName')]",
            "keyVaultId": "[reference(variables('keyVaultResourceId'), '2018-02-14').secretUriWithVersion]",
            "defaultSslBinding": true
          },
          {
            "type": "Proxy",
            "hostName": "[parameters('api_management_hostName')]"
          },
          {
            "type": "DeveloperPortal",
            "hostName": "[parameters('api_custom_developerportal_hostName')]",
            "keyVaultId": "[reference(variables('keyVaultResourceId'), '2018-02-14').secretUriWithVersion]"
          } ],
        "virtualNetworkType": "None"
      }
    }
  ]

I updated the keyVaultId by just using the secret name without version. It should refers to latest version. Once i deployed the change and renew the certificate, the binding didn’t immediately refreshed. I’m not sure how long it would take. I checked after 5 hours and the binding in APIM is correctly referencing the latest version of certificate.

"keyVaultId": "[concat('https://',parameters('hostNamekeyVaultName'),'.vault.azure.net/secrets/',parameters('hostNamesecretName'),'/')]"

One thought on “Automate certificate renewal in Azure Devops

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s