diff --git a/README.md b/README.md index 9d31ed3..8c84639 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ Confidential computing is the protection of data-in-use through isolating comput | Addition | Description | |---|---| | **[Federated Multi-Party Demo](/multi-party-samples/advanced-app-federated/README.md)** ⭐ | New 4-party (Contoso, Fabrikam, Wingtip Toys, Woodgrove Bank) **federated** analytics demo. Each partner decrypts its own data inside its own AMD SEV-SNP TEE and returns only aggregates — no raw PII ever crosses the trust boundary. Includes a 3-minute [`DEMO-SCRIPT.md`](/multi-party-samples/advanced-app-federated/DEMO-SCRIPT.md). | +| **[CVM samples now support Intel TDX](/vm-samples/README.md)** | [`BuildRandomCVM.ps1`](/vm-samples/BuildRandomCVM.ps1) auto-detects AMD SEV-SNP (`DCa*`/`ECa*`) vs Intel TDX (`DCe*`/`ECe*`, e.g. `Standard_DC2es_v6`) from the chosen VM SKU and runs the matching attestation config. See the [Intel TDX examples](/vm-samples/README.md#intel-tdx-examples) in the VM samples README. | +| **Updated in-VM attestation tooling** | The CVM build script now runs the latest pre-built `attest` binary from [Azure/cvm-attestation-tools](https://github.com/Azure/cvm-attestation-tools/releases/latest) inside the VM (Linux + Windows), then **decodes the returned MAA JWT** (header, payload and key claims like `x-ms-attestation-type` and `x-ms-compliance-status`) using `jq` on Linux and built-in `ConvertFrom-Json` on Windows. The legacy [`WindowsAttest.ps1`](/vm-samples/WindowsAttest.ps1) is kept for reference but is **no longer recommended**. | | **Repo redirect** | [`https://aka.ms/accsamples`](https://aka.ms/accsamples) now points to this repo. The legacy [`confidential-container-samples`](https://github.com/Azure-Samples/confidential-container-samples) repo remains read-only / archived for reference. | ### Previously (May 2026) @@ -147,11 +149,12 @@ Simpler 2-container demonstration (Contoso, Fabrikam Fashion) without partner an ### [VM Samples](/vm-samples/README.md) Confidential Virtual Machine (CVM) deployment scripts: -- **BuildRandomCVM.ps1** - Deploy CVMs with Customer Managed Keys, Confidential Disk Encryption, and attestation - - Windows Server 2022 Datacenter - - Windows 11 Enterprise 24H2 - - Ubuntu 24.04 LTS - - RHEL 9.5 +- **BuildRandomCVM.ps1** 🆕 *Updated June 2026* — Deploy CVMs with **Confidential OS disk encryption bound to a Customer Managed Key (CMK)** and automated in-VM attestation. The OS disk and VM Guest State (vTPM + Secure Boot) are encrypted with an HSM-backed RSA-3072 key in your Key Vault Premium; the key is only released after Microsoft Azure Attestation (MAA) verifies the VM is a genuine SEV-SNP / TDX CVM, so even the Azure host fabric cannot read the disk. See [What is Confidential OS disk encryption with CMK?](/vm-samples/README.md#what-is-confidential-os-disk-encryption-with-cmk) and [`https://aka.ms/accdocs`](https://aka.ms/accdocs). + - **AMD SEV-SNP** (`DCa*` / `ECa*`, e.g. `Standard_DC2as_v5`) **and Intel TDX** (`DCe*` / `ECe*`, e.g. `Standard_DC2es_v6`) — auto-detected from the chosen VM SKU, with the matching attestation config selected automatically + - Windows Server 2022 Datacenter, Windows Server 2019, Windows 11 Enterprise 24H2, Ubuntu 24.04 LTS, RHEL 9.5 — all confidential-VM images + - **New attestation flow** — runs the latest pre-built `attest` binary from [`Azure/cvm-attestation-tools`](https://github.com/Azure/cvm-attestation-tools/releases/latest) inside the freshly deployed VM (Linux + Windows) and decodes the returned MAA JWT (header, payload, key claims like `x-ms-attestation-type`, `x-ms-compliance-status`, `secure-boot`, `tpm-enabled`) using `jq` on Linux / built-in `ConvertFrom-Json` on Windows + - **Pre-flight checks** before any resources are created: rejects Intel SGX SKUs (different isolation model), validates the chosen SKU is offered in the target region, and confirms there is enough vCPU quota in the VM family + - **Bastion-optional** (`-DisableBastion`) and **smoketest mode** (`-smoketest`) for CI / cost-controlled validation - **BuildRandomSQLCVM.ps1** - SQL Server 2022 on Confidential VM ### [AKS Samples](/aks-samples/README.md) diff --git a/vm-samples/AttestationClientScreenshot.png b/vm-samples/AttestationClientScreenshot.png index decbe79..1df1505 100644 Binary files a/vm-samples/AttestationClientScreenshot.png and b/vm-samples/AttestationClientScreenshot.png differ diff --git a/vm-samples/BuildRandomCVM.ps1 b/vm-samples/BuildRandomCVM.ps1 index 0893eb5..fc99adc 100644 --- a/vm-samples/BuildRandomCVM.ps1 +++ b/vm-samples/BuildRandomCVM.ps1 @@ -125,6 +125,101 @@ if (!$?) { $tmp = Get-AzContext $ownername = $tmp.Account.Id +#---------Pre-flight: SKU availability and quota check--------------------------------------------------------------- +# Verify the chosen VM SKU is a Confidential VM SKU (AMD SEV-SNP or Intel TDX, NOT Intel SGX which is +# a different isolation model and not supported by this script), is offered in the chosen region and not +# restricted for this subscription, and that there's enough vCPU quota left in the SKU's family before +# we start creating resources. +write-host "----------------------------------------------------------------------------------------------------------------" +write-host "Pre-flight check: confirming '$vmSize' is available in '$region' with sufficient quota..." -ForegroundColor Cyan + +# Reject Intel SGX SKUs early - this script targets Confidential VMs (full-VM isolation), +# not SGX enclaves (per-process isolation). SGX SKUs use the DC*s_v3 / DC*s_v2 naming. +if ($vmSize -match '^Standard_DC\d+s_v[23]$') { + write-host "ERROR: '$vmSize' is an Intel SGX SKU (application-enclave isolation), which is NOT supported by this script." -ForegroundColor Red + write-host "This script provisions Confidential VMs (whole-VM hardware isolation) using either:" -ForegroundColor Yellow + write-host " - AMD SEV-SNP : DCa*/ECa* (e.g. Standard_DC2as_v5, Standard_DC4as_v5)" -ForegroundColor Yellow + write-host " - Intel TDX : DCe*/ECe* (e.g. Standard_DC2es_v6, Standard_EC4es_v6)" -ForegroundColor Yellow + write-host "For Intel SGX (DCsv3/DCsv2) workloads, see https://learn.microsoft.com/azure/confidential-computing/virtual-machine-solutions-sgx instead." -ForegroundColor Yellow + exit 1 +} + +# Warn (but don't fail) if the SKU doesn't look like a known CVM SKU naming pattern. +if ($vmSize -notmatch '^Standard_(DC|EC)\d+[a-z]+_v\d+$' -or $vmSize -notmatch '_(DC|EC)\d+(a|e)') { + write-host "Warning: '$vmSize' does not match a known Confidential VM SKU pattern (DCa*/ECa* for SEV-SNP, DCe*/ECe* for TDX). Continuing, but deployment may fail if this is not a CVM SKU." -ForegroundColor Yellow +} + +$skuInfo = $null +try { + $skuInfo = Get-AzComputeResourceSku -Location $region -ErrorAction Stop | + Where-Object { $_.ResourceType -eq 'virtualMachines' -and $_.Name -eq $vmSize } | + Select-Object -First 1 +} catch { + write-host "Warning: could not query Get-AzComputeResourceSku for '$region': $($_.Exception.Message)" -ForegroundColor Yellow +} + +function Show-QuotaHelp($sku, $region) { + write-host "" + write-host "To find regions where this SKU IS available to your subscription, try:" -ForegroundColor Yellow + write-host " Get-AzComputeResourceSku | Where-Object { `$_.ResourceType -eq 'virtualMachines' -and `$_.Name -eq '$sku' -and (-not `$_.Restrictions -or `$_.Restrictions.Count -eq 0) } | Select-Object Locations, Name" -ForegroundColor Gray + write-host "" + write-host "To list the Confidential VM SKUs offered in '$region' (SEV-SNP DCa*/ECa* and Intel TDX DCe*/ECe*):" -ForegroundColor Yellow + write-host " Get-AzComputeResourceSku -Location '$region' | Where-Object { `$_.ResourceType -eq 'virtualMachines' -and `$_.Name -match '_(DC|EC)\d+(a|e)' } | Select-Object Name, @{n='Restricted';e={`$_.Restrictions.Count -gt 0}}" -ForegroundColor Gray + write-host "" + write-host "To see your vCPU usage and limits in '$region':" -ForegroundColor Yellow + write-host " Get-AzVMUsage -Location '$region' | Where-Object { `$_.Name.Value -match 'DCa|DCe|ECa|ECe|cores' } | Format-Table -AutoSize" -ForegroundColor Gray + write-host "" + write-host "To request a quota increase, see: https://learn.microsoft.com/azure/quotas/quickstart-increase-quota-portal" -ForegroundColor Yellow +} + +if ($null -eq $skuInfo) { + write-host "ERROR: VM SKU '$vmSize' is not offered in region '$region'." -ForegroundColor Red + Show-QuotaHelp $vmSize $region + exit 1 +} + +# Check subscription-level restrictions (e.g. NotAvailableForSubscription) +$subRestriction = $skuInfo.Restrictions | Where-Object { + $_.ReasonCode -eq 'NotAvailableForSubscription' -or + ($_.RestrictionInfo -and $_.RestrictionInfo.Locations -contains $region) -or + ($_.Values -contains $region) +} +if ($subRestriction) { + $reason = ($skuInfo.Restrictions | ForEach-Object { $_.ReasonCode }) -join ', ' + write-host "ERROR: VM SKU '$vmSize' is restricted for this subscription in '$region' (reason: $reason)." -ForegroundColor Red + Show-QuotaHelp $vmSize $region + exit 1 +} + +# Determine vCPU count and family for the SKU +$skuVCpus = ($skuInfo.Capabilities | Where-Object { $_.Name -eq 'vCPUs' } | Select-Object -First 1).Value -as [int] +$skuFamily = $skuInfo.Family # e.g. 'standardDCASv5Family' +if (-not $skuVCpus) { $skuVCpus = 2 } # fall back to a sensible minimum + +# Check vCPU quota for that family in this region +try { + $usage = Get-AzVMUsage -Location $region -ErrorAction Stop | + Where-Object { $_.Name.Value -eq $skuFamily } | + Select-Object -First 1 + if ($usage) { + $available = [int]$usage.Limit - [int]$usage.CurrentValue + write-host ("Quota for {0} in {1}: {2}/{3} used, {4} vCPUs available, this SKU needs {5}." -f ` + $skuFamily, $region, $usage.CurrentValue, $usage.Limit, $available, $skuVCpus) -ForegroundColor Cyan + if ($available -lt $skuVCpus) { + write-host "ERROR: Insufficient vCPU quota in family '$skuFamily' in '$region' to deploy '$vmSize' ($skuVCpus vCPUs needed, $available available)." -ForegroundColor Red + Show-QuotaHelp $vmSize $region + exit 1 + } + } else { + write-host "Note: could not find VM usage entry for family '$skuFamily' in '$region' - proceeding without quota check." -ForegroundColor Yellow + } +} catch { + write-host "Warning: Get-AzVMUsage failed for '$region': $($_.Exception.Message). Proceeding without quota check." -ForegroundColor Yellow +} + +write-host "Pre-flight check passed: '$vmSize' is available and within quota in '$region'." -ForegroundColor Green +write-host "----------------------------------------------------------------------------------------------------------------" + # Create Resource Group $resourceGroupTags = @{ owner = $ownername @@ -261,39 +356,168 @@ if (-not $DisableBastion) { write-host "VM is only accessible via private network connectivity (VPN, ExpressRoute, or peered networks)" } -# COMMENTED OUT FOR NOW, will be re-factored to use latest attestation code from https://github.com/Azure/cvm-attestation-tools -#---------Do attestation check, kick off a script inside the VM to do the attestation check--------- -# Run attestation based on OS type -<# -write-host "Running an attestation check inside the $osType VM, please wait for output..." - -if ($osType -like 'Windows*' -and $osType -ne 'Windows11') { - # Windows family (Server and other Windows SKUs except Windows11) - use PowerShell script - $output = Invoke-AzVMRunCommand -Name $vmname -ResourceGroupName $resgrp -CommandId 'RunPowerShellScript' -ScriptPath .\WindowsAttest.ps1 -} elseif ($osType -eq "Windows11") { - # Windows 11 VM - use PowerShell script - $output = Invoke-AzVMRunCommand -Name $vmname -ResourceGroupName $resgrp -CommandId 'RunPowerShellScript' -ScriptPath .\WindowsAttest.ps1 +#---------Do attestation check inside the VM using Azure/cvm-attestation-tools----------------------------------- +# Downloads the latest pre-built attest CLI release from https://github.com/Azure/cvm-attestation-tools/releases +# and runs it inside the freshly deployed CVM, returning the output to the caller. + +# Pick the right config based on the VM SKU's isolation type: +# AMD SEV-SNP: DCa*/DCad*/ECa*/ECad* (e.g. Standard_DC2as_v5) -> config_snp.json +# Intel TDX : DCe*/DCed*/ECe*/ECed* (e.g. Standard_DC2es_v5) -> config_tdx.json +if ($vmSize -match '_(DC|EC)\d+e[a-z]*_') { + $attestConfig = "config_tdx.json" + $isolationType = "Intel TDX" } else { - # Linux VMs (Ubuntu/RHEL) - use shell script + $attestConfig = "config_snp.json" + $isolationType = "AMD SEV-SNP" +} + +write-host "----------------------------------------------------------------------------------------------------------------" +write-host "Running attestation inside the $osType VM using cvm-attestation-tools (isolation: $isolationType, config: $attestConfig)..." -ForegroundColor Cyan +write-host "This downloads the latest release of attest from https://github.com/Azure/cvm-attestation-tools/releases inside the VM." + +if ($VMisLinux) { + # Linux: download attest-lin.zip from the latest release, extract, run attest + # Note: the zip extracts files at its root (no "attest-lin/" subfolder) $attestScript = @" #!/bin/bash -echo "Linux $osType CVM attestation check" -echo "Checking if running in a Confidential VM..." -if [ -d "/sys/kernel/security/tpm0" ]; then - echo "TPM device detected" +set -e +export DEBIAN_FRONTEND=noninteractive +if ! command -v unzip >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + (apt-get update -y && apt-get install -y unzip jq) >/dev/null 2>&1 || \ + (dnf install -y unzip jq || yum install -y unzip jq) >/dev/null 2>&1 +fi +WORKDIR=`$(mktemp -d) +cd "`$WORKDIR" +echo "Downloading latest attest-lin.zip from cvm-attestation-tools..." +curl -fsSL -o attest-lin.zip https://github.com/Azure/cvm-attestation-tools/releases/latest/download/attest-lin.zip +unzip -q attest-lin.zip +chmod +x attest read_report 2>/dev/null || true +echo "--------- attest --c $attestConfig ---------" +./attest --c $attestConfig 2>&1 | tee attest.out || echo "attest exited with code `$?" + +# Extract JWT (a single token of the form xxx.yyy.zzz with base64url chars) +# from the attest output and pretty-print header + payload claims using jq. +JWT=`$(grep -Eo '[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' attest.out | awk '{ print length, `$0 }' | sort -nr | head -1 | cut -d' ' -f2-) +if [ -n "`$JWT" ] && command -v jq >/dev/null 2>&1; then + echo "" + echo "--------- Decoded JWT (via jq) ---------" + b64d() { local s=`$1; local m=`$(( `${#s} % 4 )); if [ `$m -eq 2 ]; then s="`${s}=="; elif [ `$m -eq 3 ]; then s="`${s}="; fi; echo "`$s" | tr '_-' '/+' | base64 -d 2>/dev/null; } + H=`$(echo "`$JWT" | cut -d. -f1) + P=`$(echo "`$JWT" | cut -d. -f2) + echo "--- header ---" + b64d "`$H" | jq . + echo "--- payload ---" + b64d "`$P" | jq . + echo "--- key MAA claims ---" + b64d "`$P" | jq '{iss, "x-ms-attestation-type", "x-ms-compliance-status", "x-ms-isolation-tee": ."x-ms-isolation-tee"."x-ms-attestation-type", "x-ms-runtime-vm-configuration-secure-boot": ."x-ms-runtime"."vm-configuration"."secure-boot", "x-ms-runtime-vm-configuration-tpm-enabled": ."x-ms-runtime"."vm-configuration"."tpm-enabled"}' else - echo "No TPM device found" + echo "(no JWT found in attest output to decode, or jq unavailable)" fi -echo "For full attestation on Linux, additional tools and configuration may be required" -echo "This is a basic check - implement proper attestation logic for production use" + +cd / +rm -rf "`$WORKDIR" "@ - $output = Invoke-AzVMRunCommand -Name $vmname -ResourceGroupName $resgrp -CommandId 'RunShellScript' -ScriptString $attestScript + $runCommandId = 'RunShellScript' +} else { + # Windows: download attest-win.zip from the latest release, extract, run attest.exe + # Note: the zip extracts files at its root (no "attest-win/" subfolder) + $attestScript = @" +`$ErrorActionPreference = 'Stop' +`$ProgressPreference = 'SilentlyContinue' +`$work = Join-Path `$env:TEMP "cvm-attest-`$(Get-Random)" +New-Item -ItemType Directory -Path `$work -Force | Out-Null +Set-Location `$work +Write-Host "Downloading latest attest-win.zip from cvm-attestation-tools..." +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +Invoke-WebRequest -Uri 'https://github.com/Azure/cvm-attestation-tools/releases/latest/download/attest-win.zip' -OutFile 'attest-win.zip' -UseBasicParsing +Expand-Archive -Path 'attest-win.zip' -DestinationPath '.' -Force +Write-Host "--------- attest.exe --c $attestConfig ---------" + +# attest.exe writes INFO logs to stderr; under `$ErrorActionPreference='Stop' that surfaces +# as a NativeCommandError even though the tool is working. Relax EAP for this single call, +# merge stderr into stdout, and rely on `$LASTEXITCODE for success/failure. +# Use a wide -Width on Out-String so the JWT (which can be ~1.5KB on a single line) +# is not wrapped at the default ~80-char console width - otherwise the regex below only +# matches a wrapped fragment and base64url decoding fails with "Invalid length". +`$prevEap = `$ErrorActionPreference +`$ErrorActionPreference = 'Continue' +if (`$PSVersionTable.PSVersion.Major -ge 7) { `$PSNativeCommandUseErrorActionPreference = `$false } +`$attestOut = (& .\attest.exe --c $attestConfig 2>&1 | Out-String -Width 16384) +`$attestExit = `$LASTEXITCODE +`$ErrorActionPreference = `$prevEap +Write-Host `$attestOut +if (`$attestExit -ne 0) { Write-Host "attest.exe exited with code `$attestExit" -ForegroundColor Yellow } + +# Extract JWT (xxx.yyy.zzz, base64url) from the attest output and pretty-print +# header + payload claims using built-in PowerShell JSON support (no jq needed). +# Defensive: collapse the captured output to a single line so any stray CR/LF inside +# the token is removed, then anchor the regex to 'eyJ' (base64url of '{"') which is +# the canonical JWT header start. Without that anchor, the greedy 3-segment match +# can cross attest.exe's interleaved Python INFO log lines (which share '-' / '_' +# with base64url) and pick up a corrupted token, producing garbage on decode. +`$attestOutFlat = (`$attestOut -replace '\s+', '') +`$jwtMatch = [regex]::Matches(`$attestOutFlat, 'eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{100,}\.[A-Za-z0-9_-]{50,}') | Sort-Object { `$_.Length } -Descending | Select-Object -First 1 +if (`$jwtMatch) { + function ConvertFrom-Base64Url(`$s) { + `$s = (`$s -replace '\s', '').Replace('-', '+').Replace('_', '/') + switch (`$s.Length % 4) { 2 { `$s += '==' } 3 { `$s += '=' } } + [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(`$s)) + } + `$parts = `$jwtMatch.Value.Split('.') + Write-Host "" + Write-Host '--------- Decoded JWT ---------' + Write-Host '--- header ---' + ConvertFrom-Base64Url `$parts[0] | ConvertFrom-Json | ConvertTo-Json -Depth 10 + Write-Host '--- payload ---' + `$payload = ConvertFrom-Base64Url `$parts[1] | ConvertFrom-Json + `$payload | ConvertTo-Json -Depth 10 + Write-Host '--- key MAA claims ---' + [pscustomobject]@{ + iss = `$payload.iss + 'x-ms-attestation-type' = `$payload.'x-ms-attestation-type' + 'x-ms-compliance-status' = `$payload.'x-ms-compliance-status' + 'x-ms-isolation-tee' = `$payload.'x-ms-isolation-tee'.'x-ms-attestation-type' + 'secure-boot' = `$payload.'x-ms-runtime'.'vm-configuration'.'secure-boot' + 'tpm-enabled' = `$payload.'x-ms-runtime'.'vm-configuration'.'tpm-enabled' + } | Format-List +} else { + Write-Host '(no JWT found in attest output to decode)' +} + +Set-Location `$env:TEMP +Remove-Item -Recurse -Force `$work -ErrorAction SilentlyContinue +"@ + $runCommandId = 'RunPowerShellScript' +} + +# Retry loop: Invoke-AzVMRunCommand can return 409 Conflict for several minutes +# after VM creation while the run-command extension is still finalising. This is +# especially common on TDX SKUs. Back off and retry on Conflict / "in progress". +$output = $null +$maxAttempts = 10 +for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + try { + write-host "Attestation run-command attempt $attempt of $maxAttempts..." -ForegroundColor Cyan + $output = Invoke-AzVMRunCommand -Name $vmname -ResourceGroupName $resgrp -CommandId $runCommandId -ScriptString $attestScript -ErrorAction Stop + break + } catch { + $msg = $_.Exception.Message + if ($attempt -lt $maxAttempts -and ($msg -like '*Conflict*' -or $msg -like '*in progress*' -or $msg -like '*409*')) { + write-host "Run-command extension busy (409); waiting 60s before retry..." -ForegroundColor Yellow + Start-Sleep -Seconds 60 + } else { + throw + } + } +} + +write-host "----------------------------------------------------------------------------------------------------------------" +write-host "--------------Output from cvm-attestation-tools running inside the VM--------------" +foreach ($entry in $output.Value) { + if ($entry.Message) { write-host $entry.Message } } -write-host "--------------Output from the script that ran inside the VM--------------" -write-host $output.Value.message # repeat the output from the script that ran inside the VM write-host "----------------------------------------------------------------------------------------------------------------" -write-host "Build and validation complete, check the output above for the attestation status." -#> +write-host "Build and attestation complete." -ForegroundColor Green # Smoketest cleanup - automatically remove all resources if smoketest flag is used diff --git a/vm-samples/README.md b/vm-samples/README.md index f676b24..28c91f3 100644 --- a/vm-samples/README.md +++ b/vm-samples/README.md @@ -1,10 +1,12 @@ # Confidential Virtual Machines -**Last Updated:** February 2026 +**Last Updated:** June 2026 ## Overview -Deploy Confidential Virtual Machines (CVMs) with AMD SEV-SNP hardware protection, Customer Managed Keys (CMK), and automated attestation. +Deploy Confidential Virtual Machines (CVMs) with AMD SEV-SNP or Intel TDX hardware protection, **Confidential OS disk encryption bound to a Customer Managed Key (CMK)**, and automated attestation. The script auto-detects the isolation type from the chosen VM SKU and runs the matching attestation flow inside the freshly deployed VM using the latest [Azure/cvm-attestation-tools](https://github.com/Azure/cvm-attestation-tools) release. + +> 📚 New to Azure Confidential Computing? Start at [`https://aka.ms/accdocs`](https://aka.ms/accdocs) for the full product documentation, including [Confidential VM overview](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) and [Confidential OS disk encryption](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview#confidential-os-disk-encryption). ``` ┌─────────────────────────────────────────────────────────────────────────┐ @@ -41,41 +43,67 @@ Deploy Confidential Virtual Machines (CVMs) with AMD SEV-SNP hardware protection | Script | Description | Status | |--------|-------------|--------| -| `BuildRandomCVM.ps1` | Deploy CVM with CMK, Confidential Disk Encryption, and Bastion | **Stable** | +| `BuildRandomCVM.ps1` | Deploy CVM with Confidential OS disk encryption + CMK and Bastion | **Stable** | | `BuildRandomSQLCVM.ps1` | SQL Server 2022 on Confidential VM | **Stable** | --- ## BuildRandomCVM.ps1 -Builds a CVM with Customer Managed Key, Confidential Disk Encryption, a private VNet (no public IP) and deploy Azure Bastion for remote access over the Internet. It supports Windows Server 2022, Windows 11 Enterprise, Ubuntu 24.04 LTS, and RHEL 9.5 CVM images. The script automatically detects the GitHub repository URL from the local git configuration and includes it in resource tagging. It will then kick off an attestation inside the CVM and present back the output via Invoke-AzVMRunCommand. +Builds a CVM with **Confidential OS disk encryption bound to a Customer Managed Key** (see [What is Confidential OS disk encryption?](#what-is-confidential-os-disk-encryption-with-cmk) below) and a private VNet (no public IP), and optionally deploys Azure Bastion for remote access. Once the VM is up, the script downloads the latest pre-built `attest` binary from [Azure/cvm-attestation-tools](https://github.com/Azure/cvm-attestation-tools/releases/latest) inside the VM, runs it against the matching SNP/TDX config, and streams the attestation JWT and claims back to the caller via `Invoke-AzVMRunCommand` (with a 60s backoff retry loop to absorb the transient 409 Conflict that the run-command extension can return immediately after VM provisioning). + +Supported images: Windows Server 2022, Windows Server 2019, Windows 11 Enterprise, Ubuntu 24.04 LTS, and RHEL 9.5 CVM. The script tags the resource group with the GitHub repo URL (auto-detected from git remote) for traceability. + +### Pre-flight checks (before any resources are created) + +To save you a half-built deployment in a region or subscription that can't actually host the VM, the script runs the following checks **before** creating the resource group, Key Vault, DES, VNet or VM: + +1. **Confidential VM SKU type** — confirms `-vmsize` is an AMD SEV-SNP (`DCa*`/`ECa*`) or Intel TDX (`DCe*`/`ECe*`) SKU. **Intel SGX SKUs (`DC*s_v3` / `DC*s_v2`) are explicitly rejected** because they use a different isolation model (per-process enclaves, not full-VM); the script prints a clear error and points at the [SGX VM solutions docs](https://learn.microsoft.com/azure/confidential-computing/virtual-machine-solutions-sgx) instead. +2. **SKU availability in the chosen region** — calls `Get-AzComputeResourceSku -Location ` and verifies the SKU is offered there and is not blocked for your subscription (e.g. `NotAvailableForSubscription`). +3. **vCPU quota** — calls `Get-AzVMUsage -Location `, locates the SKU's family (e.g. `standardDCASv5Family`, `standardDCESv6Family`) and confirms there are at least enough free vCPUs left to deploy the requested size. + +If any check fails the script aborts immediately with a clear error and prints the helper commands you can run yourself to find a region with availability or check your quota, e.g.: + +```powershell +# Regions where this SKU is available to your subscription +Get-AzComputeResourceSku | Where-Object { $_.ResourceType -eq 'virtualMachines' -and $_.Name -eq '' -and (-not $_.Restrictions -or $_.Restrictions.Count -eq 0) } | Select-Object Locations, Name + +# All Confidential VM SKUs offered in +Get-AzComputeResourceSku -Location '' | Where-Object { $_.ResourceType -eq 'virtualMachines' -and $_.Name -match '_(DC|EC)\d+(a|e)' } | Select-Object Name, @{n='Restricted';e={$_.Restrictions.Count -gt 0}} + +# Your current vCPU usage and limits in +Get-AzVMUsage -Location '' | Where-Object { $_.Name.Value -match 'DCa|DCe|ECa|ECe|cores' } | Format-Table -AutoSize +``` Use at your own risk, no warranties implied. ## Usage -Clone this repo locally (Windows deployments depend on WindowsAttest.ps1). +Clone this repo locally. Basename is a prefix assigned to all resources created by the script and will be given a 5 char suffix - for example: myCVM-sdfrw. The script will generate a random complex password and output it to the terminal once; make sure you copy it if you want to login to the CVM. ```powershell -./BuildRandomCVM.ps1 -subsID -basename -osType [-description ] [-smoketest] [-region ] [-vmsize ] +./BuildRandomCVM.ps1 -subsID -basename -osType [-description ] [-smoketest] [-region ] [-vmsize ] [-policyFilePath ] [-DisableBastion] ``` ## Parameters: - **subsID**: Your Azure subscription ID (required) - **basename**: A prefix for all resources created by the script (required) - **osType**: The operating system to deploy (required) -- **description**: Optional description that will be added as a tag to the resource group +- **description**: Optional description added as a tag to the resource group - **smoketest**: Optional switch that automatically removes all resources after completion (useful for testing) -- **region**: Optional Azure region to deploy resources (defaults to "northeurope") -- **vmsize**: Optional VM size SKU (defaults to "Standard_DC2as_v5"). Use this to select a different Confidential VM SKU, e.g. "Standard_DC4as_v5". +- **region**: Optional Azure region (defaults to `northeurope`) +- **vmsize**: Optional VM size SKU (defaults to `Standard_DC2as_v5`). Use SEV-SNP SKUs like `Standard_DC4as_v5` or Intel TDX SKUs like `Standard_DC2es_v6` — the script picks the matching attestation config automatically. +- **policyFilePath**: Optional path to a custom key release policy JSON (defaults to `-UseDefaultCVMPolicy`) +- **DisableBastion**: Optional switch that skips Azure Bastion creation; the VM will only be reachable via private network connectivity (VPN, ExpressRoute, peering) ## OS Type Options: -- **Windows**: Windows Server 2022 Datacenter (supports RDP via Bastion) -- **Windows11**: Windows 11 Enterprise 24H2 (supports RDP via Bastion) -- **Ubuntu**: Ubuntu 24.04 LTS CVM (supports SSH via Bastion) -- **RHEL**: Red Hat Enterprise Linux 9.5 CVM (supports SSH via Bastion) +- **Windows**: Windows Server 2022 Datacenter (RDP via Bastion) +- **Windows2019**: Windows Server 2019 Datacenter (RDP via Bastion) +- **Windows11**: Windows 11 Enterprise 24H2 (RDP via Bastion) +- **Ubuntu**: Ubuntu 24.04 LTS CVM (SSH via Bastion) +- **RHEL**: Red Hat Enterprise Linux 9.5 CVM (SSH via Bastion) ## Example: ```powershell @@ -104,6 +132,32 @@ The script will generate a random complex password and output it to the terminal ./BuildRandomCVM.ps1 -subsID "your-subscription-id" -basename "ci" -osType "Windows11" -description "Automated testing pipeline" -smoketest ``` +## Intel TDX Examples + +The script auto-detects the isolation type from the VM SKU and runs the matching attestation flow inside the VM: + +- **AMD SEV-SNP** SKUs (`DCa*` / `DCad*` / `ECa*` / `ECad*`, e.g. `Standard_DC2as_v5`) → uses `config_snp.json`, expect `x-ms-attestation-type: sevsnpvm`. +- **Intel TDX** SKUs (`DCe*` / `DCed*` / `ECe*` / `ECed*`, e.g. `Standard_DC2es_v6`) → uses `config_tdx.json`, expect `x-ms-attestation-type: tdxvm`. + +Intel TDX SKUs are available in a subset of regions (e.g. `westeurope`, `westus3`, `northeurope`). Verify with: + +```powershell +Get-AzComputeResourceSku -Location westeurope | + Where-Object { $_.Name -match '^Standard_(DC|EC)\d+e' -and $_.ResourceType -eq 'virtualMachines' } | + Select-Object Name, @{N='Restrictions';E={ $_.Restrictions.ReasonCode -join ',' }} +``` + +```powershell +# Deploy Ubuntu 24.04 Intel TDX CVM in West Europe +./BuildRandomCVM.ps1 -subsID "your-subscription-id" -basename "mytdx" -osType "Ubuntu" -region "westeurope" -vmsize "Standard_DC2es_v6" -DisableBastion -description "ubuntu tdx attestation" + +# Deploy Windows Server 2022 Intel TDX CVM +./BuildRandomCVM.ps1 -subsID "your-subscription-id" -basename "wintdx" -osType "Windows" -region "westeurope" -vmsize "Standard_DC2es_v6" + +# Smoketest a TDX deployment +./BuildRandomCVM.ps1 -subsID "your-subscription-id" -basename "tdxci" -osType "Ubuntu" -region "westus3" -vmsize "Standard_DC4es_v6" -smoketest +``` + The script automatically tags the resource group with: - **owner**: Your Azure user principal name - **BuiltBy**: The script name that created the resources @@ -133,37 +187,36 @@ When using `-smoketest`, the script will: 5. Automatically remove all created resources (VM, Key Vault, Bastion, VNet, etc.) if not cancelled 6. Provide confirmation of successful cleanup or cancellation status -## Key Release Policy Template +## What is Confidential OS disk encryption with CMK? -The [`releasePolicyTemplate.json`](releasePolicyTemplate.json) file is a template for configuring a key release policy that can be assigned to a CVM. This policy defines the conditions under which a key can be released from Azure Key Vault to a Confidential VM. +A standard Azure VM with a customer-managed key (CMK) protects the **disk blob at rest in Azure Storage** with your key — but the OS disk is still decrypted by the Azure host before being presented to the guest, so the host (and anyone with privileged access to it) can in principle read OS disk contents in the clear. -### Key Features: -- **Attestation validation**: Ensures the VM is running in a compliant confidential computing environment -- **Custom MAA endpoint support**: Can be configured to point to a custom Microsoft Azure Attestation (MAA) endpoint -- **SEV-SNP validation**: Validates that the VM is running on AMD SEV-SNP hardware +**Confidential OS disk encryption** (also called *Confidential Disk Encryption with Customer-Managed Keys*) goes further: it provisions the OS disk with `SecurityEncryptionType = DiskWithVMGuestState`, which means: -### Default Behavior: -If the `-policyFilePath` parameter is not specified when running `BuildRandomCVM.ps1`, the script will automatically use the default CVM policy that points to the shared attestation endpoint serving the region where your CVM has been created. This provides out-of-the-box attestation functionality without requiring custom configuration. +- The OS disk and the **VM Guest State (VMGS)** blob (which holds vTPM state and Secure Boot keys) are **encrypted with a key that lives in your Azure Key Vault Premium / Managed HSM** and never leaves it in clear form. +- Decryption happens **inside the AMD SEV-SNP or Intel TDX trusted execution environment**, not on the Azure host. The host fabric, hypervisor admin, datacenter operator, and even Microsoft cannot read the decrypted OS disk content. +- The CMK is wrapped by an Azure-managed *CVM orchestrator* identity that is itself gated by an **attestation-bound key release policy** (`-UseDefaultCVMPolicy` in this script): the key is only unwrapped after Microsoft Azure Attestation (MAA) verifies the platform really is a genuine SEV-SNP / TDX CVM running the expected firmware and Secure Boot configuration. +- The vTPM state inside VMGS is also protected, so things like BitLocker keys (Windows) or LUKS volume keys (Linux) sealed to the vTPM stay confidential to the guest. -### Usage with Custom MAA: -The template can be customized to work with a private MAA endpoint created using the [`createPrivateMAA.ps1`](../attestation-samples/createPrivateMAA.ps1) script from the attestation-samples directory: +Why this matters: -1. Create a private MAA endpoint using the attestation samples -2. Update the `authority` field in `releasePolicyTemplate.json` with your MAA endpoint URL -3. Use the `-policyFilePath` parameter with `BuildRandomCVM.ps1` to apply the custom policy +| Capability | Standard VM + CMK | Confidential VM + CMK + Confidential OS disk encryption | +|---|---|---| +| Disk-at-rest encrypted with your key | ✅ | ✅ | +| Customer holds key in AKV/HSM | ✅ | ✅ | +| Key release gated by **hardware attestation** | ❌ | ✅ | +| OS disk decrypted **inside TEE**, not on host | ❌ | ✅ | +| Host admin / hypervisor blocked from reading OS disk | ❌ | ✅ | +| vTPM + Secure Boot state encrypted with same CMK | ❌ | ✅ (via VMGS) | -```powershell -# Deploy CVM with default shared attestation endpoint (regional) -./BuildRandomCVM.ps1 -subsID "your-subscription-id" -basename "myvm" -osType "Windows" +In short, this is the strongest "my data, my key, my hardware boundary" posture available for an Azure VM today: the cloud operator runs the VM but cannot see inside it, and the disk cannot be decrypted anywhere except inside an attested confidential VM you own. See the official docs for details: [Confidential OS disk encryption](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview#confidential-os-disk-encryption), [How to create a CVM with CMK (Azure CLI)](https://learn.microsoft.com/azure/confidential-computing/quick-create-confidential-vm-azure-cli), and the umbrella docs at [`https://aka.ms/accdocs`](https://aka.ms/accdocs). -# Deploy CVM with custom key release policy -./BuildRandomCVM.ps1 -subsID "your-subscription-id" -basename "myvm" -osType "Windows" -policyFilePath "./releasePolicyTemplate.json" -``` +## Key Release Policy -This ensures that the CVM's customer-managed key can only be released when the attestation validation passes through your specified MAA endpoint (custom) or the regional shared endpoint (default). +If the `-policyFilePath` parameter is not specified when running `BuildRandomCVM.ps1`, the script uses the default CVM key release policy (`-UseDefaultCVMPolicy`), which points at the shared attestation endpoint serving the region where the CVM is created. This provides out-of-the-box attestation-gated key release without requiring custom configuration. For background on attestation-bound key release see the [Azure Key Vault SKR docs](https://learn.microsoft.com/azure/key-vault/keys/policy-grammar) and [`https://aka.ms/accdocs`](https://aka.ms/accdocs). ## Important Notes: -Note this will deploy an Azure Keyvault *Premium* SKU [pricing](https://azure.microsoft.com/en-gb/pricing/details/key-vault/#pricing) & enables purge protection for 10 days (you can adjust the purge protection period but AKV Premium is required for CVMs with confidential disk encryption +Note this will deploy an Azure Keyvault *Premium* SKU [pricing](https://azure.microsoft.com/en-gb/pricing/details/key-vault/#pricing) & enables purge protection for 10 days (you can adjust the purge protection period but AKV Premium with HSM-backed RSA-3072 keys is required for CVMs with **Confidential OS disk encryption** — see [`https://aka.ms/accdocs`](https://aka.ms/accdocs) for the full requirements). By default the script will create resources in North Europe - you can specify a different region using the `-region` parameter. Make sure to check availability of CVMs in your chosen region first. @@ -187,41 +240,65 @@ New-AzResourceGroupDeployment -Name DeployLocalTemplate -ResourceGroupName " **Recommended tooling:** [Azure/cvm-attestation-tools](https://github.com/Azure/cvm-attestation-tools) +> +> Pre-built `attest` binaries for Linux (`attest-lin.zip`) and Windows (`attest-win.zip`), with ready-made configs for AMD SEV-SNP (`config_snp.json`) and Intel TDX (`config_tdx.json`). See the [project README](https://github.com/Azure/cvm-attestation-tools/blob/main/README.md) and the [latest release](https://github.com/Azure/cvm-attestation-tools/releases/latest) for additional examples and the source. `BuildRandomCVM.ps1` in this repo runs that flow automatically inside the freshly deployed CVM. -The WindowsAttest.ps1 script can manually be invoked inside a Windows CVM to do an automated attestation check against the West Europe shared attestation endpoint. This script works with both Windows Server 2022 and Windows 11 Enterprise CVMs. +## In-VM attestation via cvm-attestation-tools -Expected output for Windows CVMs: +`BuildRandomCVM.ps1` selects the right config from the VM SKU and executes the matching binary inside the VM via `Invoke-AzVMRunCommand`: -Running on a CVM (DCa / ECa Series SKU using AMD SEV-SNP hardware) -> This Windows OS is running on sevsnpvm VM hardware -> This VM is an Azure compliant CVM attested by https://sharedweu.weu.attest.azure.net +| Isolation | Example SKUs | Binary | Config | Expected `x-ms-attestation-type` | +|---|---|---|---|---| +| AMD SEV-SNP | `Standard_DC2as_v5`, `Standard_EC4as_v5` | `attest` / `attest.exe` | `config_snp.json` | `sevsnpvm` | +| Intel TDX | `Standard_DC2es_v6`, `Standard_EC4es_v6` | `attest` / `attest.exe` | `config_tdx.json` | `tdxvm` | -NOT running on a CVM (any other Azure SKU) -> This VM is NOT an Azure compliant CVM +A successful run prints the JWT plus parsed claims and ends with `Attested Platform Successfully!!` and `x-ms-compliance-status: azure-compliant-cvm`. -![Screenshot of output from attestation script](./AttestationClientScreenshot.png) +## Running attestation manually against an existing CVM -## Linux CVMs -For Ubuntu and RHEL CVMs, the script performs a basic attestation check by looking for TPM devices and CVM indicators. For production use, you should implement proper Linux attestation tools -and libraries. +From an authenticated PowerShell session you can re-run attestation against an already-deployed CVM by reusing the same payload `BuildRandomCVM.ps1` injects. Replace `` and `` with your values, and switch the config filename for the isolation type of the target VM. -You can download the script to a CVM or execute directly from GitHub >inside< your CVM by pasting the following single line Command in a PowerShell session that is running with Administrative permissions (review the script 1st to ensure you are happy with the binaries and packages it installs or download & customize) +Linux (Ubuntu / RHEL) target: ```powershell -$ScriptFromGitHub = Invoke-WebRequest -uri https://raw.githubusercontent.com/Azure-Samples/confidential-computing/refs/heads/main/vm-samples/WindowsAttest.ps1 ; Invoke-Expression $($ScriptFromGitHub.Content) +$attest = @' +#!/bin/bash +set -e +command -v unzip >/dev/null || (apt-get update -y && apt-get install -y unzip) >/dev/null 2>&1 || (dnf install -y unzip || yum install -y unzip) >/dev/null 2>&1 +W=$(mktemp -d); cd "$W" +curl -fsSL -o a.zip https://github.com/Azure/cvm-attestation-tools/releases/latest/download/attest-lin.zip +unzip -q a.zip +chmod +x attest 2>/dev/null || true +./attest --c config_snp.json # use config_tdx.json for Intel TDX SKUs +cd /; rm -rf "$W" +'@ +Invoke-AzVMRunCommand -ResourceGroupName -VMName -CommandId 'RunShellScript' -ScriptString $attest ``` -If you want to run this command against your CVM >from< your own workstation over the Internet you can use the following 1-line command, edit the to match the VM you're targetting and paste it into a PowerShell session that is authenticated to your Azure subscription (in this case output will not be colour-coded) +Windows (Server 2019 / 2022 / Windows 11) target: ```powershell -$ScriptContent = Invoke-WebRequest -Uri https://raw.githubusercontent.com/Azure-Samples/confidential-computing/main/vm-samples/WindowsAttest.ps1 -UseBasicParsing | Select-Object -ExpandProperty Content ; Invoke-AzVMRunCommand -ResourceGroupName -VMName -CommandId "RunPowerShellScript" -ScriptString $ScriptContent +$attest = @' +$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue' +$w = Join-Path $env:TEMP "cvm-attest-$(Get-Random)"; New-Item -ItemType Directory -Path $w -Force | Out-Null; Set-Location $w +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +Invoke-WebRequest -Uri 'https://github.com/Azure/cvm-attestation-tools/releases/latest/download/attest-win.zip' -OutFile 'a.zip' -UseBasicParsing +Expand-Archive -Path 'a.zip' -DestinationPath '.' -Force +& .\attest.exe --c config_snp.json # use config_tdx.json for Intel TDX SKUs +'@ +Invoke-AzVMRunCommand -ResourceGroupName -VMName -CommandId 'RunPowerShellScript' -ScriptString $attest ``` -For more information on Azure confidential Computing see the [public docs](https://aka.ms/accdocs) +You can also SSH/RDP into the VM and run the binary directly; the same `attest --c ` invocation works interactively. + +## Deprecated: WindowsAttest.ps1 + +[`WindowsAttest.ps1`](WindowsAttest.ps1) is the previous-generation, Windows-only check (uses `cvm-platform-checker-exe` against the West Europe shared MAA endpoint, SEV-SNP only). It is retained for historical reference but is **no longer recommended** — use [Azure/cvm-attestation-tools](https://github.com/Azure/cvm-attestation-tools) instead, which is cross-platform and supports both SEV-SNP and Intel TDX. + +For more information on Azure Confidential Computing see the [public docs](https://aka.ms/accdocs). # TURBO Charged Versions diff --git a/vm-samples/WindowsAttest.ps1 b/vm-samples/WindowsAttest.ps1 index ff5cbc1..ce73df5 100644 --- a/vm-samples/WindowsAttest.ps1 +++ b/vm-samples/WindowsAttest.ps1 @@ -2,11 +2,25 @@ # Simon Gallagher, Microsoft https://github.com/vinfnet # NO warranties implied, use at your own risk # Review and customize code before running in your environment +# +# DEPRECATED / LEGACY - NO LONGER THE RECOMMENDED APPROACH +# ---------------------------------------------------------------------------- +# This script is retained for historical reference only. New deployments should +# use the cross-platform pre-built attestation tooling from: +# https://github.com/Azure/cvm-attestation-tools +# Releases provide ready-to-run binaries for both Linux (`attest-lin.zip`) and +# Windows (`attest-win.zip`), and support both AMD SEV-SNP (`config_snp.json`) +# and Intel TDX (`config_tdx.json`) Confidential VMs. `BuildRandomCVM.ps1` in +# this repo runs that flow automatically inside the freshly deployed CVM. +# ---------------------------------------------------------------------------- +# # Script to run on a Confidential Virtual machine (https://aka.ms/accdocs) to check if it is running on a Confidential VM (CVM) and attested by Azure Attestation service # More detailed version https://github.com/Azure/confidential-computing-cvm-guest-attestation/tree/main/cvm-platform-checker-exe # it will download a sample app from GitHub to get the JWT token from the attestation service and decode it # it will install + uninstall dependencies like VC Redist and JWTDetails (https://github.com/darrenjrobinson/JWTDetails) PowerShell module +Write-Warning "WindowsAttest.ps1 is deprecated. Prefer https://github.com/Azure/cvm-attestation-tools (attest-win.zip / attest-lin.zip) which supports both SEV-SNP and Intel TDX." + #force install of NuGet provider, otherwise script prompts for install on a 'fresh' VM Install-PackageProvider -Name NuGet -force write-output "Installing JWTDetails module"