Easily Encrypt your Azure VMs with KeyVault

There are a lot of technical guides out there that allow you to take advantage of the awesome, amazing, stupendous, incredible Azure VM encryption. However, they all assume a decently high degree of knowledge about lots of things. I'm really not a coder or a PowerShell ninja - in fact, I specifically specialize in sucking at them - but am getting better! I decided on Friday, however, to build a single PowerShell "run book" that goes from soup to nuts on how to encrypt your VM with no other pre-requisites other than you have an Azure Subscription and you know how to load Azure Powershell.

Before going further, you should also consider - if you don't know what is happening in the following run book, you should learn them, otherwise, don't do this to production VMs, wait for the wizard in a few (months?) hehe

I started my journey by reading this article and as an aside - mad props to the team for helping me along the way with a few "I'm stuck" items which I will call out below.

What I want to do is take a VM in Azure, encrypt it with BitLocker whose keys are secured by certificate authentication whose keys are further protected by a key encryption key. At the end of this process, I will have a BitLocker encrypted VM (OS and Data) and the private key stuff will be protected in Azure KeyVault!

Pre-Requisites:

  1. A subscription in Azure
  2. The Azure AD Powershell module (which also requires the Sign On Assistant)
  3. The Azure Powershell module (1.0.1 +)
  4. A subscription admin that IS NOT multi-factor enabled - the MSONLINE module doesn't support that yet which is actually quite annoying.

Steps:

1. Open a PowerShell as Admin - I use ISE cause it is easier to move around but as is your preference.

2. Setup your environment:
ipmo azure
ipmo msoline
$msolcred = get-credential
connect-msolservice -credential $msolcred
add-azurermaccount -subscriptionid <subscriptionid>

3. Set up some variables, create a resource group and deploy a VM with ARM (I'm doing this with a clean VM to play with which you should do too if you are just starting out, otherwise, replace variables below with your own stuff).

$KeyVaultName = 'kv-azurekvtesting'
$Location = 'westus'
$RGName = 'rg-azurekvtesting'
$deploymentName = 'dn-azurekvtesting'
$templateURI="https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-windows/azuredeploy.json"`

If you want more control over how this VM gets created in the resource group (like names and such), you'll want to do a parameter file. Me, I'm lazy and just need a stinking VM to play with so I take the defaults.

New-AzureRmResourceGroup -Name $RGName -Location $Location
New-AzureRmResourceGroupDeployment -Name $deploymentName -ResourceGroupName $rgname -templateUri $templateURI`

This will ask for a username/password and server name, make something up but remember them!
This takes a while and isn't as verbose as I wish it would be.
Also, note, this is a simple VM with a data disk that I am using for the example. In real life, these VMs would already exist in their own Resource Groups (assuming you've converted from ASM to ARM). You DID convert from ASM to ARM didn't you cause none of this works in ASM/Old Azure mode?
If you want to know more about the above deployment magic, get to reading.

4. Set up some additional variables - note that this get could get more than one, should add a better VM finder here but I'm in a hurry cause it's almost New Years!
$vm = get-azurermvm -ResourceGroupName $rgname
$vmName = $vm.Name`

5. Generate a certificate - yah [for PowerShell in Win10](https://technet.microsoft.com/en-us/library/hh848633.aspx) - no more "certutil!"
$fileName = "C:\mypfx.pfx"
$cert = new-selfsignedcertificate -type custom -subject "CN=Just a Test VM Cert" -keyalgorithm RSA -keylength 2048 -notafter (get-date).AddMonths(36) -certstorelocation "cert:\localmachine\my"
==See note below, use this line if you are getting an existing cert --> $cert = Get-PfxCertificate -FilePath C:\mypfx.pfx ==

Note the cert is now in the cert store of your local computer.
==Update: At present, the above command doesn't work very well for generating the correct self signed cert. This stumped me for several hours as it kept returning an error on encryption: Set-AzureRmVMDiskEncryptionExtension : InvalidParameter: Failed to configure bitlocker as expected. Exception: Invalid
provider type specified. Fixed this by using IIS to generate a self signed cert and exporting it as a PFX and a password.==

6. Create an Azure AD application and a service principal (kind of like a user that will have access to the certificate) which will eventually go into the KeyVault. More info here
$azureADApplication = New-AzureRMADApplication -DisplayName $vmname -homepage 'https://thiscanbeanything' -IdentifierUris 'https://thisneedstobegloballyunique'
$aadClientID = $azureadapplication.applicationid
$serviceprincipal = new-azurermadserviceprincipal -ApplicationId $aadClientID`

If you are curious, Azure AD Applications are (at the time of this writing), still stuck in the old portal at https://manage.windowsazure.com.

7. Extract that cert so we can put it into the KeyVault AND deploy to the VM. We already have most of this in the variables, but KeyVault has to have a properly formatted (serialized) PFX in JSON format, so instead of getting crazy with Byte Arrays, just save it out.
$bincert = $cert.getrawcertdata()
$credvalue = [system.convert]::ToBase64String($bincert)
$thumbprint = $cert.thumbprint
$plaintextPassword = '!password1' #A plaintext password for the pfx file, you should make something better
$mypwd = ConvertTo-SecureString -String $plaintextPassword -Force –AsPlainText
==Use this if you are creating your cert with PowerShell, otherwise, this file is already there if you exported an existing cert: Get-ChildItem -Path cert:\localMachine\my\$thumbprint | Export-PfxCertificate -FilePath $fileName -Password $mypwd ==
$fileContentBytes = get-content $fileName -Encoding Byte
$fileContentEncoded = [System.Convert]::ToBase64String($fileContentBytes)
We are putting this JSON object into the KeyVault as a secret. Note that currently, only PFX's are supported like this:
$jsonObject = @"
{
"data": "$filecontentencoded",
"dataType" :"pfx",
"password": "$plaintextPassword"
}
"@
Next we byte out the object, then covert it to base 64, that's what actually goes into the KeyVault:

$jsonObjectBytes = [System.Text.Encoding]::UTF8.GetBytes($jsonObject)
$jsonEncoded = [System.Convert]::ToBase64String($jsonObjectBytes)
I'll put this into the KeyVault in a second, but need to create it and a SPN Credential first - sourcing from here

8. Add the certificate to the service principal - this is for authentication - sourced from here.
By the way, I'm a bit annoyed here because the entire script, minus this one line, uses Azure Powershell 1.0.1+ - this ONE line requires the Azure AD Powershell. There is some overlap from the line above where I create the service principal - it is a New-AzureRMADServicePrincipal. There is also a New-MSOLServicePrincipal. Hopefully the next version of Azure Powershell will include a "New-AzureRMADServicePrincipleCredential" so I don't need this whole extra step.
New-MsolServicePrincipalCredential -ServicePrincipalName $serviceprincipal.ServicePrincipalName -Type asymmetric -Value $credValue

9. Create a KeyVault:

new-azurermkeyvault -vaultname $keyVaultName -location $location -ResourceGroupName $rgname

10. Grant permissions to the AAD service principal to access the KeyVault and enable the KeyVault to be used for Encryption and Deployment:

set-azurermkeyvaultaccesspolicy -vaultname $keyvaultname -resourcegroup $rgname -enabledfordiskencryption
set-azurermkeyvaultaccesspolicy -vaultname $keyvaultname -resourcegroup $rgname -enabledfordeployment
set-azurermkeyvaultaccesspolicy -vaultname $keyvaultname -serviceprincipalname $aadclientid -permissionstokeys all -permissionstosecrets all -resourcegroupname $rgname

The above command is a bit insecure - idealy, you would want to do least truest privs on the SPN like get, read, encrypt, wrap - but for our purposes should be fine.

11. Validate the KeyVault looks correct and set a few more parameters (notice my service principal is now shown):
$keyvault = get-azurermkeyvault -vaultname $keyvaultname
$diskencryptionvaulturl = $keyvault.vaulturi
$keyvaultresourceid = $keyvault.resourceid

12. Set up the Key Encryption Key for the keyvault - think of this as the encrypting of the encryption key for double secret probation:

$kekname = 'keyencryptionkey'
$kek = add-azurekeyvaultkey -vaultname $keyvaultname -name $kekname -destination 'software'
$KeyEncryptionKeyUrl = $kek.key.kid

13. Put the cert into the KeyVault:

$secret = ConvertTo-SecureString -String $jsonEncoded -AsPlainText –Force
Set-AzureKeyVaultSecret -VaultName $keyVaultName -Name $vmName -SecretValue $secret
$encryptionCertURI = (get-azurekeyvaultsecret -vaultname $keyVaultName -Name $vmname).Id

14. Deploy the cert to the VM - sourced from here and here:

add-azurermvmsecret -VM $vm -sourcevaultid $keyvault.resourceid -certificatestore 'My' -CertificateURL $encryptionCertURI
update-azurermvm -resourcegroupname $rgname -vm $vm
This takes a few minutes, but when done, we can verify our cert is now on the VM (which is just too cool) by opening up MMC within the VM and looking for our cert we created at home.

15. Encrypt the VM - FINALLY! This is sourced from the original article at the top but I needed to muck with it some so I used this MSDN article for the command:
Set-AzureRmVMDiskEncryptionExtension -ResourceGroupName $rgname -VMName $vmname -AadClientID $aadClientID -AadClientCertThumbprint $Thumbprint -DiskEncryptionKeyVaultUrl $diskencryptionvaulturl -DiskEncryptionKeyVaultId $keyvault.resourceid -KeyEncryptionKeyUrl $keyEncryptionKeyUrl -KeyEncryptionKeyVaultId $keyvault.resourceid
This can take a very long while, get a cookie.
This will also restart the VM - if you are logged in watching (awesome nerd fun), you will see the Bitlocker KeyVault extension get installed and setup.

16. Validate you are now encrypted
get-azurermvmdiskencryptionstatus -resourcegroup $rgname -vmname $vmname`

Future things:

1. What if I add a data disk to this VM later? It won't auto encrypt - you'll need to add a sequence to the Set-AzureRmVMDiskEncryptionExtension and re-run:
$sequenceVersion = [Guid]::NewGuid();
Set-AzureRmVMDiskEncryptionExtension -ResourceGroupName $rgname -VMName $vmname -AadClientID $aadClientID -AadClientCertThumbprint $Thumbprint -DiskEncryptionKeyVaultUrl $diskencryptionvaulturl -DiskEncryptionKeyVaultId $keyvault.resourceid -KeyEncryptionKeyUrl $keyEncryptionKeyUrl -KeyEncryptionKeyVaultId $keyvault.resourceid -SequenceVersion $sequenceVersion

2. How can I tell where all my encrypted VMs are?

$osVolEncrypted = {(Get-AzureRmVMDiskEncryptionStatus -ResourceGroupName $_.ResourceGroupName -VMName $_.Name).OsVolumeEncrypted}
$dataVolEncrypted= {(Get-AzureRmVMDiskEncryptionStatus -ResourceGroupName $_.ResourceGroupName -VMName $_.Name).DataVolumesEncrypted}
Get-AzureRmVm | Format-Table @{Label="MachineName"; Expression={$_.Name}}, @{Label="OsVolumeEncrypted"; Expression=$osVolEncrypted}, @{Label="DataVolumesEncrypted"; Expression=$dataVolEncrypted}

3. Where are all those secrets located? (From same article above item 2)

Get-AzureKeyVaultSecret -VaultName $KeyVaultName | where {$_.Tags.ContainsKey('DiskEncryptionKeyFileName')} | format-table @{Label="MachineName"; Expression={$_.Tags['MachineName']}}, @{Label="VolumeLetter"; Expression={$_.Tags['VolumeLetter']}}, @{Label="EncryptionKeyURL"; Expression={$_.Id}}

Get the Whole Script Here! derekmartin.org