Using an ARM template to deploy your SSL certificate stored in KeyVault on an Web App

Everyone knows it’s completely normal nowadays to have your website loaded over https. Troy Hunt explains why. There are quite some examples out there on how to use Let’s Encrypt certificates on your Azure web app, see this one by Henry Been for example. For most of us that’s a perfect and free solutions. It doesn’t work for all of us however.

Sometimes you require additional verification while requesting a new certificate or you simple do not own the domain and therefor cannot or are not allowed to use Let’s Encrypt. Here’s how to add a custom SSL certificate to an Azure Web App stored in KeyVault.

Create the KeyVault with the required settings Here’s the ARM template to create a KeyVault with the required settings to be able to grab a certificate from it during a Web App deployment. There are two things we care about in this context. First, you need to enable ‘enabledForTemplateDeployment’. Second, we need to grand the Microsoft.Web resource provider access to the KeyVault to get the certificate. That’s the second resource in this file. That objectId is always the same for everyone.

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "keyVaultName": {
      "type": "string"
    },
    "AzureAdTenantId": {
      "type": "string"
    },
    "accessPolicies": {
      "defaultValue": {
        "list": []
      },
      "type": "object"
    },
    "location": {
      "type": "string"
    }
  },
  "variables": {},
  "resources": [
    {
      "type": "Microsoft.KeyVault/vaults",
      "apiVersion": "2016-10-01",
      "name": "[parameters('keyVaultName')]",
      "location": "[parameters('location')]",
      "properties": {
        "sku": {
          "family": "A",
          "name": "standard"
        },
        "tenantId": "[parameters('AzureAdTenantId')]",
        "enabledForDeployment": false,
        "enabledForDiskEncryption": false,
        "enabledForTemplateDeployment": true
      }
    },
    {
      "type": "Microsoft.KeyVault/vaults/accessPolicies",
      "name": "[concat(parameters('keyVaultName'), '/add')]",
      "apiVersion": "2018-02-14",
      "dependsOn": [
        "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]"
      ],
      "properties": {
        "accessPolicies": [
          {
            "comment": "Microsoft.Web resource provider",
            "tenantId": "[parameters('AzureAdTenantId')]",
            "objectId": "05b021cf-d75d-4fcb-8a32-9417dc419f94",
            "permissions": {
              "keys": [],
              "secrets": [
                "Get"
              ],
              "certificates": []
            }
          }
        ]
      }
    }
  ]
}

Store the certificate in KeyVault I use an Azure DevOps pipeline to run all the steps described in the remainder of this blog. The first three steps are about uploading the certificate into KeyVault. First, go to the secure files under Pipelines, Library and upload your certificate. Then, add a download secure file task to your deployment pipeline and select your certificate. We now need to extract a base64 string from the file and upload that to KeyVault as a secret. Add an PowerShell task and execute the following script.

$secName = “$(certificateName).pfx
$tempDirectory = $env:AGENT_TEMPDIRECTORY
 
$pfxFilePath = Join-Path $tempDirectory $secName
 
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$flag = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
 
$cert.Import($pfxFilePath, "$(certificatePassword)", $flag)
        
$bin = $cert.RawData
$base64Value = [System.Convert]::ToBase64String($bin)
 
Write-Host "##vso[task.setvariable variable=certificateBase64Content;]$base64Value"

This task saves the base64 value in an Azure DevOps variable that we will use in the next step. To upload the certificate to KeyVault I use an ARM Template as shown here. I use the Azure resource group deployment task to deploy the template with the following parameter override:

-keyVaultName $(keyVaultName) -certName $(certificate) -certValue "$(certificateBase64Content)" -location $(location)
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "keyVaultName": {
        "type": "string"
      },
      "certValue": {
        "type": "string"
      },
      "certName": {
        "type": "string"
      },
      "location": {
          "type": "string"
      }
    },
    "variables": {},
    "resources": [
      {
        "type": "Microsoft.KeyVault/vaults/secrets",
        "name": "[concat(parameters('keyVaultName'), '/', parameters('certName'))]",
        "location": "[parameters('location')]",
        "apiVersion": "2018-02-14",
        "properties": {
          "value": "[parameters('certValue')]",
          "contentType": "application/x-pkcs12"
        }
      }
    ]
  }

Deploy the certificate to your Web App The last step we need take is to deploy the Web App with it’s hostname binding and certificate. I use the following ARM template to do that with the following parameter overrides in the Azure resource group deployment task:

-webappName $(webappName) -domainName $(domainName) -serverfarmName $(serverfarmname) -keyVaultName $(keyVaultName) -certificateName $(certificate) -location $(location)
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "webappName": {
            "type": "string"
        },
        "domainName": {
            "type": "string"
        },
        "serverfarmName": {
            "type": "string"
        },
        "keyvaultName": {
            "type": "string"
        },
        "certificateName": {
            "type": "string"
        },
        "location": {
            "type": "string"
        }
    },
    "variables": { },
    "resources": [
        {
            "apiVersion": "2018-02-01",
            "name": "[parameters('webappname')]",
            "type": "Microsoft.Web/sites",
            "location": "[parameters('location')]",
            "properties": {
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('serverfarmName'))]"
            }
        },
        {
            "type": "Microsoft.Web/sites/hostNameBindings",
            "apiVersion": "2016-08-01",
            "name": "[concat(parameters('webappName'), '/', parameters('domainName'))]",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/certificates', parameters('certificateName'))]",
                 "[resourceId('Microsoft.Web', parameters('webappName'))]"
            ],
            "properties": {
                "siteName": "[parameters('webappName')]",
                "hostNameType": "Verified",
                "sslState": "SniEnabled",
                "thumbprint": "[reference(resourceId('Microsoft.Web/certificates', parameters('certificateName'))).Thumbprint]"
            }
        },
        {
            "type": "Microsoft.Web/certificates",
            "name": "[parameters('certificateName')]",
            "apiVersion": "2016-03-01",
            "location": "[parameters('location')]",
            "properties": {
                "keyVaultId": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName'))]",
                "keyVaultSecretName": "[parameters('certificateName')]",
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('serverfarmName'))]"
            }
        }
    ]
}

That’s it! If you now navigate to your app’s domain there will be a nice, green lock.