Problem:

Ideally, when you are setting up your Azure subscription, you are creating all your artifacts in code (a.k.a. Infrastructure as Code). While most resources and their sub resources can be defined in ARM template, keys for an Azure Key Vault cannot. Why is this important? To deploy Azure Virtual Machines with the Azure Disk Encryption extension, you’ll need a key encryption key, KEK, in a Key Vault.

Solution:

Using PowerShell, you can deploy my ARM template containing all required components to create a KEK. The template is a subscription based deployment with a nested resource group deployment. Here are the steps to deploy my ARM template:

  1. Download the following files from my Github repo:
    • Deploy-AzureSubscription.ps1
    • subscription.json
  2. Open PowerShell.
  3. Ensure the AZ module is installed.
Get-Module -ListAvailable
  1. Change your working directory to the folder where you downloaded the script.
  2. Connect to Azure:
Connect-AzAccount
  1. Set the context to target the appropriate subscription. For example:
Set-AzContext -Subscription 'Visual Studio Enterprise Subscription'
  1. Call the script using all the parameters. See below for an example:
.\Deploy-AzureSubscription.ps1 -Location eastus -ResourceGroup rg-shared-d-eastus

Explanation:

A newer Azure resource type called a “deployment script” will allow you to create the KEK. However, there are some additional requirements to using a deployment script. Let’s walk through those requirements defined in my ARM templates:

  1. User Assigned Identity: A deployment script resource requires a User Assigned Identity to create a storage account and a container needed to run the script. Also, the identity will need to be added to the Key Vault Access Policy to add the KEK. Here’s an example of the User Assigned Identity resource:
{
    "comments": "---------- USER ASSIGNED MANAGED IDENTITY ----------",
    "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
    "name": "uami-deploykek",
    "apiVersion": "2018-11-30",
    "location": "[resourceGroup().location]",
    "dependsOn": []
},
  1. Role Assignment: The deployment script cannot create the key in the Key Vault if the User Assigned Identity does not have the appropriate permissions to run the script. Here’s an example of the Role Assignment resource:
{
    
    "type": "Microsoft.Authorization/roleAssignments",
    "name": "[variables('guid')]",
    "apiVersion": "2020-04-01-preview",
    "dependsOn": [
        "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'uami-deploykek')]",
        "[resourceId('Microsoft.KeyVault/vaults', variables('keyVault'))]"
    ],
    "properties": {
        "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]",
        "principalId": "[reference('uami-deploykek').principalId]"
    }
},
  1. Key Vault: The key for the KEK must be stored in a Key Vault. Here’s an example of the Key Vault resource which includes the access policy:
{
    "comments": "---------- KEY VAULT ----------",
    "type": "Microsoft.KeyVault/vaults",
    "name": "[variables('keyVault')]",
    "apiVersion": "2016-10-01",
    "location": "[resourceGroup().location]",
    "tags": {},
    "dependsOn": [],
    "properties": {
        "tenantId": "[subscription().tenantId]",
        "sku": {
            "family": "A",
            "name": "Standard"
        },
        "accessPolicies": [
            {
                "tenantId": "[subscription().tenantId]",
                "objectId": "[parameters('UserObjectId')]",
                "permissions": {
                    "keys": [
                        "encrypt",
                        "decrypt",
                        "wrapKey",
                        "unwrapKey",
                        "sign",
                        "verify",
                        "get",
                        "list",
                        "create",
                        "update",
                        "import",
                        "delete",
                        "backup",
                        "restore",
                        "recover",
                        "purge"
                    ],
                    "secrets": [
                        "get",
                        "list",
                        "set",
                        "delete",
                        "backup",
                        "restore",
                        "recover",
                        "purge"
                    ],
                    "certificates": [
                        "get",
                        "list",
                        "delete",
                        "create",
                        "import",
                        "update",
                        "managecontacts",
                        "getissuers",
                        "listissuers",
                        "setissuers",
                        "deleteissuers",
                        "manageissuers",
                        "recover",
                        "purge"
                    ],
                    "storage": [
                        "get",
                        "list",
                        "delete",
                        "set",
                        "update",
                        "regeneratekey",
                        "recover",
                        "purge",
                        "backup",
                        "restore",
                        "setsas",
                        "listsas",
                        "getsas",
                        "deletesas"
                    ]
                }
            },
            {
                "tenantId": "[subscription().tenantId]",
                "objectId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'uami-deploykek'), '2018-11-30', 'Full').properties.principalId]",
                "permissions": {
                    "keys": [
                        "get",
                        "list",
                        "create"
                    ]
                }
            }
        ],
        "enabledForDeployment": true,
        "enabledForTemplateDeployment": true,
        "enabledForDiskEncryption": true
    }
},

Now that the requirements for the deployment script have been defined, we need to create the deployment script resource. Here’s an example of the deployment script resource:

{
    "comments": "---------- DEPLOYMENT SCRIPT > KEK ----------",
    "name": "ds-diskencryptionkek",
    "type": "Microsoft.Resources/deploymentScripts",
    "apiVersion": "2019-10-01-preview",
    "identity": {
        "type": "UserAssigned",
        "userAssignedIdentities": {
            "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities','uami-deploykek')]": {}
        }
    },
    "location": "[resourceGroup().location]",
    "kind": "AzurePowerShell",
    "tags": {},
    "dependsOn": [
        "uami-deploykek",
        "[variables('guid')]",
        "[variables('keyVault')]"
    ],
    "properties": {
        "azPowerShellVersion": "3.0.0",
        "cleanupPreference": "OnSuccess",
        "scriptContent": "
            param(
                [string] [Parameter(Mandatory=$true)] $KeyVault
            )
            
            if(!(Get-AzKeyVaultKey -Name DiskEncryption -VaultName $KeyVault))
            {
                Add-AzKeyVaultKey -Name DiskEncryption -VaultName $KeyVault -Destination Software
            }
            
            $KeyEncryptionKeyURL = (Get-AzKeyVaultKey -VaultName $KeyVault -Name 'DiskEncryption' -IncludeVersions | Where-Object {$_.Enabled -eq $true}).Id
            
            Write-Output $KeyEncryptionKeyURL
            
            $DeploymentScriptOutputs = @{}
            
            $DeploymentScriptOutputs['text'] = $KeyEncryptionKeyURL
            
            ",
        "arguments": "[format(' -KeyVault {0}', variables('keyVault'))]",
        "forceUpdateTag": "[parameters('Timestamp')]",
        "retentionInterval": "P1D",
        "timeout": "PT30M"
    }
},

References: