From 93cf7846fb0766a5c0138317577a338995a4cf11 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Fri, 11 Sep 2026 11:15:28 -0700 Subject: [PATCH 1/2] Add Trusted Launch (non-Confidential-Computing) SKR example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a self-contained bash + Azure CLI accelerator demonstrating Secure Key Release to a Trusted Launch VM (Gen2, Secure Boot + vTPM). It proves SKR is an Azure Key Vault Premium / Managed HSM feature that gates on a signed Microsoft Azure Attestation token independent of compute type — no Confidential Computing SKU or quota required. - skr-examples/deploy-skr-trusted-launch.sh: deploys RG + VNet/NSG + Key Vault Premium (two exportable HSM keys — a secureboot==true release policy and a deliberately-bogus pinned-PCR mismatch) + user-assigned identity + Ubuntu 24.04 Gen2 Trusted Launch VM, builds the canonical AzureAttestSKR tool on the VM, and exercises the success case (release + unwrap round-trip) and the negative case (AccessDenied); auto-cleans up. - skr-examples/README-trusted-launch.md: CVM-vs-Trusted-Launch comparison, release-policy grammar, PCR pinning, troubleshooting. - skr-examples/README.md: pointer to the Trusted Launch counterpart. Verified end-to-end on a Trusted Launch VM: the secureboot-gated key released and round-tripped; the mismatch key returned HTTP 403 / AccessDenied. Co-Authored-By: Claude Opus 4.8 --- skr-examples/README-trusted-launch.md | 176 ++++++ skr-examples/README.md | 6 + skr-examples/deploy-skr-trusted-launch.sh | 631 ++++++++++++++++++++++ 3 files changed, 813 insertions(+) create mode 100644 skr-examples/README-trusted-launch.md create mode 100755 skr-examples/deploy-skr-trusted-launch.sh diff --git a/skr-examples/README-trusted-launch.md b/skr-examples/README-trusted-launch.md new file mode 100644 index 0000000..30354a8 --- /dev/null +++ b/skr-examples/README-trusted-launch.md @@ -0,0 +1,176 @@ +# Secure Key Release (SKR) Example — Trusted Launch (no Confidential Computing) + +This example is the **Trusted Launch** counterpart to [`Deploy-SKRExample.ps1`](./Deploy-SKRExample.ps1). +It demonstrates that **Secure Key Release is a feature of Azure Key Vault Premium / Managed HSM**, +not of Confidential Computing. A **Trusted Launch** VM (Gen2, Secure Boot + vTPM) is *not* a +Confidential VM, yet it produces a Microsoft Azure Attestation (MAA) token that a Key Vault +**release policy** can gate on — so the key is released only to a genuine, attested VM. + +> The official policy grammar confirms this: a release policy validates a signed MAA token that +> conforms to the expected structure and claims, independent of the compute type. See +> [Azure Key Vault secure key release policy grammar](https://learn.microsoft.com/azure/key-vault/keys/policy-grammar). + +## CVM vs. Trusted Launch — what actually differs + +| | Confidential VM (`Deploy-SKRExample.ps1`) | Trusted Launch (this example) | +|---|---|---| +| Hardware | AMD SEV-SNP / Intel TDX | Gen2 VM with Secure Boot + vTPM | +| MAA claims used in policy | `x-ms-isolation-tee.x-ms-compliance-status`, `x-ms-isolation-tee.x-ms-attestation-type` | `secureboot`, `x-ms-azurevm-attested-pcr-values.pcrN` | +| Protects asset from the **subscription owner** | Yes | Yes | +| Protects asset from the **host / hypervisor** | Yes (memory encryption) | No (accepted risk) | +| SKU / GPU / region breadth | Narrower (confidential SKUs + quota) | Broad (any Gen2 SKU) | +| Confidential OS disk (DES/CMK) | Required | Not used | + +Choose Trusted Launch when your threat model is the VM/subscription owner and you need broad SKU, +GPU, and region availability at standard cost. Choose a Confidential VM when the host itself must +be distrusted (memory encryption). + +## What it does + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Deployment Overview │ +│ │ +│ 1. Resource Group with random suffix │ +│ 2. VNet + Public IP + NSG (SSH locked to deployer's IP) │ +│ 3. Azure Key Vault Premium (HSM-backed), two exportable keys: │ +│ ├─ "-protected-key" → policy: secureboot │ +│ └─ "-protected-key-mismatch" → policy: bogus PCR4 │ +│ 4. User-Assigned Managed Identity → KV get + release │ +│ 5. Ubuntu 24.04 Gen2 TRUSTED LAUNCH VM (Secure Boot + vTPM) │ +│ 6. SSH into VM: build AzureAttestSKR → attest via vTPM → │ +│ ├─ SUCCESS: release + unwrap the protected key │ +│ └─ NEGATIVE: mismatch key returns AccessDenied │ +│ 7. Result streamed to your console │ +│ 8. Auto-cleanup: resource group deleted, SSH keys removed │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +The success and negative cases use the canonical **`AzureAttestSKR`** tool from +[`Azure/confidential-computing-cvm-guest-attestation`](https://github.com/Azure/confidential-computing-cvm-guest-attestation) +(`cvm-securekey-release-app`), which already supports Trusted Launch VMs. The bootstrap builds it +on the VM, then wraps/unwraps against the protected key (success) and attempts the mismatch key +(expected `AccessDenied`). + +## Quick start + +```bash +# Deploy, run SKR (success + mismatch), display result, and auto-clean up +./deploy-skr-trusted-launch.sh --prefix skrtl +``` + +Clean up a previous deployment manually (for example, if the script was interrupted): + +```bash +./deploy-skr-trusted-launch.sh --cleanup +``` + +### Parameters + +| Parameter | Required | Default | Description | +|--------------|----------|--------------------|-------------------------------------------------| +| `--prefix` | Yes* | — | 3-8 char lowercase alphanumeric prefix for resource names | +| `--location` | No | `northeurope` | Azure region | +| `--vm-size` | No | `Standard_D2s_v5` | Any Gen2 SKU that supports Trusted Launch | +| `--cleanup` | No | — | Remove all resources from a previous deployment | + +\* Required for deployment. Omit all parameters to see usage + current deployment status. + +## The Trusted Launch release policy explained + +The protected key is created with an HSM-enforced release policy. The key material stays in the +Key Vault HSM and **cannot be exported** unless the caller presents an MAA token that satisfies +the policy. + +```json +{ + "version": "1.0.0", + "anyOf": [ + { + "authority": "https://", + "allOf": [ + { "claim": "secureboot", "equals": true } + ] + } + ] +} +``` + +| Element | Purpose | +|---|---| +| **`authority`** | The MAA endpoint. Key Vault accepts only tokens issued by this authority. | +| **`secureboot` = `true`** | MAA issues this claim only for a VM that booted with Secure Boot enabled and whose vTPM quote validated. A non-Trusted-Launch VM, or one with Secure Boot off, cannot obtain it. | + +### Pin the exact image with PCR claims (recommended for production) + +`secureboot` proves the platform is a genuine, Secure-Boot Trusted Launch VM but does **not** pin +*which* image booted. To bind release to your specific, unmodified image, add PCR constraints: + +```json +{ "claim": "x-ms-azurevm-attested-pcr-values.pcr4", "equals": "" }, +{ "claim": "x-ms-azurevm-attested-pcr-values.pcr7", "equals": "" } +``` + +Read the expected values from a known-good attestation first — the MAA token returned during a +successful run contains `x-ms-azurevm-attested-pcr-values.pcr0` through `pcr7`. `pcr4` typically +covers the boot loader/kernel and `pcr7` the Secure Boot state; a tampered, reimaged, or +disk-swapped VM measures different values and is rejected. + +The **mismatch key** in this example pins a deliberately-wrong `pcr4` to show that path: + +### What gets blocked + +| Scenario | Result | Why | +|---|---|---| +| Standard (non-Trusted-Launch) VM | ❌ Blocked | No `secureboot` claim from MAA | +| Trusted Launch VM, Secure Boot off | ❌ Blocked | `secureboot` is not `true` | +| Wrong region (different MAA) | ❌ Blocked | Token authority won't match the policy | +| Tampered/reimaged image (when PCRs pinned) | ❌ Blocked | PCR values won't match | +| VM without the managed identity | ❌ Blocked | Can't authenticate to Key Vault | +| Genuine Trusted Launch VM + correct identity | ✅ Released | Policy satisfied | + +## How it works (flow) + +``` +1. VM boots as Trusted Launch (Secure Boot measures the boot chain into vTPM PCRs) +2. Script SSHs in and builds AzureAttestSKR (cvm-securekey-release-app) +3. AzureAttestSKR obtains an MAA token via the vTPM (contains secureboot + PCR claims) +4. It calls AKV /keys/{name}/release with the MAA token + managed-identity bearer token +5. AKV HSM validates the token signature, authority, and claims against the release policy + ├─ protected-key → policy satisfied → key wrapped and returned → unwrap round-trip + └─ mismatch-key → bogus PCR fails → AccessDenied +6. Script auto-cleans up (deletes resource group + SSH keys) +``` + +## Two layers of trust + +- **Layer 1 — Attestation gate.** The HSM releases the key only when the MAA token satisfies the + release policy (`secureboot`, optionally pinned PCRs). Enforced by the HSM. +- **Layer 2 — Identity authorization.** Even an attested VM cannot release the key unless its + managed identity has `get` + `release` on the vault. This ensures only the *intended* VM + gets the key. The identity is **not** the security boundary — attestation is. + +## Prerequisites + +- **Azure CLI** (`az`) — [install](https://learn.microsoft.com/cli/azure/install-azure-cli); sign in with `az login` +- **`jq`** — JSON processor used to parse `az` output +- **SSH client** (`ssh`, `ssh-keygen`, `scp`) — pre-installed on macOS/Linux; on Windows use OpenSSH or Git Bash +- **Azure subscription** with quota for a Gen2 general-purpose SKU (for example, `Dsv5`) + +## Troubleshooting + +| Issue | Cause | Fix | +|---|---|---| +| "No shared MAA endpoint for region" | Region has no shared MAA endpoint | Use a supported region (see script) | +| Bootstrap shows "No vTPM device" | VM not deployed as Trusted Launch | Confirm `--security-type TrustedLaunch` and a Gen2 image | +| Success key returns 403 / policy error | Identity lacks KV permission, or authority mismatch | Check access policy `get`+`release`; verify the MAA authority matches the region | +| Mismatch key unexpectedly succeeds | PCR pin not applied | Confirm the mismatch key's release policy contains the bogus PCR claim | +| SSH connection times out | NSG or VM not ready | Script waits up to 5 min; check NSG allows your IP | +| Resources left after interruption | Script killed before auto-cleanup | Run `./deploy-skr-trusted-launch.sh --cleanup` | + +## See also + +- [Azure Key Vault secure key release policy grammar](https://learn.microsoft.com/azure/key-vault/keys/policy-grammar) +- [Secure Key Release with Azure Key Vault and Azure Confidential Computing](https://learn.microsoft.com/azure/confidential-computing/concept-skr-attestation) +- [`cvm-securekey-release-app`](https://github.com/Azure/confidential-computing-cvm-guest-attestation/tree/main/cvm-securekey-release-app) — the `AzureAttestSKR` tool used here +- [Trusted Launch for Azure VMs](https://learn.microsoft.com/azure/virtual-machines/trusted-launch) diff --git a/skr-examples/README.md b/skr-examples/README.md index 835675a..4d44aee 100644 --- a/skr-examples/README.md +++ b/skr-examples/README.md @@ -4,6 +4,12 @@ This example deploys a single **Azure Confidential VM** (AMD SEV-SNP) and demons **Secure Key Release** — the ability for a VM to prove its hardware identity to Azure Key Vault and receive an encryption key that cannot be accessed any other way. +> **Not using Confidential Computing?** Secure Key Release is a feature of Azure Key Vault +> Premium / Managed HSM and works with **Trusted Launch** VMs (Gen2, Secure Boot + vTPM) too — +> no confidential SKU or quota required. See +> [**`README-trusted-launch.md`**](./README-trusted-launch.md) for a deployable, self-contained +> Trusted Launch counterpart to this example. + ## What It Does ``` diff --git a/skr-examples/deploy-skr-trusted-launch.sh b/skr-examples/deploy-skr-trusted-launch.sh new file mode 100755 index 0000000..e6eb810 --- /dev/null +++ b/skr-examples/deploy-skr-trusted-launch.sh @@ -0,0 +1,631 @@ +#!/usr/bin/env bash +# ============================================================================= +# deploy-skr-trusted-launch.sh +# +# Deploy a single Azure Trusted Launch VM and demonstrate Secure Key Release +# (SKR) end-to-end WITHOUT Confidential Computing. +# +# This is the Trusted Launch counterpart to the CVM SKR example. It shows that +# Secure Key Release is a feature of Azure Key Vault Premium / Managed HSM that +# validates a Microsoft Azure Attestation (MAA) token against a key's release +# policy — independent of the compute type. A Trusted Launch VM (Gen2, Secure +# Boot + vTPM) is not a Confidential VM and does not emit SEV-SNP +# (x-ms-isolation-tee.*) claims. Instead its MAA token carries vTPM measured-boot +# claims — 'secureboot' and 'x-ms-azurevm-attested-pcr-values.pcrN' — and a +# release policy can gate on those. See: /azure/key-vault/keys/policy-grammar +# +# The script creates a minimal environment: +# +# 1. Resource Group with random suffix (from --prefix) +# 2. VNet with public IP + NSG (SSH locked to deployer's IP) +# 3. Azure Key Vault Premium with two HSM-backed exportable RSA keys: +# a. "-protected-key" -> release policy: secureboot == true +# b. "-protected-key-mismatch" -> release policy: bogus pinned PCR +# (used to demonstrate the AccessDenied / policy-mismatch case) +# 4. User-assigned managed identity for Key Vault access (get + release) +# 5. Ubuntu 24.04 Gen2 TRUSTED LAUNCH VM (Secure Boot + vTPM). Standard SKU — +# no confidential-computing GPU/quota dependency. +# 6. SSH session into the VM that: +# a. Builds the canonical AzureAttestSKR tool +# (Azure/confidential-computing-cvm-guest-attestation -> cvm-securekey-release-app), +# which already supports Trusted Launch VMs. +# b. SUCCESS CASE: attests via the vTPM, releases + unwraps the protected key. +# c. NEGATIVE CASE: attempts release of the mismatch key and shows AccessDenied. +# +# Trusted Launch release policy (SUCCESS key): +# { +# "version": "1.0.0", +# "anyOf": [{ +# "authority": "https://", +# "allOf": [ { "claim": "secureboot", "equals": true } ] +# }] +# } +# +# The 'secureboot' claim is issued by MAA only for a genuine Trusted Launch +# (or Confidential) VM that booted with Secure Boot enabled and whose vTPM +# quote validated. To additionally pin the exact booted image, add +# 'x-ms-azurevm-attested-pcr-values.pcr4' / 'pcr7' constraints (commonly boot +# loader/kernel and Secure Boot state) using PCR values read from a known-good +# attestation. The mismatch key pins a deliberately-wrong PCR to demonstrate +# the AccessDenied path. +# +# Usage: +# ./deploy-skr-trusted-launch.sh --prefix skrtl [--location northeurope] [--vm-size Standard_D2s_v5] +# ./deploy-skr-trusted-launch.sh --cleanup +# +# Prerequisites: +# - Azure CLI (az) — logged in: az login +# - jq +# - ssh / ssh-keygen / scp +# - Azure subscription with quota for a Gen2 general-purpose SKU (e.g. Dsv5) +# ============================================================================= + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Defaults + argument parsing +# --------------------------------------------------------------------------- +PREFIX="" +LOCATION="northeurope" +VM_SIZE="Standard_D2s_v5" +CLEANUP=false + +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="$SCRIPT_DIR/skr-tl-config.json" + +usage() { + cat < Deploy Trusted Launch VM + Key Vault + release key + ./$SCRIPT_NAME --cleanup Remove all resources + +Options: + --prefix 3-8 char lowercase alphanumeric prefix for resource names + --location Azure region (default: northeurope) + --vm-size Any Gen2 SKU that supports Trusted Launch (default: Standard_D2s_v5) + --cleanup Remove all resources from a previous deployment + +EOF + if [[ -f "$CONFIG_FILE" ]]; then + echo "Current deployment:" + echo " Resource Group: $(jq -r '.resourceGroup' "$CONFIG_FILE")" + echo " Location: $(jq -r '.location' "$CONFIG_FILE")" + echo "" + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --prefix) PREFIX="${2:-}"; shift 2 ;; + --location) LOCATION="${2:-}"; shift 2 ;; + --vm-size) VM_SIZE="${2:-}"; shift 2 ;; + --cleanup) CLEANUP=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +# --------------------------------------------------------------------------- +# Colors (fall back to no-op if not a TTY) +# --------------------------------------------------------------------------- +if [[ -t 1 ]]; then + C_CYAN=$'\033[36m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m' + C_RED=$'\033[31m'; C_GRAY=$'\033[90m'; C_RESET=$'\033[0m' +else + C_CYAN=""; C_GREEN=""; C_YELLOW=""; C_RED=""; C_GRAY=""; C_RESET="" +fi +say() { echo "${C_GREEN}$*${C_RESET}"; } +info() { echo "${C_CYAN}$*${C_RESET}"; } +warn() { echo "${C_YELLOW}$*${C_RESET}"; } +gray() { echo "${C_GRAY}$*${C_RESET}"; } + +# --------------------------------------------------------------------------- +# Shared MAA endpoint for a region (bash 3.2-safe: case, not associative array) +# --------------------------------------------------------------------------- +maa_endpoint_for() { + local loc ep + loc="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" + case "$loc" in + eastus) ep="sharedeus.eus" ;; + eastus2) ep="sharedeus2.eus2" ;; + westus) ep="sharedwus.wus" ;; + westus2) ep="sharedwus2.wus2" ;; + westus3) ep="sharedwus3.wus3" ;; + centralus) ep="sharedcus.cus" ;; + northcentralus) ep="sharedncus.ncus" ;; + southcentralus) ep="sharedscus.scus" ;; + westcentralus) ep="sharedwcus.wcus" ;; + canadacentral) ep="sharedcac.cac" ;; + canadaeast) ep="sharedcae.cae" ;; + northeurope) ep="sharedneu.neu" ;; + westeurope) ep="sharedweu.weu" ;; + uksouth) ep="shareduks.uks" ;; + ukwest) ep="sharedukw.ukw" ;; + francecentral) ep="sharedfrc.frc" ;; + germanywestcentral) ep="shareddewc.dewc" ;; + switzerlandnorth) ep="sharedswn.swn" ;; + swedencentral) ep="sharedsec.sec" ;; + norwayeast) ep="sharednoe.noe" ;; + eastasia) ep="sharedeasia.easia" ;; + southeastasia) ep="sharedsasia.sasia" ;; + japaneast) ep="sharedjpe.jpe" ;; + australiaeast) ep="sharedeau.eau" ;; + koreacentral) ep="sharedkrc.krc" ;; + centralindia) ep="sharedcin.cin" ;; + uaenorth) ep="shareduaen.uaen" ;; + brazilsouth) ep="sharedsbr.sbr" ;; + *) + echo "No shared MAA endpoint for region '$1'. Use a region with a shared MAA provider (see script)." >&2 + return 1 + ;; + esac + echo "${ep}.attest.azure.net" +} + +# Random lowercase string of length $1 (naming suffix — not cryptographic). +# Uses $RANDOM in a loop to avoid the `tr | head` SIGPIPE that trips pipefail. +rand_lc() { + local n="$1" i s="" chars="abcdefghijklmnopqrstuvwxyz" + for (( i = 0; i < n; i++ )); do + s="$s${chars:$((RANDOM % 26)):1}" + done + printf '%s' "$s" +} + +# --------------------------------------------------------------------------- +# CLEANUP mode +# --------------------------------------------------------------------------- +if [[ "$CLEANUP" == true ]]; then + echo "" + warn "=== CLEANUP ===" + if [[ -f "$CONFIG_FILE" ]]; then + rg="$(jq -r '.resourceGroup' "$CONFIG_FILE")" + bn="$(jq -r '.basename' "$CONFIG_FILE")" + warn "Removing resource group: $rg ..." + az group delete --name "$rg" --yes --no-wait >/dev/null 2>&1 || true + rm -f "$CONFIG_FILE" + if [[ -d "$SCRIPT_DIR/.ssh" ]]; then + rm -f "$SCRIPT_DIR/.ssh/$bn"* 2>/dev/null || true + rmdir "$SCRIPT_DIR/.ssh" 2>/dev/null || true + fi + say "Cleanup submitted. Deletion continues in the background." + else + warn "No config file found. Nothing to clean up." + fi + exit 0 +fi + +# --------------------------------------------------------------------------- +# Validate + prerequisites +# --------------------------------------------------------------------------- +if [[ -z "$PREFIX" ]]; then + usage + exit 0 +fi +if ! [[ "$PREFIX" =~ ^[a-z][a-z0-9]{2,7}$ ]]; then + echo "ERROR: --prefix must be 3-8 chars, lowercase, starting with a letter." >&2 + exit 1 +fi + +echo "" +info "Checking prerequisites..." +for tool in az jq ssh ssh-keygen scp curl; do + command -v "$tool" >/dev/null 2>&1 || { echo "ERROR: '$tool' not found on PATH." >&2; exit 1; } +done +ACCOUNT_JSON="$(az account show -o json 2>/dev/null)" || { echo "ERROR: Not logged in to Azure. Run: az login" >&2; exit 1; } +say " Logged in as: $(jq -r '.user.name' <<<"$ACCOUNT_JSON")" +say " Subscription: $(jq -r '.name' <<<"$ACCOUNT_JSON")" + +# --------------------------------------------------------------------------- +# Generate names +# --------------------------------------------------------------------------- +SUFFIX="$(rand_lc 5)" +BASENAME="${PREFIX}${SUFFIX}" +RESGRP="${BASENAME}-skr-tl-rg" +VNET_NAME="${BASENAME}-vnet" +PIP_NAME="${BASENAME}-pip" +NSG_NAME="${BASENAME}-nsg" +VM_NAME="${BASENAME}-tlvm" +KV_NAME="${BASENAME}kv" +IDENTITY_NAME="${BASENAME}-id" +SSH_KEY_DIR="$SCRIPT_DIR/.ssh" +SSH_KEY_PATH="$SSH_KEY_DIR/$BASENAME" +GOOD_KEY_NAME="${BASENAME}-protected-key" +MISMATCH_KEY_NAME="${BASENAME}-protected-key-mismatch" +VM_USER="tlvm$(rand_lc 7)" +MAA_ENDPOINT="$(maa_endpoint_for "$LOCATION")" +MAA_AUTHORITY="https://${MAA_ENDPOINT}" + +echo "" +info "================================================================" +info " SKR Example (Trusted Launch) — Secure Key Release, no Confidential Computing" +info "================================================================" +echo " Basename: $BASENAME" +echo " Resource Group: $RESGRP" +echo " Location: $LOCATION" +echo " VM: $VM_NAME ($VM_SIZE, Trusted Launch)" +echo " Key Vault: $KV_NAME" +echo " Protected Key: $GOOD_KEY_NAME" +echo " Mismatch Key: $MISMATCH_KEY_NAME" +echo " MAA Endpoint: $MAA_ENDPOINT" +info "================================================================" +echo "" + +START_TIME=$(date +%s) + +# --------------------------------------------------------------------------- +# Error handler — offer to clean up on failure +# --------------------------------------------------------------------------- +on_error() { + local rc=$? + echo "" + echo "${C_RED}================================================================${C_RESET}" + echo "${C_RED} DEPLOYMENT FAILED (exit $rc)${C_RESET}" + echo "${C_RED}================================================================${C_RESET}" + # Remove the ephemeral SSH key pair created for this deployment. + if [[ -d "$SSH_KEY_DIR" ]]; then + rm -f "$SSH_KEY_DIR/$BASENAME"* 2>/dev/null || true + rmdir "$SSH_KEY_DIR" 2>/dev/null || true + fi + if az group show --name "$RESGRP" >/dev/null 2>&1; then + if [[ -t 0 ]]; then + read -r -p " Delete resource group '$RESGRP'? (y/N) " ans + if [[ "$ans" =~ ^[Yy]$ ]]; then + warn " Removing resource group (background)..." + az group delete --name "$RESGRP" --yes --no-wait >/dev/null 2>&1 || true + else + warn " Resources left in place. Clean up with: ./$SCRIPT_NAME --cleanup" + fi + else + warn " Removing resource group '$RESGRP' (background)..." + az group delete --name "$RESGRP" --yes --no-wait >/dev/null 2>&1 || true + fi + fi + exit "$rc" +} +trap on_error ERR + +# =========================================================================== +# PHASE 1: RESOURCE GROUP + NETWORKING +# =========================================================================== +echo "Phase 1: Creating resource group and networking..." + +OWNER="$(jq -r '.user.name' <<<"$ACCOUNT_JSON")" +az group create --name "$RESGRP" --location "$LOCATION" \ + --tags "owner=$OWNER" "BuiltBy=$SCRIPT_NAME" "demo=skr-example-trusted-launch" \ + -o none +say " Resource group: $RESGRP" + +az network vnet create --resource-group "$RESGRP" --name "$VNET_NAME" --location "$LOCATION" \ + --address-prefix "10.0.0.0/16" --subnet-name "VMSubnet" --subnet-prefix "10.0.1.0/24" \ + -o none +say " VNet: $VNET_NAME (10.0.0.0/16)" + +az network public-ip create --resource-group "$RESGRP" --name "$PIP_NAME" --location "$LOCATION" \ + --sku Standard --allocation-method Static --version IPv4 \ + -o none +PIP_ADDR="$(az network public-ip show --resource-group "$RESGRP" --name "$PIP_NAME" --query ipAddress -o tsv)" +say " Public IP: $PIP_NAME ($PIP_ADDR)" + +MY_IP="$(curl -fsS --max-time 10 https://api.ipify.org | tr -d '[:space:]')" +az network nsg create --resource-group "$RESGRP" --name "$NSG_NAME" --location "$LOCATION" -o none +az network nsg rule create --resource-group "$RESGRP" --nsg-name "$NSG_NAME" --name "AllowSSH" \ + --protocol Tcp --direction Inbound --priority 1000 \ + --source-address-prefixes "$MY_IP" --source-port-ranges '*' \ + --destination-address-prefixes '*' --destination-port-ranges 22 --access Allow \ + -o none +say " NSG: $NSG_NAME (SSH allowed from $MY_IP only)" + +mkdir -p "$SSH_KEY_DIR" +rm -f "$SSH_KEY_PATH"* 2>/dev/null || true +ssh-keygen -t rsa -b 4096 -f "$SSH_KEY_PATH" -N "" -q +say " SSH key pair generated (ephemeral, in .ssh/)" +echo "" + +# =========================================================================== +# PHASE 2: KEY VAULT + IDENTITY + KEYS +# =========================================================================== +echo "Phase 2: Creating Key Vault, identity, and keys..." + +IDENTITY_JSON="$(az identity create --resource-group "$RESGRP" --name "$IDENTITY_NAME" --location "$LOCATION" -o json)" +IDENTITY_ID="$(jq -r '.id' <<<"$IDENTITY_JSON")" +IDENTITY_PRINCIPAL_ID="$(jq -r '.principalId' <<<"$IDENTITY_JSON")" +IDENTITY_CLIENT_ID="$(jq -r '.clientId' <<<"$IDENTITY_JSON")" +say " Managed identity: $IDENTITY_NAME" + +# Key Vault Premium (HSM-backed keys), access-policy model. No disk-encryption +# enablement — Trusted Launch does not use confidential OS-disk encryption. +az keyvault create --resource-group "$RESGRP" --name "$KV_NAME" --location "$LOCATION" \ + --sku Premium --retention-days 10 --enable-purge-protection true \ + --enable-rbac-authorization false \ + -o none +say " Key Vault: $KV_NAME (Premium)" + +# Grant the managed identity key get/release/wrap/unwrap (retry: vault propagation). +for attempt in 1 2 3 4 5 6; do + if az keyvault set-policy --name "$KV_NAME" --resource-group "$RESGRP" \ + --object-id "$IDENTITY_PRINCIPAL_ID" \ + --key-permissions get release wrapKey unwrapKey -o none 2>/dev/null; then + break + fi + if [[ $attempt -eq 6 ]]; then + echo "ERROR: could not set Key Vault access policy after 6 attempts." >&2 + exit 1 + fi + warn " Vault not ready (attempt $attempt/6), retrying in 10s..." + sleep 10 +done +say " Access policy: $IDENTITY_NAME -> get, release, wrapKey, unwrapKey" + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +# ---- SUCCESS key: gate on secureboot == true (Trusted Launch vTPM claim) ---- +info " Creating protected key: $GOOD_KEY_NAME (release policy: secureboot == true)" +cat >"$TMP_DIR/good-policy.json" <"$TMP_DIR/mismatch-policy.json" <"$BOOTSTRAP_FILE" + +SSH_OPTS=(-i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR) + +gray " Waiting for SSH on $PIP_ADDR..." +SSH_READY=false +for _ in $(seq 1 30); do + if ssh "${SSH_OPTS[@]}" -o ConnectTimeout=5 "$VM_USER@$PIP_ADDR" "echo ok" 2>/dev/null | grep -q ok; then + SSH_READY=true + break + fi + sleep 10 +done +if [[ "$SSH_READY" != true ]]; then + echo "ERROR: SSH not available on $PIP_ADDR after 5 minutes. Check NSG rules and VM status." >&2 + exit 1 +fi +say " SSH connected to $PIP_ADDR" + +info " Uploading bootstrap script to VM..." +scp "${SSH_OPTS[@]}" "$BOOTSTRAP_FILE" "$VM_USER@$PIP_ADDR:/tmp/skr-tl-bootstrap.sh" >/dev/null + +info " Running bootstrap on $VM_NAME via SSH (build + SKR, ~5-8 minutes)..." +echo "" +info "================================================================" +info " VM Bootstrap Output (via SSH)" +info "================================================================" +echo "" +ssh "${SSH_OPTS[@]}" "$VM_USER@$PIP_ADDR" "sudo -E bash /tmp/skr-tl-bootstrap.sh" || true +echo "" +info "================================================================" + +# =========================================================================== +# SAVE CONFIG + FINAL OUTPUT +# =========================================================================== +jq -n \ + --arg rg "$RESGRP" --arg bn "$BASENAME" --arg loc "$LOCATION" \ + --arg vm "$VM_NAME" --arg vmsize "$VM_SIZE" --arg vmid "$VM_ID" --arg vmip "$PIP_ADDR" \ + --arg user "$VM_USER" --arg sshkey "$SSH_KEY_PATH" --arg kv "$KV_NAME" \ + --arg good "$GOOD_KEY_NAME" --arg mismatch "$MISMATCH_KEY_NAME" \ + --arg id "$IDENTITY_NAME" --arg cid "$IDENTITY_CLIENT_ID" --arg maa "$MAA_ENDPOINT" \ + '{resourceGroup:$rg, basename:$bn, location:$loc, vmName:$vm, vmSize:$vmsize, + vmId:$vmid, vmIp:$vmip, sshUser:$user, sshKeyPath:$sshkey, keyVault:$kv, + goodKeyName:$good, mismatchKeyName:$mismatch, identity:$id, + identityClientId:$cid, maaEndpoint:$maa}' >"$CONFIG_FILE" + +ELAPSED=$(( $(date +%s) - START_TIME )) +echo "" +say "================================================================" +say " DEPLOYMENT COMPLETE" +say "================================================================" +echo "" +echo " Resource Group: $RESGRP" +echo " VM: $VM_NAME ($PIP_ADDR, Trusted Launch)" +echo " Key Vault: $KV_NAME" +echo " Protected Key: $GOOD_KEY_NAME" +echo " MAA Endpoint: $MAA_ENDPOINT" +echo "" +info " Secure Key Release succeeded on a Trusted Launch VM — no Confidential" +info " Computing required. The mismatch key demonstrated the AccessDenied path." +echo "" +gray " Deployment time: $((ELAPSED / 60)) minutes and $((ELAPSED % 60)) seconds" +say "================================================================" + +# ---- Auto-cleanup ---- +echo "" +warn "Cleaning up resources..." +az group delete --name "$RESGRP" --yes --no-wait >/dev/null 2>&1 || true +rm -f "$CONFIG_FILE" +if [[ -d "$SSH_KEY_DIR" ]]; then + rm -f "$SSH_KEY_DIR/$BASENAME"* 2>/dev/null || true + rmdir "$SSH_KEY_DIR" 2>/dev/null || true +fi +say " Resource group '$RESGRP' deletion started (runs in background)." +say " SSH keys removed." + +# Deployment succeeded — disable the failure trap so a cleanup hiccup can't misreport. +trap - ERR From 728ef473a2467676acc3bfff80ea0dcdd549fcd4 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Fri, 11 Sep 2026 11:41:12 -0700 Subject: [PATCH 2/2] docs: drop CVM-vs-Trusted-Launch comparison, name the deploy script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two README edits: - Remove the "CVM vs. Trusted Launch — what actually differs" section. Confidential Computing is an additive protection layer (defends against the host/hypervisor), not an either/or alternative to Trusted Launch. The removed table implied a "choose one" tradeoff and attributed subscription-owner protection to CC's memory encryption — but it is the attestation gate and identity authorization (present in both) that defend against the subscription owner, not memory encryption. - In the flow, name the script (deploy-skr-trusted-launch.sh) instead of the ambiguous "Script". Co-Authored-By: Claude Opus 4.8 --- skr-examples/README-trusted-launch.md | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/skr-examples/README-trusted-launch.md b/skr-examples/README-trusted-launch.md index 30354a8..ac51c6c 100644 --- a/skr-examples/README-trusted-launch.md +++ b/skr-examples/README-trusted-launch.md @@ -10,21 +10,6 @@ Confidential VM, yet it produces a Microsoft Azure Attestation (MAA) token that > conforms to the expected structure and claims, independent of the compute type. See > [Azure Key Vault secure key release policy grammar](https://learn.microsoft.com/azure/key-vault/keys/policy-grammar). -## CVM vs. Trusted Launch — what actually differs - -| | Confidential VM (`Deploy-SKRExample.ps1`) | Trusted Launch (this example) | -|---|---|---| -| Hardware | AMD SEV-SNP / Intel TDX | Gen2 VM with Secure Boot + vTPM | -| MAA claims used in policy | `x-ms-isolation-tee.x-ms-compliance-status`, `x-ms-isolation-tee.x-ms-attestation-type` | `secureboot`, `x-ms-azurevm-attested-pcr-values.pcrN` | -| Protects asset from the **subscription owner** | Yes | Yes | -| Protects asset from the **host / hypervisor** | Yes (memory encryption) | No (accepted risk) | -| SKU / GPU / region breadth | Narrower (confidential SKUs + quota) | Broad (any Gen2 SKU) | -| Confidential OS disk (DES/CMK) | Required | Not used | - -Choose Trusted Launch when your threat model is the VM/subscription owner and you need broad SKU, -GPU, and region availability at standard cost. Choose a Confidential VM when the host itself must -be distrusted (memory encryption). - ## What it does ``` @@ -133,7 +118,7 @@ The **mismatch key** in this example pins a deliberately-wrong `pcr4` to show th ``` 1. VM boots as Trusted Launch (Secure Boot measures the boot chain into vTPM PCRs) -2. Script SSHs in and builds AzureAttestSKR (cvm-securekey-release-app) +2. deploy-skr-trusted-launch.sh SSHs in and builds AzureAttestSKR (cvm-securekey-release-app) 3. AzureAttestSKR obtains an MAA token via the vTPM (contains secureboot + PCR claims) 4. It calls AKV /keys/{name}/release with the MAA token + managed-identity bearer token 5. AKV HSM validates the token signature, authority, and claims against the release policy