Installing a certificate from Azure KeyVault into an Azure VM

Why?

A client may need to authenticate with a trusted endpoint before communication is allowed. This is called client authentication. Trust is established by both parties using the same certificate to secure the communication channel. The problem is providing access to the certificate to install on the clients and servers while limiting exposure of the certificate. This is why KeyVault does not provide a way to directly export certificates once they have been created or imported. The problem constraint will require strategies to limit exposure of secrets throughout the provisioning and maintenance lifecycle of clients and servers.

What?

Azure Powershell will be used to enable Azure’s trusted internal Microsoft.Compute resource provider to access KeyVault. After obtaining access the resource provider can use KeyVault to install certificates in a VM’s credential store during provisioning. It does this using settings specified in an Azure Resource Manager (ARM) template. Azure Powershell can also be used to update existing certificates in VM credential stores after provisioning. By leveraging Azure Powershell and ARM to manipulate the secret it is never exposed.

How?

The certificate must be imported into KeyVault as a secret with a particular JSON format.

Figure 1: Importing a PFX as a secret in KeyVault that can be used by ARM [1].
# Encode the PFX file as a base 64 string.
$fileContentBytes = Get-Content [path to PFX file] -Encoding Byte
$fileContentEncoded = [System.Convert]::ToBase64String($fileContentBytes)

# Create and encode the JSON object as a base 64 string.
$certPassword = [certificate password]

$jsonObject = @"
{
"data": "$fileContentEncoded",
"dataType": "pfx",
"password": "$certPassword"
}
"@

$jsonObjectBytes = [System.Text.Encoding]::UTF8.GetBytes($jsonObject)
$jsonEncoded = [System.Convert]::ToBase64String($jsonObjectBytes)

# Convert the JSON to a secure string so it can be imported as a secret.
$secret = ConvertTo-SecureString -String $jsonEncoded -AsPlainText -Force
Set-AzureKeyVaultSecret `
-VaultName [vault name] `
-Name [secret name] `
-SecretValue $secret

Set-AzureKeyVaultSecret imports the secret into KeyVault. The boilerplate beforehand is to transform the PFX file into a format that can be used by resource provider.

Figure 2: Snippet of ARM template for provisioning new Azure VM with secrets installed [2].
Template Parameters:
"vaultName": {
    "type": "string",
    "metadata": {
        "description": "Name of Key Vault that has a secret"
    }
},
"vaultResourceGroup": {
    "type": "string",
    "metadata": {
        "description": "Resource Group of Key Vault that has a secret"
    }
},
"secretUrlWithVersion": {
    "type": "string",
    "metadata": {
        "description": "Url of the certificate in Key Vault"
    }
}

VM Resource Properties:
"osProfile": {
    "secrets": [{
        "sourceVault": {
            "id": "[resourceId(parameters('vaultResourceGroup'), 'Microsoft.KeyVault/vaults', parameters('vaultName'))]"
        },
        "vaultCertificates": [{
            "certificateUrl": "[parameters('secretUrlWithVersion')]",
            "certificateStore": "My"
        }]
    }]
}

The ARM template assumes a secretUrlWithVersion exists in the KeyVault with the vaultName in the vaultResourceGroup. This is the secret that was imported in Figure 1.

The OS profile is defined in the VM resource’s properties section . The OS profile can be configured to install secrets from KeyVault into the VM’s LocalMachine certificate stores. Figure 2 shows the secret will be installed in the Cert:\LocalMachine\My certificate store. LocalMachine is always used, but the second part of the path can be changed.

Figure 3: Updating an existing VM with a new certificate from KeyVault [1].
# Install a secret stored in KeyVault into an Azure VM's credential store.
$resourceGroup = [resource group name]
$vm = Get-AzureRmVM -ResourceGroupName $resourceGroup -Name [VM name]
$vaultId = "/subscriptions/[subscription guid]/resourceGroups/$resourceGroup/providers/Microsoft.KeyVault/vaults/[vault name]"
$certStore = "My"
$certUrl = [KeyVault secret URL w/ version id]
$vm = Add-AzureRmVMSecret -VM $vm -SourceVaultId $vaultId -CertificateStore $certStore -CertificateUrl $certUrl

# Update the VM so the changes take effect.
Update-AzureRmVM -ResourceGroupName $resourceGroup -VM $vm

The Add-AzureRmVMSecret cmdlet enables installing a secret stored in KeyVault into an Azure VM’s credential store. It updates the OS profile configuration to add a sourceVault and certificateUrl in the certificateStore. Update-AzureRmVM must be called before changes to the VM resource’s configuration take effect.

Figure 4: Add-AzureRmVMSecret cmdlet syntax [3].
Add-AzureRmVMSecret [-VM] <PSVirtualMachine> `
[[-SourceVaultId] <String> ] `
[[-CertificateStore] <String> ] `
[[-CertificateUrl] <String> ] `
[ <CommonParameters>]

-CertificateUrl specifies the URL that points to a Key Vault secret which contains a certificate.
The certificate is the Base64 encoding of the following JavaScript Object Notation (JSON) object, which is encoded in UTF-8.

Figure 5: Format of JSON secret [3].
 {
 "data": "<Base64-encoded-file>",
 "dataType": "<file-format>",
 "password": "<pfx-file-password>"
 }

Currently, dataType accepts only .pfx files.

Note

The KeyVault must reside in the same geographical location as the VM. Calling Add-AzureRmVMSecret with a -SourceVaultId that is in a different geographical location and then calling Update-AzureRmVM throws a KeyVaultAndVMInDifferentRegions error code.

Figure 6: KeyVaultAndVMInDifferentRegions error code.
Update-AzureRmVm : Long running operation failed with status 'Failed'.
StartTime: 7/5/2016 1:29:03 PM
EndTime: 7/5/2016 1:29:04 PM
OperationID: 
Status: Failed
ErrorCode: KeyVaultAndVMInDifferentRegions
ErrorMessage: The Key Vault https://[vault name].vault.azure.net/secrets/[secret name]/[version] is located 
in location Central US, which is different from the location of the VM, westus. 
At line:1 char:1
+ Update-AzureRmVm -ResourceGroupName [resource group name] -VM $vm
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 + CategoryInfo : CloseError: (:) [Update-AzureRmVM], ComputeCloudException
 + FullyQualifiedErrorId : Microsoft.Azure.Commands.Compute.UpdateAzureVMCommand
Note

The VM must be stopped before calling Update-AzureRmVm otherwise a VMExtensionProvisioningError will be thrown.

Figure 7: VMExtensionProvisioningError when VM is not stopped.
PS> Update-AzureRmVM -ResourceGroupName [VM resource group name] -VM $vm

Status : Failed
StatusCode : OK
RequestId : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Output : 
Error : {
 "Details": [],
 "InnerError": null,
 "Code": "VMExtensionProvisioningError",
 "Message": "VM has reported a failure when processing extension 'customScriptArtifact'. Error message: \"Failed to download 
 all specified files. Exiting. Error Message: The remote server returned an error: (403) Forbidden.\".",
 "Target": null
 }
StartTime : 7/5/2016 7:16:52 PM -07:00
EndTime : 7/5/2016 7:16:53 PM -07:00
TrackingOperationId : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

PS> $vm = Get-AzureRmVM -ResourceGroupName [VM resource group name] -Name [VM name]

PS> Update-AzureRmVM -ResourceGroupName [VM resource group name] -VM $vm

Status : Succeeded
StatusCode : OK
RequestId : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Output : 
Error : 
StartTime : 7/5/2016 7:20:19 PM -07:00
EndTime : 7/5/2016 7:20:22 PM -07:00
TrackingOperationId : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

The operation can be validated by starting the VM, remote desktop into the VM, and checking the Cert:\LocalMachine\My certificate store contains the expected certificate.

Figure 8: Checking the Cert:\LocalMachine\My contains the expected certificate.
PS> Get-ChildItem Cert:\LocalMachine\My

Further Reading

Azure KeyVault must be created with the -EnabledForDeployment switch to grant the Microsoft.Compute resource provider access to KeyVault.

Setting up Azure KeyVault using Powershell

Create a self-signed certificate to test importing secrets into KeyVault.

Creating a self-signed certificate

Automate stopping and starting Azure VMs.

How to Start and Stop an Azure VM

Works Cited

  1. https://blogs.technet.microsoft.com/kv/2015/07/14/deploy-certificates-to-vms-from-customer-managed-key-vault/
  2. https://azure.microsoft.com/en-us/documentation/templates/201-vm-push-certificate-windows/
  3. https://msdn.microsoft.com/en-us/library/mt150069.aspx
  4. https://azure.microsoft.com/en-us/documentation/articles/service-fabric-secure-azure-cluster-with-certs/
  5. https://azure.microsoft.com/en-us/documentation/articles/service-fabric-cluster-security-update-certs-azure/
  6. https://github.com/ChackDan/Service-Fabric/tree/master/Scripts/ServiceFabricRPHelpers

Join the Conversation

2 Comments

Leave a comment

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

%d bloggers like this: