Azure Stack HCI / Failover Cluster Sync vTPM Certs for live migration

Azure Stack HCI / Failover Cluster Sync vTPM Certs for live migration

I hit a small hiccup when trying to use vTPM in our guest VMs on Azure Stack HCI, even though the nodes support TPM 2.0. Creating a VM and enabling TPM works, but after a live or quick migration the destination nodes can’t use the VM's vTPM. A German Sysadmin blogger ran into the same thing. Both of us didn't set up the Host Guardian Service, which is the preferred fix.

For me it's an inherited setup.

If you’re short on time and absolutely need TPM, you can synchronise the certificates manually - as Rachfal describes - or you can run my script that harvests all the certificates and redeploys them to all the nodes. That's only a reasonable workaround because the certificates are valid for ten years.

Still, if you’re starting from scratch, or have the time. Just setup the recommended Host Guardian Service configuration. Because this is an UntrustedGuardian setup, some might even call it an "Hack". For me it's a classic Knutsel!

<#
  Sync‑ShieldedVMCerts.ps1
  -------------------
  "Mesh"‑synchronise Shielded‑VM Encryption & Signing certs between Azure Stack HCI hosts
#>

# ----- VARIABLES ------------------------------------------------------------------------

$Nodes = @(
    # Replace or expand with your failovercluster / Azure Stack HCI / Hyper‑V node hostnames
    'HCIPROD01',
    'HCIPROD02',
    'HCIPROD03',
    'HCIPROD04',
    'HCIPROD05'

)

# The password that protects the PFX blobs.
$Password = ConvertTo-SecureString 'ChangeAndMakeMeRandomAndSecure!' -AsPlainText -Force

# Password prompt when you don't want to leak to terminal/logging (not even temp passwords ;).
# Uncomment this variable and comment out the one above here
#$Password = Read‑Host 'Temp PFX password (input hidden)' -AsSecureString
# ----------------------------------------------------------------------------------------


# Function to convert to plaintext, when prompt is used
function ConvertTo-PlainText {
    param([SecureString] $Secure)
    $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure)
    try   { [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) }
    finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) }
}
$pwdPlain  = ConvertTo-PlainText $Password
$storeName = 'Shielded VM Local Certificates'
$ErrorActionPreference = 'Stop'


# Harvest from given nodes
Write-Host "`n[Harvest]"
$mesh = @{}

foreach ($n in $Nodes) {
    Write-Host "  • $n"
    $harvest = Invoke-Command -ComputerName $n -ScriptBlock {
        param($store,$pwd)
        Get-ChildItem -Path "Cert:\LocalMachine\$store" | ForEach-Object {
            $bytes = $_.Export(
                [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx,
                $pwd
            )
            [PSCustomObject]@{
                Thumbprint = $_.Thumbprint
                PfxBase64  = [Convert]::ToBase64String($bytes)
            }
        }
    } -ArgumentList $storeName,$pwdPlain

    foreach ($item in $harvest) {
        if (-not $mesh.ContainsKey($item.Thumbprint)) {
            $mesh[$item.Thumbprint] = $item.PfxBase64
        }
    }
}
Write-Host "  -> Collected $($mesh.Count) unique certs.`n"


# Distribute full set to all nodes
Write-Host "[Distribute]"
foreach ($n in $Nodes) {
    Write-Host "  • $n"
    Invoke-Command -ComputerName $n -ScriptBlock {
        param($store,$pwd,[hashtable]$all)

        $flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet `
               -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable `
               -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet

        $localStore = New-Object System.Security.Cryptography.X509Certificates.X509Store($store,'LocalMachine')
        $localStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
        try {
            $present = $localStore.Certificates | Select-Object -ExpandProperty Thumbprint
            foreach ($thumb in $all.Keys) {
                if ($present -notcontains $thumb) {
                    $bytes = [Convert]::FromBase64String($all[$thumb])
                    $cert  = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes,$pwd,$flags)
                    $localStore.Add($cert)
                }
            }
        }
        finally { $localStore.Close() }
    } -ArgumentList $storeName,$pwdPlain,$mesh
}

Write-Host "`nAll Shielded‑VM certificates are now present on every node.`n"
pause

Don't forget to edit your backup infrastructure that might use instant restore when switching to vTPM VM's

Proost