Deploy a Marketplace Linux VM with disk encryption using ARM templates

Protect your data at rest with disk encryption on Linux VMs and deploying them as Infrastructure as Code.

disk-encryption-vm-on-azure
VM with disk encryption


In this blog post  I’ll describe the steps to take for creating a Marketplace Linux VM with key encryption key (kek) disk encryption (dm-crypt) using ARM templates as much as possible. To do this we will follow the following steps:

  • Create Key Vault ARM template
  • Create a Service Principle for VM access to the Key Vault
  • Deploy the Key Vault
  • Add Key to Key Vault
  • Deploy the Marketplace VM using ARM templates

As a result you will have a VM with encrypted disks. The encryption keys/secrets are stored in the Azure Key Vault.

To execute the steps in this tutorial you are expected to have a basic knowledge of Powershell and execution of ARM (linked) templates on Azure.

Step 1: Create Key Vault ARM template
To deploy the Key Vault I use an ARM template for deployment. The template has 3 parameters: keyVaultName, tenantId, objectId. The tenantId is the tenant id of current subscription. The objectId is the AAD user or Service Principle that will access the Key Vault. The below Key Vault is only enabled for disk encryption:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "keyVaultName": {
      "type": "string",
      "metadata": {
        "description": "Name of the vault"
      }
    },
    "tenantId": {
      "type": "string",
      "metadata": {
        "description": "Tenant Id for the subscription and use assigned access to the vault. Available from the Get-AzureRMSubscription PowerShell cmdlet"
      }
    },
    "objectId": {
      "type": "string",
      "metadata": {
        "description": "Object Id of the AAD user or service principal that will have access to the vault. Available from the Get-AzureRMADUser or the Get-AzureRMADServicePrincipal cmdlets"
      }
    }
  },
  "variables":{
     "skuFamily": "A",
     "skuName": "Standard",
     "keysPermissions":"wrapKey",
     "secretsPermissions":"Set"
  },
  "resources": [
    {
      "type": "Microsoft.KeyVault/vaults",
      "name": "[parameters('keyVaultName')]",
      "apiVersion": "2015-06-01",
      "location": "[resourceGroup().location]",
      "properties": {
        "enabledForDeployment": false,
        "enabledForTemplateDeployment": false,
        "enabledForVolumeEncryption": true,
        "tenantId": "[parameters('tenantId')]",
        "accessPolicies": [
          {
            "tenantId": "[parameters('tenantId')]",
            "objectId": "[parameters('objectId')]",
            "permissions": {
              "keys": "[variables('keysPermissions')]",
              "secrets": "[variables('secretsPermissions')]"
            }
          }
        ],
        "sku": {
          "name": "[variables('skuName')]",
          "family": "[variables('skuFamily')]"
        }
      },
       "tags": {
        "displayName": "KeyVault"

      }
    }
  ]
}

In this template the objectId (Service Principle) is given wrapKey permissions on the key and Set permissions on the secrets.

Get your tenantId by running get-azurermsubscription in your azure powershell:

PS C:\***\Microsoft.Compute> Get-AzureRmSubscription

SubscriptionName : ****
SubscriptionId   : ****
TenantId         : ****
State            : Enabled

Step 2: Create a Service Principle for VM access to the Key Vault
An ADApplication and Service Principle can be created with the following powershell:

PS C:\***\Microsoft.Compute> $aadClientSecret = "<your secret>"
PS C:\***\Microsoft.Compute> $azureAdApplication = New-AzureRmADApplication -DisplayName "<displayname>" -HomePage "<homepage>" -IdentifierUris "<IdentifierUris>" -Password $aadClientSecret
PS C:\***\Microsoft.Compute> $azureAdApplication.ApplicationId
PS C:\***\Microsoft.Compute> $servicePrincipal = New-AzureRmADServicePrincipal –ApplicationId $azureAdApplication.ApplicationId
PS C:\***\Microsoft.Compute> $servicePrincipal

Guid (AADClientId)
----
******

DisplayName                    Type                           ObjectId
-----------                    ----                           --------
<displayname>                  ServicePrincipal               *****

You will need the AADClientId and the ObjectId later in the process.

Step 3: Deploy Key Vault
Now you can deploy the ARM template with the parameters with the following Powershell command: New-AzureRMResourceGroupDeployment. After deployment you have a Key Vault with a Service Principle which is able to set the key for disk encryption in the Key Vault.

New-AzureRMResourceGroupDeployment -Name "<deploymentname>" -ResourceGroupName "<kvresourcegroup>" -Mode "Incremental" -TemplateFile "myktemplate.json" -KeyVaultName <my kv name> -tenantId <tenantId> -objectId <objectId> -Verbose

Step 4: Add Key to Key Vault
You can add a new key to your Key Vault using the following powershell. The KeyEncryptionKeyUrl is needed as parameter for the ARM template that will deploy the VM.

PS C:\***\Microsoft.Compute> $kekname = 'keyencryptionkey'
PS C:\***\Microsoft.Compute> $kek = add-azurekeyvaultkey -vaultname <kvname> -name $kekname -destination 'software'
PS C:\***\Microsoft.Compute> $KeyEncryptionKeyUrl = $kek.key.kid
PS C:\***\Microsoft.Compute> $KeyEncryptionKeyUrl
https://<kvname>.vault.azure.net/keys/keyencryptionkey/****

For using the key you need to check if EnabledForDiskEncryption is enabled. At this moment the ARM template does not enable it. To enable EnabledForDiskEncryption run:

PS C:\***\Microsoft.Compute> Set-AzureRmKeyVaultAccessPolicy -EnabledForDiskEncryption -VaultName peterpeter

Step 5: Create the Marketplace VM with disk encryption
In this step we take a Marketplace VM and add disk encryption to the template. Currently disk encryption is supported on the following Linux distributions – RHEL 7.2, CentOS 7.2, Ubuntu 16.04. In this case we take a RHEL 7.2.

The ARM template first creates a Marketplace RHEL 7.2 VM, then apply the disk encryption extension and finally enable the disk encryption with a linked template on the VM.

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "aadClientID": {
      "metadata": {
        "description": "Client ID of AAD app which has permissions to KeyVault"
      },
      "type": "string"
    },
    "aadClientSecret": {
      "metadata": {
        "description": "Client Secret of AAD app which has permissions to KeyVault"
      },
      "type": "securestring"
    },
    "diskFormatQuery": {
      "defaultValue": "",
      "metadata": {
        "description": "the query string used to identify the disks to format and encrypt. This parameter only works when you set the EncryptionOperation as EnableEncryptionFormat. For example, passing [{\"dev_path\":\"/dev/md0\",\"name\":\"encryptedraid\",\"file_system\":\"ext4\"}] will format /dev/md0, encrypt it and mount it at /mnt/dataraid. This parameter should only be used for RAID devices. The specified device must not have any existing filesystem on it."
      },
      "type": "string"
    },
    "encryptionOperation": {
      "allowedValues": [ "EnableEncryption", "EnableEncryptionFormat" ],
      "defaultValue": "EnableEncryption",
      "metadata": {
        "description": "EnableEncryption would encrypt the disks in place and EnableEncryptionFormat would format the disks directly"
      },
      "type": "string"
    },
    "volumeType": {
      "allowedValues": [ "OS", "Data", "All" ],
      "defaultValue": "All",
      "metadata": {
        "description": "Defines which drives should be encrypted. OS encryption is supported on RHEL 7.2, CentOS 7.2 &amp;amp;amp;amp; Ubuntu 16.04."
      },
      "type": "string"
    },
    "keyEncryptionKeyURL": {
      "defaultValue": "",
      "metadata": {
        "description": "URL of the KeyEncryptionKey used to encrypt the volume encryption key"
      },
      "type": "string"
    },
    "keyVaultName": {
      "type": "string",
      "metadata": {
        "description": "Name of the KeyVault to place the volume encryption key"
      }
    },
    "keyVaultResourceGroup": {
      "type": "string",
      "metadata": {
        "description": "Resource group of the KeyVault"
      }
    },
    "passphrase": {
      "defaultValue": "",
      "metadata": {
        "description": "The passphrase for the disks"
      },
      "type": "securestring"
    },
    "sequenceVersion": {
      "defaultValue": "1",
      "metadata": {
        "description": "sequence version of the bitlocker operation. Increment this everytime an operation is performed on the same VM"
      },
      "type": "string"
    },
    "_linkedTemplateDiskEncriptionUri":{
       "defaultvalue":"https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/201-encrypt-running-linux-vm/updatevm-kek.json",
       "type":"string",
        "metadata": {
            "description": "location of the linked template"
        }
    },
    "vmname":{
        "type":"string",
        "metadata": {
            "description": "Name of the VM"
      }
    }
  },
  "variables": {
    "adminUsername":"adminUsername",
    "adminPassword":"setagoodpassword1234Q!W@U*Y^TGTY",
    "storageAccountName": "[concat(uniquestring(parameters('vmname')), 'storage')]",
    "dataDisk1VhdName": "[concat(uniquestring(parameters('vmname')), 'datadisk1')]",
    "dataDisk2VhdName": "[concat(uniquestring(parameters('vmname')), 'datadisk2')]",
    "imagePublisher": "RedHat",
    "imageOffer": "RHEL",
    "OSDiskName": "[concat(uniquestring(parameters('vmname')), 'osdisk')]",
    "nicName": "[concat(uniquestring(parameters('vmname')), 'nic')]",
    "addressPrefix": "10.0.0.0/16",
    "subnetName": "Subnet",
    "subnetPrefix": "10.0.0.0/24",
    "storageAccountType": "Standard_LRS",
    "publicIPAddressName": "[concat(uniquestring(parameters('vmname')), 'publicip')]",
    "publicIPAddressType": "Dynamic",
    "vmStorageAccountContainerName": "vhds",
    "vmSize": "Standard_DS2",
    "virtualNetworkName": "[concat(uniquestring(parameters('vmname')), 'vnet')]",
    "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]",
    "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
    "extensionName": "AzureDiskEncryptionForLinux",
    "extensionVersion": "0.1",
    "keyEncryptionAlgorithm": "RSA-OAEP",
    "keyVaultURL": "[concat('https://', parameters('keyVaultName'), '.vault.azure.net/')]",
    "keyVaultResourceID": "[concat(subscription().id,'/resourceGroups/',parameters('keyVaultResourceGroup'),'/providers/Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]",
    "enableDiskEncriptionLinkedTemplateUri":"[parameters('_linkedTemplateDiskEncriptionUri')]"
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "name": "[variables('storageAccountName')]",
      "apiVersion": "2015-06-15",
      "location": "[resourceGroup().location]",
      "properties": {
        "accountType": "[variables('storageAccountType')]"
      }
    },
    {
      "apiVersion": "2015-06-15",
      "type": "Microsoft.Network/publicIPAddresses",
      "name": "[variables('publicIPAddressName')]",
      "location": "[resourceGroup().location]",
      "properties": {
        "publicIPAllocationMethod": "[variables('publicIPAddressType')]"
      }
    },
    {
      "apiVersion": "2015-06-15",
      "type": "Microsoft.Network/virtualNetworks",
      "name": "[variables('virtualNetworkName')]",
      "location": "[resourceGroup().location]",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "[variables('addressPrefix')]"
          ]
        },
        "subnets": [
          {
            "name": "[variables('subnetName')]",
            "properties": {
              "addressPrefix": "[variables('subnetPrefix')]"
            }
          }
        ]
      }
    },
    {
      "apiVersion": "2015-06-15",
      "type": "Microsoft.Network/networkInterfaces",
      "name": "[variables('nicName')]",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
        "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
      ],
      "properties": {
        "ipConfigurations": [
          {
            "name": "ipconfig1",
            "properties": {
              "privateIPAllocationMethod": "Dynamic",
              "publicIPAddress": {
                "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]"
              },
              "subnet": {
                "id": "[variables('subnetRef')]"
              }
            }
          }
        ]
      }
    },
    {
      "apiVersion": "2015-06-15",
      "type": "Microsoft.Compute/virtualMachines",
      "name": "[parameters('vmname')]",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
        "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
      ],
      "properties": {
        "hardwareProfile": {
          "vmSize": "[variables('vmSize')]"
        },
        "osProfile": {
          "computerName": "[parameters('vmname')]",
          "adminUsername": "[variables('adminUsername')]",
          "adminPassword": "[variables('adminPassword')]"
        },
        "storageProfile": {
          "imageReference": {
            "publisher": "[variables('imagePublisher')]",
            "offer": "[variables('imageOffer')]",
            "sku": "7.2",
            "version": "latest"
          },
          "osDisk": {
            "name": "osdisk",
            "vhd": {
              "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2015-06-15').primaryEndpoints.blob, variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]"
            },
            "caching": "ReadWrite",
            "createOption": "FromImage"
          },
          "dataDisks": [
            {
              "name": "datadisk1",
              "diskSizeGB": "100",
              "lun": 0,
              "vhd": {
                "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2015-06-15').primaryEndpoints.blob, variables('vmStorageAccountContainerName'),'/',variables('dataDisk1VhdName'),'.vhd')]"
              },
              "createOption": "Empty"
            },
            {
              "name": "datadisk2",
              "diskSizeGB": "100",
              "lun": 1,
              "vhd": {
                "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2015-06-15').primaryEndpoints.blob, variables('vmStorageAccountContainerName'),'/',variables('dataDisk2VhdName'),'.vhd')]"
              },
              "createOption": "Empty"
            }
          ]
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
            }
          ]
        },
        "diagnosticsProfile": {
          "bootDiagnostics": {
            "enabled": "true",
            "storageUri": "[reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2015-06-15').primaryEndpoints.blob]"
          }
        }
      }
    },
     {
      "type": "Microsoft.Compute/virtualMachines/extensions",
      "name": "[concat(parameters('vmname'),'/', variables('extensionName'))]",
      "apiVersion": "2015-06-15",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/', parameters('vmname'))]"
      ],
        "properties": {
        "protectedSettings": {
          "AADClientSecret": "[parameters('aadClientSecret')]",
          "Passphrase": "[parameters('passphrase')]"
        },
        "publisher": "Microsoft.Azure.Security",
        "settings": {
          "AADClientID": "[parameters('aadClientID')]",
          "DiskFormatQuery": "[parameters('diskFormatQuery')]",
          "EncryptionOperation": "[parameters('encryptionOperation')]",
          "KeyEncryptionAlgorithm": "[variables('keyEncryptionAlgorithm')]",
          "KeyEncryptionKeyURL": "[parameters('keyEncryptionKeyURL')]",
          "KeyVaultURL": "[variables('keyVaultURL')]",
          "SequenceVersion": "[parameters('sequenceVersion')]",
          "VolumeType": "[parameters('volumeType')]"
        },
        "type": "AzureDiskEncryptionForLinux",
        "typeHandlerVersion": "[variables('extensionVersion')]"
      }
    },
     {
        "apiVersion": "2015-01-01",
      "dependsOn": [
        "[resourceId('Microsoft.Compute/virtualMachines/extensions',  parameters('vmname'), variables('extensionName'))]"
      ],
      "name": "[concat(parameters('vmname'), 'updateVm')]",
      "type": "Microsoft.Resources/deployments",
      "properties": {
        "mode": "Incremental",
        "parameters": {
          "keyEncryptionKeyURL": {
            "value": "[parameters('keyEncryptionKeyURL')]"
          },
          "keyVaultResourceID": {
            "value": "[variables('keyVaultResourceID')]"
          },
          "keyVaultSecretUrl": {
            "value": "[reference(resourceId('Microsoft.Compute/virtualMachines/extensions',  parameters('vmname'), variables('extensionName'))).instanceView.statuses[0].message]"
          },
          "vmName": {
            "value": "[parameters('vmname')]"
          }
        },
        "templateLink": {
          "contentVersion": "1.0.0.0",
          "uri": "[variables('enableDiskEncriptionLinkedTemplateUri')]",
        }
      }
    }
  ]
}
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "vmName": {
            "type": "string",
            "metadata": {
                "description": "Name of the Virtual Machine"
            }
        },
        "keyVaultResourceID": {
            "type": "string",
            "metadata": {
                "description": "KeyVault resource id. Ex: /subscriptions/9135e259-1f76-4dbd-a5c8-bc4fcdf3cf1c/resourceGroups/DiskEncryptionTest/providers/Microsoft.KeyVault/vaults/DiskEncryptionTestAus"
            }
        },
        "keyVaultSecretUrl": {
            "type": "string",
            "metadata": {
                "description": "KeyVault secret Url. Ex: https://diskencryptiontestaus.vault.azure.net/secrets/BitLockerEncryptionSecretWithKek/e088818e865e48488cf363af16dea596"
            }
        },
        "keyEncryptionKeyURL": {
            "type": "string",
            "defaultValue": "",
            "metadata": {
                "description": "KeyVault key encryption key Url. Ex: https://diskencryptiontestaus.vault.azure.net/keys/DiskEncryptionKek/562a4bb76b524a1493a6afe8e536ee78"
            }
        }
    },
    "resources": [
        {
            "apiVersion": "2015-06-15",
            "type": "Microsoft.Compute/virtualMachines",
            "name": "[parameters('vmName')]",
            "location": "[resourceGroup().location]",
            "properties": {
                "storageProfile": {
                    "osDisk": {
                        "encryptionSettings": {
                            "diskEncryptionKey": {
                                "sourceVault": {
                                    "id": "[parameters('keyVaultResourceID')]"
                                },
                                "secretUrl": "[parameters('keyVaultSecretUrl')]"
                            },
                            "keyEncryptionKey": {
                                "sourceVault": {
                                    "id": "[parameters('keyVaultResourceID')]"
                                },
                                "keyUrl": "[parameters('keyEncryptionKeyURL')]"
                            }
                        }
                    }
                }
            }
        }
    ]
}

You have to deploy the linked template somewhere available on the internet. The template can now be executed:

PS C:\***\Microsoft.Compute > New-AzureRMResourceGroupDeployment -Name "computeTest-13234" `
                               -ResourceGroupName <resource group name >  `
                               -Mode "Incremental" `
                               -TemplateFile testrhelencryption.json `
                               -aadClientID "<aadClientId>"`
                               -aadClientSecret (ConvertTo-SecureString "<aadClientSecret>" -AsPlainText -Force)`
                               -keyEncryptionKeyURL "<KeyEncryptionKeyUrl>"`
                               -keyVaultName "<key vault name>"`
                               -keyVaultResourceGroup "<key vault resourcegroup name>"`
                               -passphrase (ConvertTo-SecureString "<passphrase>" -AsPlainText -Force)`
                               -_linkedTemplateDiskEncriptionUri "<location linked template>"
                               -vmname "<vm name>"
                               -Verbose

Next you have to reboot the machine when the encryption is done. You can check for pending reboots with the following command: Get-AzureRmVMDiskEncryptionStatus. You can also see if the disk encryption was successful. A script to check if reboot is needed can be found here: Restarting Azure VMs after encrypting the disks.

Conclusion
When using this sample code you will be able to make a VM with kek disk encryption key. The keys are stored into the Key Vault. When using ARM templates in advantage of Powershell script it enables you to go for an Infrastructure as Code scenario, and manage VMs as cattle instead of pets.

The content is based on:
Quick start encrypt running linux vm
Whitepaper azure security disk encryption

9 thoughts on “Deploy a Marketplace Linux VM with disk encryption using ARM templates”

  1. Hi,

    I need to know what location I have to mention in the line below,

    -_linkedTemplateDiskEncriptionUri “”

    I am unable to encrypt a VM with KEK provisioned from market place.

    Like

    1. You can leave the line: -_linkedTemplateDiskEncriptionUri “” out of the script. Then the template will pick it up from the default location:
      “_linkedTemplateDiskEncriptionUri”:{
             “defaultvalue”:”https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/201-encrypt-running-linux-vm/updatevm-kek.json”,
             “type”:”string”,
              “metadata”: {
                  “description”: “location of the linked template”
              }
          },

      Like

      1. Thanks Peter. I have couple queries,

        1) Does both BEK and KEK encryption work on image gallery VMs or it works on captured image as well?
        2) If I already have storage and virtual network created, does this json template will overwrite those or it will skip creating the storage and virtual network.

        I am facing challenges encrypting Linux VMs with both BEK and KEK as it is the requirement for Azure backup.

        Like

      2. Yes you can do both BEK and KEK. Look at the parameters of the _linkedTemplateDiskEncryptionUri it’s template.
        When the storage and network is there, nothing is created. You can also remove them from the template and set the correct names.

        Like

      3. Thanks. Will it work for both image gallery images and captured images? Have you tested for both?

        Like

      4. Thanks. One last question. I have some VMs with just BEK encryption. How can I apply the KEK encryption on them without decrypting (Linux OS disk can’t be decrypted as per MS article). Do you have any json template for the same?

        Like

  2. Thank you Peter.
    How can i add a tag to a SecretKey with the Encryption extensions or maybe define that Sercret Name_{Guid}
    I want to tracert my sercrets per VM-Name

    I have tested with PS and it’s work fine with but not with .Json Template
    $tags = @{“DiskEncryptionKeyEncryptionAlgorithm” = “RSA-OAEP”; “DiskEncryptionKeyFileName” = “LinuxPassPhraseFileName” ; “VMName” = “VM-Demo-1”}
    …..
    Set-AzureKeyVaultSecret -VaultName $vault_Name -Name $secretName -SecretValue $secureSecretValue -tags $tags

    Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.