diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md index 4dcaa812c..8ad87b08a 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md @@ -45,8 +45,8 @@ After completing this MicroHack you will: | 1 | Using Azure native platform controls (e.g. Policy, RBAC etc) to enforce sovereign controls in the public cloud | [Challenge](./challenges/challenge-01.md) | [Solution](./walkthrough/challenge-01/solution-01.md) | 45 min | Jan Egil Ring | | 2 | Encryption at rest with Customer Managed Keys in Azure Key Vault | [Challenge](./challenges/challenge-02.md) | [Solution](./walkthrough/challenge-02/solution-02.md) | 30 min | Ye Zhang | | 3 | Encryption in transit - enforcing TLS | [Challenge](./challenges/challenge-03.md) | [Solution](./walkthrough/challenge-03/solution-03.md) | 30 min | Ye Zhang | -| 4 | Encryption in use with Azure Confidential Compute - VM | [Challenge](./challenges/challenge-04.md) | [Solution](./walkthrough/challenge-04/solution-04.md) | 90-120 min | Murali Rao Yelamanchili | -| 5 | Encryption in use with Confidential VMs/Node Pools in Azure Kubernetes Service (AKS) | [Challenge](./challenges/challenge-05.md) | [Solution](./walkthrough/challenge-05/solution-05.md) | 90-120 min | Murali Rao Yelamanchili | +| 4 | Runtime attestation with Confidential Azure Container Instances | [Challenge](./challenges/challenge-04.md) | [Solution](./walkthrough/challenge-04/solution-04.md) | 30-45 min | Murali Rao Yelamanchili | +| 5 | Confidential workloads and runtime attestation on Azure Kubernetes Service (AKS) | [Challenge](./challenges/challenge-05.md) | [Solution](./walkthrough/challenge-05/solution-05.md) | 30-45 min | Murali Rao Yelamanchili | | 6 | Operating Sovereign in a hybrid environment with Azure Local and Azure Arc | [Challenge](./challenges/challenge-06.md) | [Solution](./walkthrough/challenge-06/solution-06.md) | 60-90 min | Jan Egil Ring / Thomas Maurer | ### General prerequisites @@ -69,12 +69,10 @@ The main cost driver for this MicroHack is virtual machines: - **ArcBox for ITPro** cost is approximately 7 USD per day. We recommend setting it up the week before the event, so for example 5 days before the event would result in a cost between 30-40 USD. - **LocalBox** cost is approximately 100-110 USD per day. We recommend setting it up the week before the event, so for example 5 days before the event would result in a cost between 5-600 USD. -- Challenge 4 and 5 contains a Confidential Compute VM (Standard_DC2as_v5) which costs approximately 5 USD per day. These 2 VMs will run only for a few hours as they will be created by the students, so using 50 students as an example running the VMs for 8 hours would results in 2 VMs x 8 hours = 230 USD. +- Challenge 4 uses two short-lived Azure Container Instances and a Basic Azure Container Registry. Challenge 5 creates one `Standard_D2as_v6` AKS system node and two `Standard_DC2as_v5` confidential nodes by default, plus two public LoadBalancer services. These resources run only for a few hours and should be deleted after each challenge. -This would result in a total cost of 789 USD. -In addition, there would be some smaller costs for other services like Key Vault, so a rough estimate is 1000 USD for one Sovereign Cloud MicroHack if following the above example. -An Azure Pricing Calculator estimate is available [here](https://azure.com/e/1a7aec76a3e049cba57cda6742025373). -This estimate can be adjusted for fewer/more students, running the VMs shorter/longer and adding additional services if desired. +The total depends on attendee count, runtime, and which optional environments are deployed. Smaller charges also apply for services such as Key Vault, ACI, and ACR. +An Azure Pricing Calculator estimate is available [here](https://azure.com/e/1a7aec76a3e049cba57cda6742025373) and should be adjusted for the current challenge design, attendee count, and runtime. If you plan to run this MicroHack in your own subscription on a limited budget, you may skip deploying the prerequisites for Challenge 6, this would leave you with a cost of less than 50 USD for one day as long as resources are deleted when finished with the challenges. diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/challenges/challenge-04.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/challenges/challenge-04.md index 40d9a270a..4b4d97ca9 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/challenges/challenge-04.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/challenges/challenge-04.md @@ -1,36 +1,42 @@ -# Challenge 4 - Encryption in use with Azure Confidential Compute – VM +# Challenge 4 - Runtime attestation with Confidential ACI [Previous Challenge](challenge-03.md) - **[Home](../Readme.md)** - [Next Challenge](challenge-05.md) ## Goal -Deploy and validate guest attestation on Azure Confidential VMs to ensure business logic only executes in trusted, compliant confidential computing environments. You'll build and deploy a sample application that implements secure attestation flows using Azure Confidential VMs. The application leverages Microsoft Azure Attestation (MAA) to validate VM integrity before executing protected business logic, demonstrating "encryption in use" capabilities. +Deploy the same visual attestation application to Confidential and Standard +Azure Container Instances (ACI). Use the side-by-side result to prove that +runtime attestation depends on AMD SEV-SNP hardware rather than application +logic alone. ## Actions -* Create Key Vault and SSH Keys -* Create Attestation Provider -* Create Virtual Network and Confidential VM -* Create Azure Bastion -* Configure Confidential VM (Run on the CVM) -* Review the attestation token output +- Build the visual attestation image server-side in Azure Container Registry. +- Generate a confidential-computing enforcement policy for the image. +- Deploy the image to Confidential and Standard ACI container groups. +- Run attestation in both applications and compare the results. +- Inspect the Microsoft Azure Attestation claims from the confidential instance. +- Remove only the resources created by this challenge. ## Success criteria -* Attestation token output proving the VM is running in a trusted confidential environment +- Confidential ACI returns a signed MAA token with `x-ms-attestation-type` set to `sevsnpvm`. +- The compliance status is `azure-compliant-uvm`. +- Standard ACI fails because `/dev/sev-guest` is unavailable. +- Both instances use the same container image. ## Learning resources -* [Azure confidential computing](https://learn.microsoft.com/azure/confidential-computing/) -* [Azure encryption overview](https://learn.microsoft.com/azure/security/fundamentals/encryption-overview) -* [Azure Confidential Computing Overview](https://learn.microsoft.com/azure/confidential-computing/overview) -* [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/overview) -* [About Azure confidential VMs](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) +- [Why Challenge 4 uses `confcom` and Docker](../walkthrough/challenge-04/CONFCOM-AND-CCE-POLICY.md) +- [Confidential containers on Azure Container Instances](https://learn.microsoft.com/azure/container-instances/container-instances-confidential-overview) +- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/overview) +- [Confidential computing enforcement policies](https://learn.microsoft.com/azure/container-instances/confidential-containers-attestation-concepts) +- [Source sample: Visual Attestation Demo v2](https://github.com/Azure/confidential-computing/tree/main/aci-samples/visual-attestation-demo-v2) ## Solution > [!TIP] -> We encourage you to try solving the challenge on your own before looking at the solution. This will help you learn and understand the concepts better. +> Try to identify which hardware evidence the application needs before opening the walkthrough.
Click here to view the solution diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/challenges/challenge-05.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/challenges/challenge-05.md index 0348a1dc0..8eda38bdb 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/challenges/challenge-05.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/challenges/challenge-05.md @@ -1,39 +1,40 @@ -# Challenge 5 - Encryption in use with Confidential VMs/Node Pools in Azure Kubernetes Service (AKS) +# Challenge 5 - Confidential workloads on AKS [Previous Challenge](challenge-04.md) - **[Home](../Readme.md)** - [Next Challenge](challenge-06.md) ## Goal -Deploy and validate guest attestation on Azure Confidential VMs to ensure business logic only executes in trusted, compliant confidential computing environments. You'll build and deploy a sample application that implements secure attestation flows using Confidential VMs in AKS. The application leverages Microsoft Azure Attestation (MAA) to validate VM integrity before executing protected business logic, demonstrating "encryption in use" capabilities. +Deploy the Azure Voting App and a runtime attestation UI to an Azure Kubernetes +Service cluster. Pin the user-facing workloads to an AMD SEV-SNP Confidential +VM node pool and verify the execution environment with a fresh MAA token. ## Actions -* Create an AKS Cluster -* Add a Confidential VM Node Pool -* Verify Node Pool Configuration -* Run Attestation Verification Sample -* Review the attestation token output +- Deploy an AKS cluster with managed identity and automatic upgrade channels. +- Add and label an AMD SEV-SNP Confidential VM node pool. +- Deploy the Azure Voting App to the confidential nodes. +- Deploy the newer runtime attestation UI to the same node pool. +- Verify pod placement and inspect the attestation claims. +- Remove the AKS cluster without deleting the shared attendee resource group. ## Success criteria -* Attestation token output proving the App is running in a trusted confidential environment +- Voting front-end pods run on nodes labelled `workload=confidential`. +- The voting application is reachable through its LoadBalancer service. +- The attestation UI returns an MAA-signed SEV-SNP token. +- The token reports `azure-compliant-cvm` and a non-debuggable TEE. ## Learning resources -* [Azure confidential computing](https://learn.microsoft.com/azure/confidential-computing/) -* [Azure encryption overview](https://learn.microsoft.com/azure/security/fundamentals/encryption-overview) -* [Azure Confidential Computing Overview](https://learn.microsoft.com/azure/confidential-computing/overview) -* [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/overview) -* [Confidential containers on Azure](https://learn.microsoft.com/azure/confidential-computing/confidential-containers) +- [When Confidential VM Orchestrator is required](../walkthrough/challenge-05/CVM-ORCHESTRATOR.md) +- [Use Confidential VMs in AKS](https://learn.microsoft.com/azure/aks/use-cvm) +- [AKS confidential computing overview](https://learn.microsoft.com/azure/aks/confidential-computing-overview) +- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/overview) +- [Source sample: Azure Voting App on confidential AKS nodes](https://github.com/Azure/confidential-computing/tree/main/aks-samples/azure-voting-app) ## Solution > [!TIP] -> We encourage you to try solving the challenge on your own before looking at the solution. This will help you learn and understand the concepts better. +> Before opening the walkthrough, decide how Kubernetes can constrain a workload to confidential nodes. -
-Click here to view the solution - -[Solution for Challenge 5](../walkthrough/challenge-05/solution-05.md) - -
+[Automated walkthrough for Challenge 5](../walkthrough/challenge-05/solution-05.md) diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/.gitignore b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/.gitignore new file mode 100644 index 000000000..f543cdf2d --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/.gitignore @@ -0,0 +1,2 @@ +/acr-config.json +/side-by-side-compare.html \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/CONFCOM-AND-CCE-POLICY.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/CONFCOM-AND-CCE-POLICY.md new file mode 100644 index 000000000..144a6220f --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/CONFCOM-AND-CCE-POLICY.md @@ -0,0 +1,159 @@ +# Why Challenge 4 uses `confcom` and Docker + +This supplement explains the Confidential Computing Enforcement (CCE) policy in +Challenge 4 from two perspectives. It is not an additional task and does not add +commands to the walkthrough. + +## The short answer + +For the custom Confidential Azure Container Instances (ACI) deployment used in +this challenge: + +- A CCE policy is part of the Azure confidential-container security model. +- Microsoft's supported tool for generating that policy is the Azure CLI + `confcom` extension. +- The policy generator uses a local Docker engine to inspect the exact container + image and calculate its permitted layer hashes. +- Docker is needed while preparing the deployment. It is not installed in the + ACI container group and is not needed by the application at runtime. +- Standard ACI does not use this CCE policy flow. That is why the Standard side + of the comparison can deploy without confidential-policy generation. + +## For a business reader + +### The problem being solved + +Memory encryption answers an important question: "Can the cloud host read the +workload's data while it is being processed?" + +It does not, by itself, answer another equally important question: "Is the +approved workload actually the software running inside that protected memory?" + +A malicious or accidental change could replace an image, alter its startup +command, add a mount, or inject an environment variable. Hardware might still +protect that changed workload from the host. The organization would have a +strong vault around the wrong program. + +The CCE policy closes that gap. It is a machine-enforced allow-list describing +what may run inside the trusted environment. Azure checks the requested +container group against that policy before allowing it to start. + +### Business value + +The policy turns "Azure says this is confidential hardware" into a more useful +assurance: "This approved workload is running on confidential hardware under +these approved conditions." + +That supports: + +- controlled processing of regulated or commercially sensitive information; +- collaboration where data owners do not fully trust every infrastructure + operator; +- release of secrets only after the environment proves its identity and policy; +- stronger change control for images, commands, mounts, and configuration; +- evidence that a relying party can evaluate before sharing data. + +### What happens without this mechanism + +Without a verifiable execution policy, confidential hardware can protect data +from the host but cannot prove that an unexpected workload was not substituted. +An image or configuration change could process or disclose data differently +while still benefiting from memory encryption. + +For Confidential ACI, policy enforcement is fail-closed. If the deployed image +or runtime settings do not match the generated policy, the container does not +start. The `deviceHash not found` error seen during testing is an example: the +policy described different image layers from those ACI attempted to mount. + +## For a technical reader + +### Standard ACI behavior + +A conventional ACI deployment contains an image reference and runtime settings +such as command, environment variables, mounts, capabilities, and ports. Azure +pulls the image and starts the requested container. Azure Resource Manager RBAC, +registry authorization, deployment history, and operational controls govern who +can change that configuration. + +Those controls are important, but they do not produce hardware-bound evidence +that the running container exactly matches an approved configuration. + +### What Confidential ACI adds + +Confidential ACI runs the container group in an AMD SEV-SNP protected utility VM. +The trusted execution environment protects data in use. A CCE policy adds +container-level integrity by declaring the components and actions the +confidential runtime permits. + +In this challenge, `az confcom acipolicygen`: + +1. Reads the temporary ARM template and its parameter file. +2. Resolves the container images through the local Docker engine. +3. Inspects and hashes the image layers. +4. Includes permitted commands, environment rules, mounts, capabilities, and + related runtime properties. +5. Produces a Rego policy encoded as Base64. +6. Writes that value into `confidentialComputeProperties.ccePolicy` in the + temporary template. + +Azure deploys the template and the confidential runtime enforces the policy. +The attestation evidence also binds the environment to the policy, allowing a +relying party to check the policy associated with the attested workload. + +### Is `confcom` an Azure requirement? + +For the custom CCE policy used by Confidential ACI, yes. Microsoft documents +that ACI CCE policies must be generated by the Azure CLI `confcom` extension and +must not be authored manually. `confcom` is deployment tooling; it is not an +agent running inside the application container. + +### Is Docker an Azure requirement? + +Docker is a prerequisite of the documented local `confcom` policy-generation +workflow, not a general requirement of Azure Resource Manager or the running +ACI workload. + +The distinction in this challenge is: + +| Operation | Local Docker required? | Why | +|-----------|------------------------|-----| +| `az acr build` | No | ACR builds the image remotely. | +| Standard ACI deployment | No | Standard ACI does not generate a CCE policy. | +| `az confcom acipolicygen` | Yes | The tool inspects and hashes image layers through a local Docker engine. | +| Running application in ACI | No | Azure runs the resulting image and enforces the submitted policy. | + +Azure Cloud Shell does not provide a Docker daemon, so Microsoft's documented +workflow requires Azure CLI and Docker Engine on a local computer. + +### Why cleanup matters before a rerun + +`confcom` can use a locally cached image. If an ACR tag points to newly built +content while Docker still holds older content under the same tag, policy hashes +can differ from the layers ACI pulls. Azure then rejects the container startup. + +Challenge 4 uses a simple lifecycle: + +```powershell +./Deploy-VisualAttestationV2.ps1 -Build +./Deploy-VisualAttestationV2.ps1 -Compare +./Deploy-VisualAttestationV2.ps1 -Cleanup +``` + +Cleanup removes the tagged local image as well as the Challenge 4 Azure +resources. Run it before rebuilding with the same workshop values. + +### What a CCE policy does not replace + +A CCE policy does not replace image vulnerability scanning, image signing, +software supply-chain controls, identity and access management, network +security, encryption at rest, or validation of the Microsoft Azure Attestation +token by a relying application. It supplies a specific control: enforce and +attest the allowed confidential-container runtime configuration. + +## Official references + +- [Confidential containers on Azure Container Instances](https://learn.microsoft.com/azure/container-instances/container-instances-confidential-overview) +- [Prepare a Confidential ACI deployment and generate a CCE policy](https://learn.microsoft.com/azure/container-instances/container-instances-tutorial-deploy-confidential-containers-cce-arm) +- [Attestation concepts for Confidential ACI](https://learn.microsoft.com/azure/container-instances/confidential-containers-attestation-concepts) +- [Troubleshoot Confidential ACI policy and image hash errors](https://learn.microsoft.com/azure/container-instances/container-instances-troubleshooting#issues-during-confidential-container-group-deployments) +- [`az confcom` command reference](https://learn.microsoft.com/cli/azure/confcom) diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/Deploy-VisualAttestationV2.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/Deploy-VisualAttestationV2.ps1 new file mode 100644 index 000000000..97f0e30ec --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/Deploy-VisualAttestationV2.ps1 @@ -0,0 +1,649 @@ +<# +.SYNOPSIS + Build and deploy the Visual Attestation Demo v2 web UI to Azure Container Instances. + +.DESCRIPTION + Builds the container image SERVER-SIDE in Azure Container Registry via + `az acr build` (no local Docker required), then deploys the resulting image + to Azure Container Instances on either the Confidential SKU (AMD SEV-SNP, + attestation succeeds) or Standard SKU (no TEE, attestation fails - shown + for educational comparison). This MicroHack adaptation reuses the attendee + resource group and removes only resources created by this challenge. + +.PARAMETER Build + Create an ACR in the existing attendee resource group and run `az acr build` + to produce the image. Run -Cleanup before repeating a completed or failed + Challenge 4 run with the same workshop values. + +.PARAMETER Deploy + Deploy a single container group. Confidential by default. Pass -NoAcc to + deploy on Standard SKU instead (so you can see attestation fail). + +.PARAMETER Compare + Deploy BOTH a confidential and a standard container side-by-side using + the already-built image. Confidential requires Docker + the confcom CLI + extension on the local machine for CCE policy generation. + +.PARAMETER Cleanup + Delete the two challenge container groups, ACR, generated state, and tagged + local Docker image. Retain the attendee resource group and resources created + by other challenges. + +.PARAMETER NoAcc + With -Deploy: use the Standard SKU template (attestation will fail). + +.PARAMETER SkipBrowser + Don't auto-open Edge after a successful deploy. + +.PARAMETER RegistryName + Optional ACR name to reuse. If omitted, a deterministic name is generated + from HASH_SUFFIX and persisted to acr-config.json. + +.PARAMETER ResourceGroup + Existing attendee resource group. Defaults to RESOURCE_GROUP. + +.PARAMETER AttendeeId + Attendee identifier used for resource tags. Defaults to ATTENDEE_ID. + +.PARAMETER HashSuffix + Stable suffix used for challenge resource names. Defaults to HASH_SUFFIX. + +.PARAMETER Location + Azure region. Defaults to LOCATION. + +.EXAMPLE + # Build the image once, deploy to Confidential ACI (attestation succeeds) + ./Deploy-VisualAttestationV2.ps1 -Build + ./Deploy-VisualAttestationV2.ps1 -Deploy + +.EXAMPLE + # Same image, deploy to Standard ACI (attestation FAILS - educational) + ./Deploy-VisualAttestationV2.ps1 -Deploy -NoAcc + +.EXAMPLE + # Side-by-side comparison + ./Deploy-VisualAttestationV2.ps1 -Compare + +.EXAMPLE + ./Deploy-VisualAttestationV2.ps1 -Cleanup +#> +[CmdletBinding(DefaultParameterSetName='Help')] +param( + [Parameter(ParameterSetName='Build')] [switch]$Build, + [Parameter(ParameterSetName='Deploy')] [switch]$Deploy, + [Parameter(ParameterSetName='Compare')] [switch]$Compare, + [Parameter(ParameterSetName='Cleanup')] [switch]$Cleanup, + + [Parameter(ParameterSetName='Deploy')] [switch]$NoAcc, + [Parameter(ParameterSetName='Deploy')] + [Parameter(ParameterSetName='Compare')] [switch]$SkipBrowser, + + [Parameter(ParameterSetName='Build')] [string]$RegistryName, + [Parameter(ParameterSetName='Build')] [string]$ResourceGroup = $env:RESOURCE_GROUP, + [Parameter(ParameterSetName='Build')] [string]$AttendeeId = $env:ATTENDEE_ID, + [Parameter(ParameterSetName='Build')] [string]$HashSuffix = $env:HASH_SUFFIX, + [string]$Location = $env:LOCATION +) + +# UTF-8 console for nicer output on Windows +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +$ErrorActionPreference = 'Stop' + +$ImageName = 'cc-attest' +$ImageTag = '1.0' +$ConfigPath = Join-Path $PSScriptRoot 'acr-config.json' +$SampleDirectory = Join-Path $PSScriptRoot 'resources/visual-attestation-demo-v2' + +# ============================================================================ +# Helpers +# ============================================================================ +function Write-Header { param($m) Write-Host ""; Write-Host ("=" * 72) -ForegroundColor Cyan; Write-Host $m -ForegroundColor Cyan; Write-Host ("=" * 72) -ForegroundColor Cyan } +function Write-Success { param($m) Write-Host "[OK] $m" -ForegroundColor Green } +function Write-Warn2 { param($m) Write-Host "[WARN] $m" -ForegroundColor Yellow } +function Write-Err2 { param($m) Write-Host "[ERROR] $m" -ForegroundColor Red } + +function Get-Config { + if (-not (Test-Path $ConfigPath)) { return $null } + return Get-Content $ConfigPath -Raw | ConvertFrom-Json +} +function Save-Config { param($cfg) $cfg | ConvertTo-Json -Depth 10 | Set-Content $ConfigPath -Encoding UTF8 } + +function Test-AzCli { + $v = az version 2>$null + if ($LASTEXITCODE -ne 0) { throw "Azure CLI not found. Install from https://aka.ms/azcli" } + $acct = az account show 2>$null | ConvertFrom-Json + if (-not $acct) { throw "Not logged in. Run: az login" } + Write-Host "Subscription: $($acct.name) ($($acct.id))" +} + +# ============================================================================ +# Build phase +# ============================================================================ +function Invoke-Build { + Write-Header "Build phase - creating ACR and building image server-side" + Test-AzCli + + if (Test-Path $ConfigPath) { + throw "An existing Challenge 4 deployment is recorded. Run .\Deploy-VisualAttestationV2.ps1 -Cleanup before rebuilding." + } + + foreach ($requiredValue in @{ + RESOURCE_GROUP = $ResourceGroup + ATTENDEE_ID = $AttendeeId + HASH_SUFFIX = $HashSuffix + LOCATION = $Location + }.GetEnumerator()) { + if ([string]::IsNullOrWhiteSpace($requiredValue.Value)) { + throw "$($requiredValue.Key) is not set. Define the MicroHack environment variables before running -Build." + } + } + + $nameSuffix = (($HashSuffix -replace '[^a-zA-Z0-9]', '').ToLowerInvariant()) + if ($nameSuffix.Length -gt 10) { $nameSuffix = $nameSuffix.Substring(0, 10) } + if (-not $nameSuffix) { throw 'HASH_SUFFIX must contain at least one letter or number.' } + + if (-not $RegistryName) { + $RegistryName = "ccattest$nameSuffix" + } + + Write-Host "Resource Group: $ResourceGroup" + Write-Host "Registry : $RegistryName" + Write-Host "Location : $Location" + Write-Host "Image : ${ImageName}:${ImageTag}" + Write-Host "" + + Write-Host "Checking existing attendee resource group..." + az group show --name $ResourceGroup | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Resource group '$ResourceGroup' was not found" } + + Write-Host "Creating ACR (Basic, admin user enabled)..." + az acr create --resource-group $ResourceGroup --name $RegistryName --location $Location ` + --sku Basic --admin-enabled true --tags "AttendeeId=$AttendeeId" "Challenge=04" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "az acr create failed" } + + Write-Host "Running az acr build (server-side Docker build, no local daemon needed)..." + Write-Host "This typically takes 4-6 minutes (apt-get + cvm-attestation-tools clone + pip install)..." + az acr build --registry $RegistryName --image "${ImageName}:${ImageTag}" ` + --file (Join-Path $SampleDirectory 'Dockerfile') --no-logs $SampleDirectory + if ($LASTEXITCODE -ne 0) { + # Verify the image actually exists - some az acr build calls return non-zero on warnings + az acr repository show --name $RegistryName --image "${ImageName}:${ImageTag}" 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { throw "az acr build failed" } + } + Write-Success "Image built and pushed to ACR" + + $loginServer = az acr show --name $RegistryName --query loginServer -o tsv + $cfg = [pscustomobject]@{ + registryName = $RegistryName + resourceGroup = $ResourceGroup + location = $Location + loginServer = $loginServer + imageName = $ImageName + imageTag = $ImageTag + fullImage = "$loginServer/${ImageName}:${ImageTag}" + nameSuffix = $nameSuffix + attendeeId = $AttendeeId + } + Save-Config $cfg + + Write-Header "Build complete" + Write-Host "Image: $($cfg.fullImage)" + Write-Host "Config saved to: $ConfigPath" + Write-Host "" + Write-Host "Next step:" + Write-Host " ./Deploy-VisualAttestationV2.ps1 -Deploy # Confidential SKU" + Write-Host " ./Deploy-VisualAttestationV2.ps1 -Deploy -NoAcc # Standard SKU" + Write-Host " ./Deploy-VisualAttestationV2.ps1 -Compare # both side-by-side" +} + +# ============================================================================ +# Deploy helpers +# ============================================================================ +function Get-AcrCreds { + param($cfg) + $u = az acr credential show --name $cfg.registryName --query username -o tsv + $p = az acr credential show --name $cfg.registryName --query "passwords[0].value" -o tsv + if (-not $u -or -not $p) { throw "Failed to read ACR credentials" } + return @{ Username = $u; Password = $p } +} + +function New-ParamsFile { + param($Path, $Name, $Image, $Server, $User, $Pass, $Dns) + $obj = @{ + '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' + 'contentVersion' = '1.0.0.0' + 'parameters' = @{ + containerGroupName = @{ value = $Name } + location = @{ value = $Location } + appImage = @{ value = $Image } + registryServer = @{ value = $Server } + registryUsername = @{ value = $User } + registryPassword = @{ value = $Pass } + dnsNameLabel = @{ value = $Dns } + } + } + $obj | ConvertTo-Json -Depth 10 | Set-Content $Path -Encoding UTF8 +} + +function Invoke-AcrLoginForConfcom { + param($cfg, $creds) + Write-Host "Logging into ACR (required for confcom CCE policy generation)..." + az acr login --name $cfg.loginServer --username $creds.Username --password $creds.Password 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + docker login $cfg.loginServer -u $creds.Username -p $creds.Password 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { throw "ACR login failed" } + } +} + +function Test-ConfidentialPrereqs { + Write-Host "Checking prerequisites for Confidential ACI deploy..." + docker info 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Docker is not running. Required by 'az confcom acipolicygen' for CCE policy generation. Start Docker Desktop, or pass -NoAcc to deploy on Standard SKU." + } + az extension show -n confcom 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "Installing 'confcom' Azure CLI extension..." + az extension add -n confcom | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Failed to install confcom extension" } + } + Write-Success "Docker running, confcom extension installed" +} + +function Wait-ForContainer { + param($Fqdn, $TimeoutSec = 240) + Write-Host "Waiting for http://$Fqdn (timeout ${TimeoutSec}s)..." + $sw = [System.Diagnostics.Stopwatch]::StartNew() + while ($sw.Elapsed.TotalSeconds -lt $TimeoutSec) { + try { + $r = Invoke-WebRequest -Uri "http://$Fqdn" -Method Head -TimeoutSec 5 -ErrorAction SilentlyContinue + if ($r.StatusCode -eq 200) { Write-Success "Container responding"; return $true } + } catch { } + Start-Sleep -Seconds 5 + Write-Host (" ... {0:n0}s elapsed" -f $sw.Elapsed.TotalSeconds) + } + Write-Warn2 "Container did not respond within ${TimeoutSec}s. It may still be pulling the image - try the URL in a browser." + return $false +} + +function New-ComparePage { + param( + [string]$ConfidentialUrl, + [string]$StandardUrl, + [string]$OutputPath + ) + + $html = @" + + + + + + ACI Attestation Compare + + + +
+

ACI Runtime Attestation Compare

+
+ Left: Confidential SKU (expected attestation success) + Right: Standard SKU (expected attestation failure) +
+
+ +
+
+
+
Confidential
+
+ SEV-SNP expected + $ConfidentialUrl +
+
+ +
+ +
+
+
Standard
+
+ Expected /dev/sev-guest missing + $StandardUrl +
+
+ +
+
+ + +"@ + + Set-Content -Path $OutputPath -Value $html -Encoding UTF8 +} + +# ============================================================================ +# Deploy phase (single container group) +# ============================================================================ +function Invoke-Deploy { + if ($NoAcc) { Write-Header "Deploy phase - STANDARD SKU (attestation will FAIL)" } + else { Write-Header "Deploy phase - CONFIDENTIAL SKU (AMD SEV-SNP)" } + Test-AzCli + + $cfg = Get-Config + if (-not $cfg) { throw "acr-config.json not found. Run with -Build first." } + + $creds = Get-AcrCreds $cfg + $skuName = if ($NoAcc) { 'standard' } else { 'confidential' } + $workDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "microhack-ch04-$($cfg.nameSuffix)-$skuName" + New-Item -ItemType Directory -Path $workDirectory -Force | Out-Null + $template = Join-Path $workDirectory 'template.json' + $params = Join-Path $workDirectory 'parameters.json' + + if ($NoAcc) { + Write-Warn2 "*** Standard SKU mode - no TEE, no vTPM, attestation WILL fail ***" + $name = "cc-attest-std-$($cfg.nameSuffix)" + $dns = $name + Copy-Item (Join-Path $SampleDirectory 'deployment-template-standard.json') $template -Force + New-ParamsFile -Path $params -Name $name -Image $cfg.fullImage ` + -Server $cfg.loginServer -User $creds.Username -Pass $creds.Password -Dns $dns + } + else { + Test-ConfidentialPrereqs + Invoke-AcrLoginForConfcom $cfg $creds + + $name = "cc-attest-conf-$($cfg.nameSuffix)" + $dns = $name + Copy-Item (Join-Path $SampleDirectory 'deployment-template-confidential.json') $template -Force + New-ParamsFile -Path $params -Name $name -Image $cfg.fullImage ` + -Server $cfg.loginServer -User $creds.Username -Pass $creds.Password -Dns $dns + + $templateJson = Get-Content $template -Raw | ConvertFrom-Json + $containerGroup = @($templateJson.resources | Where-Object { $_.type -eq 'Microsoft.ContainerInstance/containerGroups' })[0] + $containerGroup.properties.confidentialComputeProperties.ccePolicy = '' + $templateJson | ConvertTo-Json -Depth 100 | Set-Content $template -Encoding UTF8 + + Write-Host "Generating CCE policy via 'az confcom acipolicygen' (this can take a few minutes)..." + az confcom acipolicygen -a $template --parameters $params --disable-stdio --approve-wildcards + if ($LASTEXITCODE -ne 0) { throw "az confcom acipolicygen failed" } + Write-Success "CCE policy injected into $template" + } + + Write-Host "Container group: $name" + Write-Host "DNS label : $dns" + Write-Host "Image : $($cfg.fullImage)" + Write-Host "" + + Write-Host "Submitting ARM deployment..." + az deployment group create ` + --resource-group $cfg.resourceGroup ` + --template-file $template ` + --parameters "@$params" ` + --query "properties.outputs.fqdn.value" -o tsv | Tee-Object -Variable fqdn | Out-Null + if ($LASTEXITCODE -ne 0) { throw "az deployment group create failed" } + Write-Success "Deployed: http://$fqdn" + + Wait-ForContainer -Fqdn $fqdn | Out-Null + + if ($NoAcc) { + Write-Header "Diagnostics (Standard SKU - attestation expected to fail)" + Write-Host "Last 25 lines of container logs:" + az container logs --resource-group $cfg.resourceGroup --name $name --container-name cc-attest 2>&1 | + Select-Object -Last 25 | ForEach-Object { Write-Host " $_" } + Write-Host "" + Write-Host "Click the 'Run Attestation' button in the UI - it will return an error explaining" + Write-Host "that no vTPM / no SEV-SNP hardware report is available on Standard SKU." + } + + if (-not $SkipBrowser) { + Start-Process "http://$fqdn" + } + + Write-Host "" + Write-Host "To view logs: az container logs -g $($cfg.resourceGroup) -n $name --container-name cc-attest" + Write-Host "To delete: az container delete -g $($cfg.resourceGroup) -n $name --yes" +} + +# ============================================================================ +# Compare phase (both side-by-side) +# ============================================================================ +function Invoke-Compare { + Write-Header "Compare phase - Confidential AND Standard side-by-side" + Test-AzCli + + $cfg = Get-Config + if (-not $cfg) { throw "acr-config.json not found. Run with -Build first." } + + Test-ConfidentialPrereqs + $creds = Get-AcrCreds $cfg + Invoke-AcrLoginForConfcom $cfg $creds + + $name_conf = "cc-attest-conf-$($cfg.nameSuffix)" + $name_std = "cc-attest-std-$($cfg.nameSuffix)" + $dns_conf = $name_conf + $dns_std = $name_std + + $workDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "microhack-ch04-$($cfg.nameSuffix)-compare" + New-Item -ItemType Directory -Path $workDirectory -Force | Out-Null + $tpl_conf = Join-Path $workDirectory 'template-confidential.json' + $tpl_std = Join-Path $workDirectory 'template-standard.json' + $par_conf = Join-Path $workDirectory 'parameters-confidential.json' + $par_std = Join-Path $workDirectory 'parameters-standard.json' + Copy-Item (Join-Path $SampleDirectory 'deployment-template-confidential.json') $tpl_conf -Force + Copy-Item (Join-Path $SampleDirectory 'deployment-template-standard.json') $tpl_std -Force + + New-ParamsFile -Path $par_conf -Name $name_conf -Image $cfg.fullImage ` + -Server $cfg.loginServer -User $creds.Username -Pass $creds.Password -Dns $dns_conf + New-ParamsFile -Path $par_std -Name $name_std -Image $cfg.fullImage ` + -Server $cfg.loginServer -User $creds.Username -Pass $creds.Password -Dns $dns_std + + $templateJson = Get-Content $tpl_conf -Raw | ConvertFrom-Json + $containerGroup = @($templateJson.resources | Where-Object { $_.type -eq 'Microsoft.ContainerInstance/containerGroups' })[0] + $containerGroup.properties.confidentialComputeProperties.ccePolicy = '' + $templateJson | ConvertTo-Json -Depth 100 | Set-Content $tpl_conf -Encoding UTF8 + + Write-Host "Generating CCE policy for confidential container..." + az confcom acipolicygen -a $tpl_conf --parameters $par_conf --disable-stdio --approve-wildcards + if ($LASTEXITCODE -ne 0) { throw "az confcom acipolicygen failed" } + Write-Success "CCE policy generated" + + Write-Header "Deploying CONFIDENTIAL container ($name_conf)" + az deployment group create --resource-group $cfg.resourceGroup ` + --template-file $tpl_conf --parameters "@$par_conf" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Confidential deploy failed" } + $fqdn_conf = az container show -g $cfg.resourceGroup -n $name_conf --query "ipAddress.fqdn" -o tsv + Write-Success "Confidential: http://$fqdn_conf" + + Write-Header "Deploying STANDARD container ($name_std)" + az deployment group create --resource-group $cfg.resourceGroup ` + --template-file $tpl_std --parameters "@$par_std" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Standard deploy failed" } + $fqdn_std = az container show -g $cfg.resourceGroup -n $name_std --query "ipAddress.fqdn" -o tsv + Write-Success "Standard : http://$fqdn_std" + + Write-Host "" + Write-Host "Waiting for both containers to come up..." + Wait-ForContainer -Fqdn $fqdn_conf | Out-Null + Wait-ForContainer -Fqdn $fqdn_std | Out-Null + + Write-Header "Both containers deployed" + + # Retrieve SKU info + $confSku = az container show -g $cfg.resourceGroup -n $name_conf --query "sku" -o tsv + $stdSku = az container show -g $cfg.resourceGroup -n $name_std --query "sku" -o tsv + + # Output summary table + Write-Host "" + Write-Host "╔════════════════════════════════════════════════════════════════╗" + Write-Host "║ Compare Deployment Summary ║" + Write-Host "╠════════════════════════════════════════════════════════════════╣" + Write-Host "║ Confidential SKU (Attestation succeeds) ║" + Write-Host "║ SKU: $($confSku.PadRight(54)) ║" + Write-Host "║ URL: http://$($fqdn_conf.PadRight(49)) ║" + Write-Host "╠════════════════════════════════════════════════════════════════╣" + Write-Host "║ Standard SKU (Attestation FAILS) ║" + Write-Host "║ SKU: $($stdSku.PadRight(54)) ║" + Write-Host "║ URL: http://$($fqdn_std.PadRight(49)) ║" + Write-Host "╚════════════════════════════════════════════════════════════════╝" + Write-Host "" + Write-Host "Click 'Attest' on each pane to see the difference:" + Write-Host " • Confidential: Hardware-rooted SEV-SNP attestation succeeds" + Write-Host " • Standard: Attestation fails (no /dev/sev-guest)" + Write-Host "" + + if (-not $SkipBrowser) { + $confUrl = "http://$fqdn_conf" + $stdUrl = "http://$fqdn_std" + $comparePage = Join-Path $PSScriptRoot 'side-by-side-compare.html' + New-ComparePage -ConfidentialUrl $confUrl -StandardUrl $stdUrl -OutputPath $comparePage + Write-Success "Generated: $comparePage" + Write-Host "Opening in new browser window..." + Start-Process $comparePage + } +} + +# ============================================================================ +# Cleanup phase +# ============================================================================ +function Invoke-Cleanup { + Write-Header "Cleanup phase" + $cfg = Get-Config + if (-not $cfg) { Write-Warn2 "No acr-config.json - nothing to delete."; return } + + Write-Warn2 "Removing Challenge 4 resources from '$($cfg.resourceGroup)'." + foreach ($containerName in @("cc-attest-conf-$($cfg.nameSuffix)", "cc-attest-std-$($cfg.nameSuffix)")) { + az container show --resource-group $cfg.resourceGroup --name $containerName 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + az container delete --resource-group $cfg.resourceGroup --name $containerName --yes | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Failed to delete container group '$containerName'" } + Write-Success "Deleted container group: $containerName" + } + } + + az acr show --resource-group $cfg.resourceGroup --name $cfg.registryName 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + az acr delete --resource-group $cfg.resourceGroup --name $cfg.registryName --yes | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Failed to delete ACR '$($cfg.registryName)'" } + Write-Success "Deleted ACR: $($cfg.registryName)" + } + + if (Get-Command docker -ErrorAction SilentlyContinue) { + docker image rm $cfg.fullImage 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { Write-Success "Removed local image: $($cfg.fullImage)" } + } + + Remove-Item $ConfigPath -Force -ErrorAction SilentlyContinue + Remove-Item (Join-Path $PSScriptRoot 'side-by-side-compare.html') -Force -ErrorAction SilentlyContinue + Get-ChildItem ([System.IO.Path]::GetTempPath()) -Directory -Filter "microhack-ch04-$($cfg.nameSuffix)-*" -ErrorAction SilentlyContinue | + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + Write-Success "Cleanup complete. Resource group '$($cfg.resourceGroup)' was retained." +} + +# ============================================================================ +# Main +# ============================================================================ +switch ($PSCmdlet.ParameterSetName) { + 'Build' { Invoke-Build } + 'Deploy' { Invoke-Deploy } + 'Compare' { Invoke-Compare } + 'Cleanup' { Invoke-Cleanup } + default { + Get-Help $PSCommandPath -Detailed + } +} diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/UPSTREAM-SOURCE.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/UPSTREAM-SOURCE.md new file mode 100644 index 000000000..3429d86b6 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/UPSTREAM-SOURCE.md @@ -0,0 +1,40 @@ +# Challenge 4 upstream source + +The local [`resources/visual-attestation-demo-v2`](resources/visual-attestation-demo-v2/README.md) +directory is a byte-for-byte snapshot of the Azure Confidential Computing +Visual Attestation Demo v2 sample. + +- Repository: +- Source path: `aci-samples/visual-attestation-demo-v2` +- Source commit: [`62fea0b9ce0ead5b615a3f8d19fd6d27a15cb29d`](https://github.com/Azure/confidential-computing/tree/62fea0b9ce0ead5b615a3f8d19fd6d27a15cb29d/aci-samples/visual-attestation-demo-v2) +- Source commit date: 2026-07-13 + +Do not edit the snapshot when making MicroHack-specific changes. The +top-level `Deploy-VisualAttestationV2.ps1` starts from the upstream script and +contains the workshop adaptations. + +> [!IMPORTANT] +> Run the top-level script from the Challenge 4 walkthrough directory. The +> script inside `resources/visual-attestation-demo-v2` is the unchanged +> upstream reference and retains the upstream resource-group lifecycle. + +## MicroHack adaptations + +1. Read `RESOURCE_GROUP`, `ATTENDEE_ID`, `HASH_SUFFIX`, and `LOCATION` from the + established MicroHack environment variables. +2. Reuse the attendee resource group instead of creating or deleting a resource + group. +3. Derive deterministic ACR, container-group, and DNS names from `HASH_SUFFIX`. +4. Copy ARM templates and generated parameter files to a temporary directory so + credentials and generated policy changes are not written into the snapshot. +5. Clear the policy only in the temporary confidential template and approve + wildcard prompts so `confcom` can run non-interactively. +6. Delete only the two Challenge 4 container groups and ACR during cleanup. +7. Require cleanup before another build is recorded with the same workshop + values. +8. Remove the tagged local Docker image during cleanup so a later clean run + cannot generate a CCE policy from stale local layers. + +When updating the sample, replace the complete snapshot from one upstream +commit, update the commit above, and review the top-level script diff against +the new upstream `Deploy-VisualAttestationV2.ps1`. \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/01-build-phase-start.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/01-build-phase-start.png new file mode 100644 index 000000000..17f937ccb Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/01-build-phase-start.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/02-build-complete.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/02-build-complete.png new file mode 100644 index 000000000..ee11073e0 Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/02-build-complete.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/03-compare-deployment-summary.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/03-compare-deployment-summary.png new file mode 100644 index 000000000..6cb6a74ad Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/03-compare-deployment-summary.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/04-side-by-side-attestation.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/04-side-by-side-attestation.png new file mode 100644 index 000000000..6a0e38ebc Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/04-side-by-side-attestation.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-default-provider.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-default-provider.png deleted file mode 100644 index 2472077b6..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-default-provider.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-provider.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-provider.png deleted file mode 100644 index 84298a94d..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-provider.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-specified-provider.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-specified-provider.png deleted file mode 100644 index eb65fa510..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-specified-provider.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-workflow.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-workflow.png deleted file mode 100644 index d32b1349a..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/attestation-workflow.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/azbastion-create.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/azbastion-create.png deleted file mode 100644 index 80e68513d..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/azbastion-create.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/azbastion-resource.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/azbastion-resource.png deleted file mode 100644 index 5fba179cb..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/azbastion-resource.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/create-attest-service.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/create-attest-service.png deleted file mode 100644 index d0b80921c..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/create-attest-service.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/create-resource-group.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/create-resource-group.png deleted file mode 100644 index 644b94d53..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/create-resource-group.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-access-config.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-access-config.png deleted file mode 100644 index 5efcb2359..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-access-config.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-add-role-assignment.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-add-role-assignment.png deleted file mode 100644 index 55921e1af..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-add-role-assignment.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-cloud-shell-activate.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-cloud-shell-activate.png deleted file mode 100644 index df0857004..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-cloud-shell-activate.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-cloud-shell-activated.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-cloud-shell-activated.png deleted file mode 100644 index 26289094d..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-cloud-shell-activated.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-create-a-key.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-create-a-key.png deleted file mode 100644 index 1fe03c1a1..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-create-a-key.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-create.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-create.png deleted file mode 100644 index 843829186..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-create.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-gen-ssh-key-pair.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-gen-ssh-key-pair.png deleted file mode 100644 index 28e07228e..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-gen-ssh-key-pair.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-generate-key.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-generate-key.png deleted file mode 100644 index d715bbc81..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-generate-key.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-rbac-choose-secrets-officer.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-rbac-choose-secrets-officer.png deleted file mode 100644 index e4aa61911..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-rbac-choose-secrets-officer.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-rbac-select-members.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-rbac-select-members.png deleted file mode 100644 index bed086ee3..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-rbac-select-members.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-copy-public-key.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-copy-public-key.png deleted file mode 100644 index 548116dd6..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-copy-public-key.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-set-privkey.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-set-privkey.png deleted file mode 100644 index c7ef6770f..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-set-privkey.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-set-pubkey.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-set-pubkey.png deleted file mode 100644 index cfa5570ea..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/kv-secret-set-pubkey.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/portal-attestation-provider-copy-uri.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/portal-attestation-provider-copy-uri.png deleted file mode 100644 index 072a9d4af..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/portal-attestation-provider-copy-uri.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/portal-attestation-service.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/portal-attestation-service.png deleted file mode 100644 index 7aec64a8b..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/portal-attestation-service.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/review-create-attest-service.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/review-create-attest-service.png deleted file mode 100644 index 37d9c5ce5..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/review-create-attest-service.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/select-resource-groups.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/select-resource-groups.png deleted file mode 100644 index 0b0c0316e..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/select-resource-groups.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-attest-client-inspect-output.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-attest-client-inspect-output.png deleted file mode 100644 index 5f33f56a8..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-attest-client-inspect-output.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-attest-client-inspect-output2.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-attest-client-inspect-output2.png deleted file mode 100644 index 37fc75ae6..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-attest-client-inspect-output2.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-part1.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-part1.png deleted file mode 100644 index d8fd3003c..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-part1.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-use-publickey.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-use-publickey.png deleted file mode 100644 index 1405d4234..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-use-publickey.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-vmsize-dc4es_v5.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-vmsize-dc4es_v5.png deleted file mode 100644 index a77299e45..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-basics-vmsize-dc4es_v5.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-bastion-session.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-bastion-session.png deleted file mode 100644 index 3ffab52da..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-bastion-session.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-bastion-settings.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-bastion-settings.png deleted file mode 100644 index cab46554a..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-bastion-settings.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-build-attest-client.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-build-attest-client.png deleted file mode 100644 index 5b8d1b0eb..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-build-attest-client.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-connect-via-bastion.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-connect-via-bastion.png deleted file mode 100644 index 921afff84..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-connect-via-bastion.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-daemon-prompts.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-daemon-prompts.png deleted file mode 100644 index 3ffab52da..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-daemon-prompts.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-image-find-ubuntu-cvm.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-image-find-ubuntu-cvm.png deleted file mode 100644 index 57a83b48f..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-image-find-ubuntu-cvm.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-image-select-ubuntu-cvm-gen2.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-image-select-ubuntu-cvm-gen2.png deleted file mode 100644 index f418487f8..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-image-select-ubuntu-cvm-gen2.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-network-interface.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-network-interface.png deleted file mode 100644 index 72d3f3f4b..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-network-interface.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-resource.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-resource.png deleted file mode 100644 index 25695fa92..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-resource.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-run-attest-client.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-run-attest-client.png deleted file mode 100644 index ca0cb6aba..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-run-attest-client.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-system-managed-identity.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-system-managed-identity.png deleted file mode 100644 index 3ae89489a..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vm-system-managed-identity.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-add-subnet.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-add-subnet.png deleted file mode 100644 index 930543d99..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-add-subnet.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-azurebastion-subnet.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-azurebastion-subnet.png deleted file mode 100644 index 8c56c1fa1..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-azurebastion-subnet.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-default-subnet.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-default-subnet.png deleted file mode 100644 index e1eec4add..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-default-subnet.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-instance-details.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-instance-details.png deleted file mode 100644 index 793cfd752..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-instance-details.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-resource.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-resource.png deleted file mode 100644 index 078d07554..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-resource.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-subnet-snapshot.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-subnet-snapshot.png deleted file mode 100644 index bf87c3841..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/images/vnet-subnet-snapshot.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/portal-guide.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/portal-guide.md deleted file mode 100644 index 2f5ab90e4..000000000 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/portal-guide.md +++ /dev/null @@ -1,487 +0,0 @@ -# Walkthrough Challenge 4 - Encryption in use with Azure Confidential Compute – VM -# Azure Portal Guide - -**Estimated Duration:** 90-120 minutes - -> 💡 **Objective:** Learn how to implement and validate guest attestation on Azure Confidential VMs using the Azure Portal to ensure business logic only executes in trusted, hardware-backed confidential computing environments. You will deploy a Confidential VM through the portal, configure secure access through Azure Bastion and Key Vault, build and run attestation client applications, and verify cryptographic proof of VM integrity before processing sensitive workloads. - -## Prerequisites - -Please ensure that you successfully verified the [General prerequisites](../../Readme.md#general-prerequisites) before continuing with this challenge. - -- Azure Portal access (https://portal.azure.com) -- Azure subscription with permissions to create VMs, Key Vault, and Attestation Providers -- Basic understanding of Azure Virtual Machines and networking concepts -- Familiarity with SSH key authentication -- Access to Azure Cloud Shell -- Basic understanding of confidential computing concepts - -## Scenario Context - -You are a security architect at a European financial services organization that processes highly sensitive customer data and must comply with strict data sovereignty and protection requirements. Your organization has determined that traditional encryption at rest and in transit is insufficient for protecting high-value workloads. - -Your mandate includes: - -- **Encryption in Use**: Data must be protected even while being processed, not just when stored or transmitted -- **Hardware-Based Trust**: Security guarantees must be rooted in hardware, not just software configurations -- **Attestation Requirements**: Workloads must verify they are running in a genuine confidential environment before processing sensitive data -- **Zero Trust Architecture**: Never assume trust; always verify the execution environment cryptographically -- **Regulatory Compliance**: Meet stringent requirements for data protection in financial services - -In this challenge, you'll implement Azure Confidential Computing using AMD SEV-SNP technology to create a hardware-based Trusted Execution Environment (TEE). You'll configure guest attestation to provide cryptographic proof that your workload is running in a protected environment before it processes any sensitive operations. - -### Understanding Guest Attestation - -Guest attestation helps you confirm that your confidential VM environment is secured by a genuine hardware-backed Trusted Execution Environment (TEE) with security features enabled for isolation and integrity. - -**You can use guest attestation to:** - -- **Verify hardware platform** - Confirm the confidential VM runs on expected AMD SEV-SNP hardware -- **Validate secure boot** - Verify secure boot is enabled, protecting firmware, bootloader, and kernel from malware -- **Provide cryptographic evidence** - Obtain JWT tokens proving the VM runs on confidential hardware -- **Prevent data exposure** - Ensure workloads refuse to start in untrusted environments - -### Attestation Workflow Pattern - -### Attestation Workflow Pattern - -![Scenario diagram](./images/attestation-workflow.png) - -In this challenge, you will implement a common pattern where **attestation requests are made from inside the workload** at program startup. The workload verifies that it's running on the correct hardware platform before executing any sensitive business logic. - -**How it works:** - -1. Your workload starts on the Confidential VM -2. Before processing any sensitive data, the workload calls the attestation library -3. The attestation library contacts Microsoft Azure Attestation (MAA) to verify the TEE environment -4. The workload parses the attestation response (JWT token) to confirm: - - The VM runs on genuine AMD SEV-SNP confidential hardware - - Secure boot is enabled and validated - - The VM guest state is protected -5. Only after successful verification does the workload proceed with sensitive operations - -**Why this matters:** - -This pattern ensures your application never processes sensitive data in an untrusted environment. If attestation fails (e.g., the code is running on a standard VM or a compromised environment), the workload can refuse to start or handle data differently. - -### Learning Resources - -- [Azure Confidential Computing overview](https://learn.microsoft.com/azure/confidential-computing/overview) -- [Confidential VM concepts](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) -- [Guest attestation for confidential VMs](https://learn.microsoft.com/azure/confidential-computing/guest-attestation-confidential-vms) -- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/overview) - -### Original Source Materials - -This challenge is based on the **Confidential VM Guest Attestation Sample Application** from the Microsoft Azure Confidential Computing repository: - -- **Main Repository**: [Azure Confidential Computing CVM Guest Attestation](https://github.com/Azure/confidential-computing-cvm-guest-attestation) -- **Source Module**: [CVM Attestation Sample App](https://github.com/Azure/confidential-computing-cvm-guest-attestation/tree/main/cvm-attestation-sample-app) - -The sample application and deployment patterns have been adapted for this MicroHack challenge to provide a guided learning experience with Azure Confidential VMs using the Azure Portal. - ---- - -## Task 1: Understand the Deployment Architecture and Naming Conventions - -💡 **Before deploying resources through the portal, it's important to understand the security architecture, components you'll be creating, and the naming conventions you'll use.** - -### Resources Being Deployed - -The following resources will be created in the `North Europe` Azure region: - -- **1 Attestation Provider** - Microsoft Azure Attestation (MAA) service for verifying TEE integrity -- **1 Virtual Network** - Isolated network with VM and Bastion subnets -- **1 Confidential VM** - Ubuntu 22.04 with AMD SEV-SNP hardware encryption -- **1 Azure Bastion** - Secure remote access without public IP exposure -- **1 Azure Key Vault** - Secure storage for SSH keys -- **Associated Resources** - NSGs, NICs, OS Disks (auto-created) - -### Security Architecture - -This setup implements a zero-trust security model: - -- **No Public IPs on VMs** - VMs are not directly accessible from internet -- **Azure Bastion (Basic SKU)** - Provides secure RDP/SSH access through Azure Portal -- **Azure Key Vault** - Stores SSH private keys securely with RBAC controls -- **Fresh SSH Key Pair** - Generated specifically for this deployment, never stored locally -- **Hardware-Based Encryption** - AMD SEV-SNP encrypts VM memory at the hardware level -- **Secure Boot** - Protects boot chain integrity from firmware through kernel - -🔑 **Security Best Practice**: This architecture ensures VM access is authenticated through Azure AD, encrypted with TLS, and never exposes SSH directly to the internet. - -### Naming Conventions - -Throughout this guide, you'll need to use consistent naming. Choose your own values: - -- **ATTENDEE_ID**: `labuser-xx` (Change this for each participant) -- **LOCATION**: `North Europe` -- **RESOURCE_GROUP**: `{ATTENDEE_ID}` (replace with your ATTENDEE_ID) - -For globally unique resources, append a random 6-character suffix: -- **KEYVAULT_NAME**: `kvmicrohacka1b2c3` (use your own random suffix) -- **ATTESTATION_NAME**: `attesta1b2c3` (use your own random suffix, no hyphens) - -> **💡 Tip**: Write down your chosen names before starting! - ---- - -## Task 2: Create Resource Group and Key Vault - -💡 **You'll create the foundational Azure resources to organize and secure your deployment.** - -### Step 1: Create Resource Group - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. Navigate to the [Azure Portal](https://portal.azure.com)
2. In the search bar at the top, type **"Resource groups"** and select it
3. Click **+ Create** | | ![Resource Groups](./images/select-resource-groups.png) | -| 4. Fill in the details:
- **Subscription**: Select your subscription
- **Resource group**: `` (use your ATTENDEE_ID)
- **Region**: `North Europe` | | ![Create Resource Group](./images/create-resource-group.png) | -| 5. Click **Review + create**
6. Click **Create** | | | - - ---- - -### Step 2: Create Key Vault - -💡 **Create a Key Vault with RBAC permissions to securely store SSH keys.** - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. In the search bar, type **"Key vaults"** and select it
2. Click **+ Create** | | | -| 3. Fill in the **Basics** tab:
- **Subscription**: Select your subscription
- **Resource group**: `rg-cc-attendee01`
- **Key vault name**: `kv-cc-a1b2c3` (use your random suffix)
- **Region**: `North Europe`
- **Pricing tier**: `Standard` | | ![Create Key vault](./images/kv-create.png) | -| 4. Click on the **Access configuration** tab:
- **Permission model**: Select **Azure role-based access control**
- Check ✅ **Azure Virtual Machines for deployment**
- Check ✅ **Azure Resource Manager for template deployment** | | ![Key Vault Access configuration](./images/kv-access-config.png) | -| 5. Click **Review + create**
6. Click **Create**
7. Wait for deployment to complete, then click **Go to resource** | | | - - -### Assign Key Vault Permissions - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. In your Key Vault, click on **Access control (IAM)** in the left menu
2. Click **+ Add** > **Add role assignment** | | ![Configure Key Vault RBAC](./images/kv-add-role-assignment.png) | -| 3. In the **Role** tab, search for and select **Key Vault Secrets Officer**
4. Click **Next** | | ![Assign role](./images/kv-rbac-choose-secrets-officer.png) | -| 5. In the **Members** tab:
- Select **User, group, or service principal**
- Click **+ Select members**
- Search for and select your user account
- Click **Select** | | ![Select role members](./images/kv-rbac-select-members.png) | -| 6. Click **Review + assign**
7. Click **Review + assign** again | | | - -> **⏱️ Wait 2-3 minutes for permissions to propagate before proceeding** - ---- - -## Task 3: Generate and Store SSH Keys Securely - -💡 **Generate SSH key pairs using Azure Cloud Shell and store them in Key Vault for secure authentication.** - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. Click the **Cloud Shell** icon (>_) in the top-right corner of the Azure Portal
2. Select **Bash** when prompted | | ![Activate Cloud shell](./images/kv-cloud-shell-activate.png)
![Active Cloud shell](./images/kv-cloud-shell-activated.png) | -| 3. Run the following commands in Cloud Shell:
# Set your attendee ID and Key Vault name
ATTENDEE_ID="attendee01" # Change this

KEYVAULT_NAME="kv-cc-a1b2c3" # Change this to your Key Vault name

# Generate SSH key pair
SSH_TEMP_FILE="/tmp/cc_ssh_key_\${ATTENDEE_ID}_\${RANDOM}"
ssh-keygen -t rsa -b 4096 -f "\$SSH_TEMP_FILE" -N "" -C "microhack-cc"

# Store private key in Key Vault
az keyvault secret set --vault-name \$KEYVAULT_NAME --name "ssh-private-key" --file "\$SSH_TEMP_FILE"

# Store public key in Key Vault
az keyvault secret set --vault-name \$KEYVAULT_NAME --name "ssh-public-key" --file "\${SSH_TEMP_FILE}.pub"

# Display public key (save this for later)
echo "=== Public Key ==="
cat "\${SSH_TEMP_FILE}.pub"
echo "=================="

# Clean up
rm -f "\${SSH_TEMP_FILE}" "\${SSH_TEMP_FILE}.pub"
| | ![Generate SSH Key pair](./images/kv-gen-ssh-key-pair.png)
![store private key in Key Vault](./images/kv-secret-set-privkey.png)
![store public key in Key Vault](./images/kv-secret-set-pubkey.png) | -| 4. **Important**: Copy and save the public key output - you'll need it when creating the VM. | | ![Copy public key output](./images/kv-secret-copy-public-key.png) | - ---- - -## Task 4: Create Attestation Provider - -💡 **Deploy Microsoft Azure Attestation service for verifying confidential VM integrity.** - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. In the search bar, type **"Attestation providers"** and select it
2. Click **+ Create** | | ![Attestation service](./images/create-attest-service.png) | -| 3. Fill in the details:
- **Subscription**: Select your subscription
- **Resource group**: `rg-cc-attendee01`
- **Name**: `attesta1b2c3` (use your random suffix, **only alphanumeric characters, no hyphens**)
- **Region**: `North Europe` | | ![Create Attestation service](./images/review-create-attest-service.png) | -| 4. Click **Review + create**
5. Click **Create** | | | - ---- - -## Task 5: Create Virtual Network with Isolated Subnets - -💡 **Create a virtual network with separate subnets for VMs and Azure Bastion to ensure network segmentation.** - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. In the search bar, type **"Virtual networks"** and select it
2. Click **+ Create** | | ![Virtual networks](./images/vnet-resource.png) | -| 3. Fill in the **Basics** tab:
- **Subscription**: Select your subscription
- **Resource group**: `rg-cc-attendee01`
- **Virtual network name**: `vm-ubuntu-cvm-vnet`
- **Region**: `North Europe` | | ![Virtual network basics](./images/vnet-instance-details.png) | -| 4. Click on the **IP addresses** tab:
- Keep the default address space or set to `10.10.0.0/24`
- Delete the "default" subnet
- Click **+ Add a subnet**
- **Subnet name**: `vm-subnet`
- **Subnet address range**: `10.10.0.0/26`
- Click **Add**
- Click **+ Add a subnet** again
- **Subnet name**: `AzureBastionSubnet` (must be exactly this name)
- **Subnet address range**: `10.10.0.64/26`
- Click **Add** | | ![Add VM subnet](./images/vnet-add-subnet.png)
![Default subnet](./images/vnet-default-subnet.png)
![Azure Bastion subnet](./images/vnet-azurebastion-subnet.png)
![Virtual network summary](./images/vnet-subnet-snapshot.png) | -| 5. Click **Review + create**
6. Click **Create** | | | - ---- - -## Task 6: Deploy Confidential VM with Hardware Encryption - -💡 **Create an Azure Confidential VM with AMD SEV-SNP hardware-based encryption and secure boot.** - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. In the search bar, type **"Virtual machines"** and select it
2. Click **+ Create** > **Azure virtual machine** | | ![Virtual machine resources](./images/vm-resource.png) | - -### Basics Tab - -| Step | Action | Screenshot | -|------|--------|------------| -| 3. Fill in the **Basics** tab:
- **Subscription**: Select your subscription
- **Resource group**: `rg-cc-attendee01`
- **Virtual machine name**: `vm-ubuntu-cvm`
- **Region**: `North Europe`
- **Availability options**: No infrastructure redundancy required
- **Security type**: **Confidential virtual machines**
- **Image**: Click **See all images**
- Search for **Ubuntu 22.04 Confidential VM**
- Select **Ubuntu Server 22.04 LTS - Confidential VM x64 Gen2**
- **VM architecture**: x64
- **Size**: Click **See all sizes**
- Search for **DC2as_v5**
- Select **Standard_DC2as_v5** (4 vCPUs, 32 GB memory)
- Click **Select** | | ![Virtual machine basics](./images/vm-basics-part1.png)
![Find Ubuntu CVM](./images/vm-image-find-ubuntu-cvm.png)
![Select Ubuntu CVM](./images/vm-image-select-ubuntu-cvm-gen2.png) | -| 4. Configure **Administrator account**:
- **Authentication type**: SSH public key
- **Username**: `azureuser`
- **SSH public key source**: **Use existing key**
- Open Cloud Shell
- Run this command to get SSH public key:
`az keyvault secret show --name ssh-public-key --vault-name --query value -o tsv`
- **SSH Public Key**: Copy the output into the `SSH Public Key` field | | ![Use Public key](./images/vm-basics-use-publickey.png) | -| 5. Configure **Inbound port rules**:
- **Public inbound ports**: None | | | - -### Disks Tab - -| Step | Action | Screenshot | -|------|--------|------------| -| 6. Click **Next: Disks >**
- **OS disk type**: Keep default (Premium SSD) | | | - -### Networking Tab - -| Step | Action | Screenshot | -|------|--------|------------| -| 7. Click **Next: Networking >**
- **Virtual network**: `vm-ubuntu-cvm-vnet`
- **Subnet**: `vm-subnet (10.10.0.0/26)`
- **Public IP**: **None**
- **NIC network security group**: Basic
- **Public inbound ports**: None | | ![Virtual machine network interface](./images/vm-network-interface.png) | - -### Management Tab - -| Step | Action | Screenshot | -|------|--------|------------| -| 8. Click **Next: Management >**
- Scroll to **Identity**
- **System assigned managed identity**: ✅ Enable | | ![Assign System MI](./images/vm-system-managed-identity.png) | - -### Advanced Tab - -| Step | Action | Screenshot | -|------|--------|------------| -| 9. Click **Next: Advanced >** | | | - -### Review and Create - -| Step | Action | Screenshot | -|------|--------|------------| -| 10. Click **Review + create**
11. Review all settings
12. Click **Create**
13. Wait for deployment (5-10 minutes) | | | - ---- - -## Task 7: Deploy Azure Bastion for Secure Access - -💡 **Create Azure Bastion to enable secure RDP/SSH connectivity without exposing VMs to the public internet.** - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. In the search bar, type **"Bastions"** and select it
2. Click **+ Create** | | ![Azure Bastion](./images/azbastion-resource.png) | -| 3. Fill in the **Basics** tab:
- **Subscription**: Select your subscription
- **Resource group**: `rg-cc-attendee01`
- **Name**: `bastion-northeurope`
- **Region**: `North Europe`
- **Tier**: Basic
- **Virtual network**: `vm-ubuntu-cvm-vnet`
- **Subnet**: `AzureBastionSubnet` (should be auto-selected)
- **Public IP address**: Create new
- **Public IP address name**: `bastion-northeurope-ip`
- Click **OK** | | ![Create Azure Bastion](./images/azbastion-create.png) | -| 4. Click **Review + create**
5. Click **Create**
6. **Wait 5-10 minutes** for Bastion deployment (this takes time) | | | - ---- - -## Task 8: Connect to VM, Build, and Run Attestation Client - -💡 **Connect to the Confidential VM through Bastion, install dependencies, build the attestation client, and verify cryptographic proof of the VM's trusted state.** - -| Step | Action | Screenshot | -|------|--------|------------| -| 1. Navigate to **Virtual machines**
2. Click on **vm-ubuntu-cvm**
3. Click **Connect** > **Connect via Bastion** | | ![Connect to VM via Bastion](./images/vm-connect-via-bastion.png) | -| 4. Fill in the Bastion connection form:
- **Authentication Type**: **SSH Private Key from Azure Key Vault**
- **Username**: `azureuser`
- **Azure Key Vault**: Select your Key Vault `kv-cc-a1b2c3`
- **Azure Key Vault Secret**: `ssh-private-key` | | ![Fill Bastion settings](./images/vm-bastion-settings.png) | -| 5. Click **Connect**
6. A new browser tab will open with a terminal connection | | | - -### Install Dependencies and Run Attestation - -| Step | Action | Screenshot | -|------|--------|------------| -| 7. In the terminal, run the following commands:

**NOTE**: if you encounter any prompts to restart services, just hit \ to confirm and continue to the next command.


# Install system dependencies
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a
export APT_LISTCHANGES_FRONTEND=none

sudo -E apt-get update -y
sudo -E apt-get upgrade -y

sudo -E apt-get install -y build-essential libcurl4-openssl-dev libjsoncpp-dev libboost-all-dev nlohmann-json3-dev cmake wget git jq

# Clone the attestation repository
git clone https://github.com/Azure/confidential-computing-cvm-guest-attestation.git

# Download the attestation package
wget https://packages.microsoft.com/repos/azurecore/pool/main/a/azguestattestation1/azguestattestation1_1.1.2_amd64.deb

# Install the attestation package
sudo dpkg -i azguestattestation1_1.1.2_amd64.deb

# Build the attestation client
cd confidential-computing-cvm-guest-attestation/cvm-attestation-sample-app/
cmake .
make
| | ![Bastion ssh session](./images/vm-bastion-session.png)
![Build Attestation Client](./images/vm-build-attest-client.png) | - -| Step | Action | Screenshot | -|------|--------|------------| -| 8. Get your custom attestation provider URI from the Azure Portal:
- Navigate to **All Resources**
- Search for your attestation provider (e.g., `attesta1b2c3`)
- Click on the attestation provider
- Copy the **Attest URI** from the Overview page | | ![Portal - Attestation Service](./images/portal-attestation-service.png)
![Portal - Copy Attestation URI](./images/portal-attestation-provider-copy-uri.png) | -| 9. Back in the Bastion terminal, run the attestation client with your custom provider URI:

# Replace with your actual Attestation URI
sudo ./AttestationClient -a https://attesta1b2c3.neu.attest.azure.net -o token \| jq -R 'split(".") \| .[0],.[1] \| @base64d \| fromjson'


🔑 **Why Use a Custom Attestation Provider?**:
- **Custom Policies**: Define organization-specific attestation policies
- **Audit Control**: Maintain your own attestation logs and policies
- **Compliance**: Meet regulatory requirements for attestation service ownership
- **Isolation**: Separate attestation infrastructure from shared services | | ![Attestation Client - Specified Provider](./images/attestation-specified-provider.png) | - ---- - -## Task 9: Understanding Attestation Results and Production Use - -💡 **Understanding what the attestation token proves and how to use it in production workloads.** - -### What the Attestation Token Proves - -The JWT token you generated contains multiple claims that cryptographically prove the VM's security posture: - -**Key Claims to Review**: - -- `x-ms-isolation-tee.x-ms-attestation-type` - Should show `sevsnpvm` (AMD SEV-SNP) -- `x-ms-isolation-tee.x-ms-compliance-status` - Should show `azure-compliant-cvm` -- `x-ms-isolation-tee.x-ms-sevsnpvm-is-debuggable` - Should be `false` (debugging disabled for security) -- `x-ms-policy-hash` - Hash of the attestation policy used -- `secureboot` - Should be `true` -- `x-ms-ver` - Attestation service version - -🔑 **Security Insight**: This JWT token is signed by Microsoft Azure Attestation. Any relying party can verify the signature to confirm the claims are authentic and the VM is running in a genuine confidential environment. - -### Production Implementation Pattern - -In a production scenario, your application would implement attestation checks before processing sensitive data: - -```python -# Pseudocode - Production attestation pattern -def process_sensitive_data(customer_data): - # Step 1: Perform attestation - attestation_token = get_attestation_token() - - # Step 2: Validate token with relying party - if not validate_attestation(attestation_token): - log_error("Attestation failed - not running in confidential environment") - raise SecurityException("Execution environment not trusted") - - # Step 3: Verify specific claims - claims = parse_jwt_claims(attestation_token) - if claims['x-ms-isolation-tee.x-ms-attestation-type'] != 'sevsnpvm': - raise SecurityException("Not running on AMD SEV-SNP hardware") - - if claims['x-ms-isolation-tee.x-ms-sevsnpvm-is-debuggable'] == 'true': - raise SecurityException("VM debugging is enabled - security risk") - - # Step 4: Only now proceed with sensitive operations - encrypted_results = process_pii_data(customer_data) - return encrypted_results -``` - -🔑 **Best Practice**: Attestation should be performed at application startup and periodically during long-running workloads to detect runtime tampering. - ---- - -## Task 10: Clean Up Resources - -💡 **Delete all resources to avoid ongoing charges.** - -### Delete Resource Group - -1. Navigate to **Resource groups** -2. Click on `rg-cc-attendee01` -3. Click **Delete resource group** -4. Type the resource group name to confirm: `rg-cc-attendee01` -5. Click **Delete** -6. Wait for deletion to complete (5-10 minutes) - -> **💡 Tip**: Deleting the resource group will remove all resources created in this workshop - -⚠️ **Warning**: This command will permanently delete all resources in the resource group including the VM, Key Vault, Bastion, and all associated resources. - ---- - -## Troubleshooting - -### Cannot Connect via Bastion - -- **Check**: Bastion deployment is complete (Status: Succeeded) -- **Check**: VM is running (Status: Running) -- **Check**: You have Key Vault Secrets Officer role on the Key Vault -- **Wait**: 2-3 minutes after Bastion deployment before connecting - -### VM Creation Failed - -- **Check**: Quota availability for DC-series VMs in North Europe -- **Try**: Different region (e.g., West Europe, UK South) -- **Try**: Smaller VM size (Standard_DC2es_v5) - -### Attestation Client Build Fails - -- **Check**: All dependencies were installed successfully -- **Try**: Re-run the apt-get commands -- **Check**: VM has internet connectivity - -### Key Vault Access Denied - -- **Check**: RBAC role assignment completed successfully -- **Wait**: 2-3 minutes for permissions to propagate -- **Verify**: You're logged in with the correct account - ---- - -## Key Takeaways - -In this challenge, you successfully implemented and validated Azure Confidential Computing with guest attestation using the Azure Portal. Here are the key concepts and best practices: - -### Confidential Computing Fundamentals - -✅ **Hardware-Based Trust** - AMD SEV-SNP provides hardware-level memory encryption that protects data in use, not just at rest or in transit - -✅ **Trusted Execution Environments (TEEs)** - Confidential VMs create isolated environments where even the cloud operator cannot access your data - -✅ **Virtual TPM (vTPM)** - Enables cryptographic attestation and secure boot validation - -### Attestation and Verification - -✅ **Guest Attestation** - Cryptographically proves that workloads run in genuine confidential computing environments before processing sensitive data - -✅ **Microsoft Azure Attestation (MAA)** - Provides centralized attestation services that validate TEE integrity and issue signed JWT tokens - -✅ **Zero Trust Validation** - Applications should verify execution environment before processing sensitive operations - -### Security Architecture - -✅ **Defense in Depth** - Combining multiple security layers (no public IPs, Azure Bastion, Key Vault, hardware encryption, attestation) - -✅ **Secure Key Management** - SSH keys stored exclusively in Azure Key Vault with RBAC controls, never persisted locally - -✅ **Network Isolation** - VMs without public IPs, accessed only through Azure Bastion with Azure AD authentication - -### Production Best Practices - -✅ **Attestation at Startup** - Perform attestation checks before processing any sensitive data - -✅ **Periodic Re-attestation** - Long-running workloads should re-attest periodically to detect runtime tampering - -✅ **JWT Token Validation** - Verify attestation tokens are signed by trusted authorities and contain expected claims - -✅ **Fail Securely** - Applications should refuse to start or handle data differently if attestation fails - -### Compliance and Governance - -✅ **Data Sovereignty** - Confidential computing ensures data remains encrypted even from cloud operators - -✅ **Regulatory Requirements** - Meets stringent requirements for financial services, healthcare, and government workloads - -✅ **Audit Trail** - Attestation tokens provide cryptographic proof for compliance auditing - ---- - -## Next Steps - -### Explore Advanced Confidential Computing Scenarios - -- **[Azure Confidential Containers](https://learn.microsoft.com/azure/confidential-computing/confidential-containers)** - Deploy containerized workloads with hardware-based confidential computing -- **[Confidential VMs with Customer-Managed Keys](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview#encryption-at-host-with-customer-managed-keys)** - Use your own encryption keys for additional control -- **[Confidential Computing on Azure Kubernetes Service](https://learn.microsoft.com/azure/aks/confidential-computing-azure)** - Orchestrate confidential containers at scale - -### Implement Application-Level Confidential Computing - -- **[Enclave Applications](https://learn.microsoft.com/azure/confidential-computing/application-development)** - Build applications that use Intel SGX or AMD SEV-SNP enclaves -- **[Confidential Inferencing](https://learn.microsoft.com/azure/machine-learning/how-to-machine-learning-confidential-containers)** - Protect ML models and data during inference -- **[Always Encrypted](https://learn.microsoft.com/sql/relational-databases/security/encryption/always-encrypted-database-engine)** - Combine confidential computing with SQL Server encryption - -### Learn More About Azure Security - -- **[Azure Security Benchmark for Confidential Computing](https://learn.microsoft.com/security/benchmark/azure/baselines/confidential-computing-security-baseline)** - Follow Microsoft's security recommendations -- **[Azure Confidential Ledger](https://learn.microsoft.com/azure/confidential-ledger/overview)** - Tamper-proof, immutable ledger for audit logs -- **[Microsoft Azure Attestation Documentation](https://learn.microsoft.com/azure/attestation/overview)** - Deep dive into attestation concepts and policies - -### Practice Confidential Computing Patterns - -- **Multi-Party Computation** - Build scenarios where multiple organizations jointly process data without exposing it to each other -- **Confidential AI** - Implement ML training and inference in confidential environments -- **Secure Enclaves** - Explore Intel SGX for process-level isolation within VMs - ---- - -## Additional Resources - -- [Azure Confidential Computing overview](https://learn.microsoft.com/azure/confidential-computing/overview) -- [Confidential VM concepts](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) -- [Guest attestation for confidential VMs](https://learn.microsoft.com/azure/confidential-computing/guest-attestation-confidential-vms) -- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/overview) -- [AMD SEV-SNP Technology](https://www.amd.com/en/developer/sev.html) -- [Azure Bastion Documentation](https://learn.microsoft.com/azure/bastion/bastion-overview) -- [Azure Key Vault Best Practices](https://learn.microsoft.com/azure/key-vault/general/best-practices) - ---- - -## Technical Notes - -1. **Resource Naming**: Ensure globally unique names for Key Vault and Attestation Provider -2. **SSH Keys**: Generated keys are securely stored in Key Vault and never exposed locally -3. **Security**: No public IPs are used; all access is through Azure Bastion -4. **Pricing**: VMs use standard pricing; remember to delete resources after the workshop -5. **Bastion**: Basic SKU is sufficient for this workshop -6. **Regions**: North Europe has good availability for DC-series VMs -7. **Attestation**: The attestation token provides cryptographic proof of VM integrity -8. **Deployment Time**: Azure Bastion typically takes 5-10 minutes to deploy -9. **Auto-Created Resources**: NSGs, NICs, and OS Disks are created automatically by the portal -10. **RBAC Propagation**: Key Vault RBAC permissions may take 2-3 minutes to propagate diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/Deploy-VisualAttestationV2.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/Deploy-VisualAttestationV2.ps1 new file mode 100644 index 000000000..e2d2252bf --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/Deploy-VisualAttestationV2.ps1 @@ -0,0 +1,586 @@ +<# +.SYNOPSIS + Build and deploy the Visual Attestation Demo v2 web UI to Azure Container Instances. + +.DESCRIPTION + Builds the container image SERVER-SIDE in Azure Container Registry via + `az acr build` (no local Docker required), then deploys the resulting image + to Azure Container Instances on either the Confidential SKU (AMD SEV-SNP, + attestation succeeds) or Standard SKU (no TEE, attestation fails - shown + for educational comparison). + +.PARAMETER Build + Create resource group, ACR, and run `az acr build` to produce the image. + +.PARAMETER Deploy + Deploy a single container group. Confidential by default. Pass -NoAcc to + deploy on Standard SKU instead (so you can see attestation fail). + +.PARAMETER Compare + Deploy BOTH a confidential and a standard container side-by-side using + the already-built image. Confidential requires Docker + the confcom CLI + extension on the local machine for CCE policy generation. + +.PARAMETER Cleanup + Delete the entire resource group created by -Build. + +.PARAMETER NoAcc + With -Deploy: use the Standard SKU template (attestation will fail). + +.PARAMETER SkipBrowser + Don't auto-open Edge after a successful deploy. + +.PARAMETER RegistryName + Optional ACR name to reuse. If omitted, a random name is generated and + persisted to acr-config.json. + +.PARAMETER Location + Azure region. Defaults to eastus (matches confidential ACI availability). + +.EXAMPLE + # Build the image once, deploy to Confidential ACI (attestation succeeds) + ./Deploy-VisualAttestationV2.ps1 -Build + ./Deploy-VisualAttestationV2.ps1 -Deploy + +.EXAMPLE + # Same image, deploy to Standard ACI (attestation FAILS - educational) + ./Deploy-VisualAttestationV2.ps1 -Deploy -NoAcc + +.EXAMPLE + # Side-by-side comparison + ./Deploy-VisualAttestationV2.ps1 -Compare + +.EXAMPLE + ./Deploy-VisualAttestationV2.ps1 -Cleanup +#> +[CmdletBinding(DefaultParameterSetName='Help')] +param( + [Parameter(ParameterSetName='Build')] [switch]$Build, + [Parameter(ParameterSetName='Deploy')] [switch]$Deploy, + [Parameter(ParameterSetName='Compare')] [switch]$Compare, + [Parameter(ParameterSetName='Cleanup')] [switch]$Cleanup, + + [Parameter(ParameterSetName='Deploy')] [switch]$NoAcc, + [Parameter(ParameterSetName='Deploy')] + [Parameter(ParameterSetName='Compare')] [switch]$SkipBrowser, + + [Parameter(ParameterSetName='Build')] [string]$RegistryName, + [Parameter(ParameterSetName='Build')] [string]$Prefix = 'sgall', + [string]$Location = 'eastus' +) + +# UTF-8 console for nicer output on Windows +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +$ErrorActionPreference = 'Stop' + +$ImageName = 'cc-attest' +$ImageTag = '1.0' +$ConfigPath = Join-Path $PSScriptRoot 'acr-config.json' + +# ============================================================================ +# Helpers +# ============================================================================ +function Write-Header { param($m) Write-Host ""; Write-Host ("=" * 72) -ForegroundColor Cyan; Write-Host $m -ForegroundColor Cyan; Write-Host ("=" * 72) -ForegroundColor Cyan } +function Write-Success { param($m) Write-Host "[OK] $m" -ForegroundColor Green } +function Write-Warn2 { param($m) Write-Host "[WARN] $m" -ForegroundColor Yellow } +function Write-Err2 { param($m) Write-Host "[ERROR] $m" -ForegroundColor Red } + +function Get-Config { + if (-not (Test-Path $ConfigPath)) { return $null } + return Get-Content $ConfigPath -Raw | ConvertFrom-Json +} +function Save-Config { param($cfg) $cfg | ConvertTo-Json -Depth 10 | Set-Content $ConfigPath -Encoding UTF8 } + +function Test-AzCli { + $v = az version 2>$null + if ($LASTEXITCODE -ne 0) { throw "Azure CLI not found. Install from https://aka.ms/azcli" } + $acct = az account show 2>$null | ConvertFrom-Json + if (-not $acct) { throw "Not logged in. Run: az login" } + Write-Host "Subscription: $($acct.name) ($($acct.id))" +} + +# ============================================================================ +# Build phase +# ============================================================================ +function Invoke-Build { + Write-Header "Build phase - creating ACR and building image server-side" + Test-AzCli + + if (-not $RegistryName) { + $rand = -join ((97..122) | Get-Random -Count 8 | ForEach-Object { [char]$_ }) + $RegistryName = "acr$rand" + Write-Warn2 "No -RegistryName given. Generated: $RegistryName" + } + $ResourceGroup = "$Prefix-$RegistryName-rg" + + Write-Host "Resource Group: $ResourceGroup" + Write-Host "Registry : $RegistryName" + Write-Host "Location : $Location" + Write-Host "Image : ${ImageName}:${ImageTag}" + Write-Host "" + + Write-Host "Creating resource group..." + az group create --name $ResourceGroup --location $Location | Out-Null + if ($LASTEXITCODE -ne 0) { throw "az group create failed" } + + Write-Host "Creating ACR (Basic, admin user enabled)..." + az acr create --resource-group $ResourceGroup --name $RegistryName --sku Basic --admin-enabled true | Out-Null + if ($LASTEXITCODE -ne 0) { throw "az acr create failed" } + + Write-Host "Running az acr build (server-side Docker build, no local daemon needed)..." + Write-Host "This typically takes 4-6 minutes (apt-get + cvm-attestation-tools clone + pip install)..." + az acr build --registry $RegistryName --image "${ImageName}:${ImageTag}" --file Dockerfile --no-logs $PSScriptRoot + if ($LASTEXITCODE -ne 0) { + # Verify the image actually exists - some az acr build calls return non-zero on warnings + az acr repository show --name $RegistryName --image "${ImageName}:${ImageTag}" 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { throw "az acr build failed" } + } + Write-Success "Image built and pushed to ACR" + + $loginServer = az acr show --name $RegistryName --query loginServer -o tsv + $cfg = [pscustomobject]@{ + registryName = $RegistryName + resourceGroup = $ResourceGroup + location = $Location + loginServer = $loginServer + imageName = $ImageName + imageTag = $ImageTag + fullImage = "$loginServer/${ImageName}:${ImageTag}" + } + Save-Config $cfg + + Write-Header "Build complete" + Write-Host "Image: $($cfg.fullImage)" + Write-Host "Config saved to: $ConfigPath" + Write-Host "" + Write-Host "Next step:" + Write-Host " ./Deploy-VisualAttestationV2.ps1 -Deploy # Confidential SKU" + Write-Host " ./Deploy-VisualAttestationV2.ps1 -Deploy -NoAcc # Standard SKU" + Write-Host " ./Deploy-VisualAttestationV2.ps1 -Compare # both side-by-side" +} + +# ============================================================================ +# Deploy helpers +# ============================================================================ +function Get-AcrCreds { + param($cfg) + $u = az acr credential show --name $cfg.registryName --query username -o tsv + $p = az acr credential show --name $cfg.registryName --query "passwords[0].value" -o tsv + if (-not $u -or -not $p) { throw "Failed to read ACR credentials" } + return @{ Username = $u; Password = $p } +} + +function New-ParamsFile { + param($Path, $Name, $Image, $Server, $User, $Pass, $Dns) + $obj = @{ + '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' + 'contentVersion' = '1.0.0.0' + 'parameters' = @{ + containerGroupName = @{ value = $Name } + location = @{ value = $Location } + appImage = @{ value = $Image } + registryServer = @{ value = $Server } + registryUsername = @{ value = $User } + registryPassword = @{ value = $Pass } + dnsNameLabel = @{ value = $Dns } + } + } + $obj | ConvertTo-Json -Depth 10 | Set-Content $Path -Encoding UTF8 +} + +function Invoke-AcrLoginForConfcom { + param($cfg, $creds) + Write-Host "Logging into ACR (required for confcom CCE policy generation)..." + az acr login --name $cfg.loginServer --username $creds.Username --password $creds.Password 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + docker login $cfg.loginServer -u $creds.Username -p $creds.Password 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { throw "ACR login failed" } + } +} + +function Test-ConfidentialPrereqs { + Write-Host "Checking prerequisites for Confidential ACI deploy..." + docker info 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Docker is not running. Required by 'az confcom acipolicygen' for CCE policy generation. Start Docker Desktop, or pass -NoAcc to deploy on Standard SKU." + } + az extension show -n confcom 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "Installing 'confcom' Azure CLI extension..." + az extension add -n confcom | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Failed to install confcom extension" } + } + Write-Success "Docker running, confcom extension installed" +} + +function Wait-ForContainer { + param($Fqdn, $TimeoutSec = 240) + Write-Host "Waiting for http://$Fqdn (timeout ${TimeoutSec}s)..." + $sw = [System.Diagnostics.Stopwatch]::StartNew() + while ($sw.Elapsed.TotalSeconds -lt $TimeoutSec) { + try { + $r = Invoke-WebRequest -Uri "http://$Fqdn" -Method Head -TimeoutSec 5 -ErrorAction SilentlyContinue + if ($r.StatusCode -eq 200) { Write-Success "Container responding"; return $true } + } catch { } + Start-Sleep -Seconds 5 + Write-Host (" ... {0:n0}s elapsed" -f $sw.Elapsed.TotalSeconds) + } + Write-Warn2 "Container did not respond within ${TimeoutSec}s. It may still be pulling the image - try the URL in a browser." + return $false +} + +function New-ComparePage { + param( + [string]$ConfidentialUrl, + [string]$StandardUrl, + [string]$OutputPath + ) + + $html = @" + + + + + + ACI Attestation Compare + + + +
+

ACI Runtime Attestation Compare

+
+ Left: Confidential SKU (expected attestation success) + Right: Standard SKU (expected attestation failure) +
+
+ +
+
+
+
Confidential
+
+ SEV-SNP expected + $ConfidentialUrl +
+
+ +
+ +
+
+
Standard
+
+ Expected /dev/sev-guest missing + $StandardUrl +
+
+ +
+
+ + +"@ + + Set-Content -Path $OutputPath -Value $html -Encoding UTF8 +} + +# ============================================================================ +# Deploy phase (single container group) +# ============================================================================ +function Invoke-Deploy { + if ($NoAcc) { Write-Header "Deploy phase - STANDARD SKU (attestation will FAIL)" } + else { Write-Header "Deploy phase - CONFIDENTIAL SKU (AMD SEV-SNP)" } + Test-AzCli + + $cfg = Get-Config + if (-not $cfg) { throw "acr-config.json not found. Run with -Build first." } + + $creds = Get-AcrCreds $cfg + $stamp = Get-Date -Format 'MMddHHmm' + $rand = -join ((97..122) | Get-Random -Count 4 | ForEach-Object { [char]$_ }) + + if ($NoAcc) { + Write-Warn2 "*** Standard SKU mode - no TEE, no vTPM, attestation WILL fail ***" + $name = "cc-attest-std-$stamp$rand" + $dns = "cc-attest-std-$stamp$rand" + $template = Join-Path $PSScriptRoot 'deployment-template-standard.json' + $params = Join-Path $PSScriptRoot 'deployment-params-standard.json' + New-ParamsFile -Path $params -Name $name -Image $cfg.fullImage ` + -Server $cfg.loginServer -User $creds.Username -Pass $creds.Password -Dns $dns + } + else { + Test-ConfidentialPrereqs + Invoke-AcrLoginForConfcom $cfg $creds + + $name = "cc-attest-conf-$stamp$rand" + $dns = "cc-attest-conf-$stamp$rand" + $template = Join-Path $PSScriptRoot 'deployment-template-confidential.json' + $params = Join-Path $PSScriptRoot 'deployment-params-confidential.json' + New-ParamsFile -Path $params -Name $name -Image $cfg.fullImage ` + -Server $cfg.loginServer -User $creds.Username -Pass $creds.Password -Dns $dns + + Write-Host "Generating CCE policy via 'az confcom acipolicygen' (this can take a few minutes)..." + az confcom acipolicygen -a $template --parameters $params --disable-stdio + if ($LASTEXITCODE -ne 0) { throw "az confcom acipolicygen failed" } + Write-Success "CCE policy injected into $template" + } + + Write-Host "Container group: $name" + Write-Host "DNS label : $dns" + Write-Host "Image : $($cfg.fullImage)" + Write-Host "" + + Write-Host "Submitting ARM deployment..." + az deployment group create ` + --resource-group $cfg.resourceGroup ` + --template-file $template ` + --parameters "@$params" ` + --query "properties.outputs.fqdn.value" -o tsv | Tee-Object -Variable fqdn | Out-Null + if ($LASTEXITCODE -ne 0) { throw "az deployment group create failed" } + Write-Success "Deployed: http://$fqdn" + + Wait-ForContainer -Fqdn $fqdn | Out-Null + + if ($NoAcc) { + Write-Header "Diagnostics (Standard SKU - attestation expected to fail)" + Write-Host "Last 25 lines of container logs:" + az container logs --resource-group $cfg.resourceGroup --name $name --container-name cc-attest 2>&1 | + Select-Object -Last 25 | ForEach-Object { Write-Host " $_" } + Write-Host "" + Write-Host "Click the 'Run Attestation' button in the UI - it will return an error explaining" + Write-Host "that no vTPM / no SEV-SNP hardware report is available on Standard SKU." + } + + if (-not $SkipBrowser) { + Start-Process "http://$fqdn" + } + + Write-Host "" + Write-Host "To view logs: az container logs -g $($cfg.resourceGroup) -n $name --container-name cc-attest" + Write-Host "To delete: az container delete -g $($cfg.resourceGroup) -n $name --yes" +} + +# ============================================================================ +# Compare phase (both side-by-side) +# ============================================================================ +function Invoke-Compare { + Write-Header "Compare phase - Confidential AND Standard side-by-side" + Test-AzCli + + $cfg = Get-Config + if (-not $cfg) { throw "acr-config.json not found. Run with -Build first." } + + Test-ConfidentialPrereqs + $creds = Get-AcrCreds $cfg + Invoke-AcrLoginForConfcom $cfg $creds + + $stamp = Get-Date -Format 'MMddHHmm' + $name_conf = "cc-attest-conf-$stamp" + $name_std = "cc-attest-std-$stamp" + $dns_conf = "cc-attest-conf-$stamp" + $dns_std = "cc-attest-std-$stamp" + + $tpl_conf = Join-Path $PSScriptRoot 'deployment-template-confidential.json' + $tpl_std = Join-Path $PSScriptRoot 'deployment-template-standard.json' + $par_conf = Join-Path $PSScriptRoot 'deployment-params-confidential.json' + $par_std = Join-Path $PSScriptRoot 'deployment-params-standard.json' + + New-ParamsFile -Path $par_conf -Name $name_conf -Image $cfg.fullImage ` + -Server $cfg.loginServer -User $creds.Username -Pass $creds.Password -Dns $dns_conf + New-ParamsFile -Path $par_std -Name $name_std -Image $cfg.fullImage ` + -Server $cfg.loginServer -User $creds.Username -Pass $creds.Password -Dns $dns_std + + Write-Host "Generating CCE policy for confidential container..." + az confcom acipolicygen -a $tpl_conf --parameters $par_conf --disable-stdio + if ($LASTEXITCODE -ne 0) { throw "az confcom acipolicygen failed" } + Write-Success "CCE policy generated" + + Write-Header "Deploying CONFIDENTIAL container ($name_conf)" + az deployment group create --resource-group $cfg.resourceGroup ` + --template-file $tpl_conf --parameters "@$par_conf" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Confidential deploy failed" } + $fqdn_conf = az container show -g $cfg.resourceGroup -n $name_conf --query "ipAddress.fqdn" -o tsv + Write-Success "Confidential: http://$fqdn_conf" + + Write-Header "Deploying STANDARD container ($name_std)" + az deployment group create --resource-group $cfg.resourceGroup ` + --template-file $tpl_std --parameters "@$par_std" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Standard deploy failed" } + $fqdn_std = az container show -g $cfg.resourceGroup -n $name_std --query "ipAddress.fqdn" -o tsv + Write-Success "Standard : http://$fqdn_std" + + Write-Host "" + Write-Host "Waiting for both containers to come up..." + Wait-ForContainer -Fqdn $fqdn_conf | Out-Null + Wait-ForContainer -Fqdn $fqdn_std | Out-Null + + Write-Header "Both containers deployed" + + # Retrieve SKU info + $confSku = az container show -g $cfg.resourceGroup -n $name_conf --query "sku" -o tsv + $stdSku = az container show -g $cfg.resourceGroup -n $name_std --query "sku" -o tsv + + # Output summary table + Write-Host "" + Write-Host "╔════════════════════════════════════════════════════════════════╗" + Write-Host "║ Compare Deployment Summary ║" + Write-Host "╠════════════════════════════════════════════════════════════════╣" + Write-Host "║ Confidential SKU (Attestation succeeds) ║" + Write-Host "║ SKU: $($confSku.PadRight(54)) ║" + Write-Host "║ URL: http://$($fqdn_conf.PadRight(49)) ║" + Write-Host "╠════════════════════════════════════════════════════════════════╣" + Write-Host "║ Standard SKU (Attestation FAILS) ║" + Write-Host "║ SKU: $($stdSku.PadRight(54)) ║" + Write-Host "║ URL: http://$($fqdn_std.PadRight(49)) ║" + Write-Host "╚════════════════════════════════════════════════════════════════╝" + Write-Host "" + Write-Host "Click 'Attest' on each pane to see the difference:" + Write-Host " • Confidential: Hardware-rooted SEV-SNP attestation succeeds" + Write-Host " • Standard: Attestation fails (no /dev/sev-guest)" + Write-Host "" + + if (-not $SkipBrowser) { + $confUrl = "http://$fqdn_conf" + $stdUrl = "http://$fqdn_std" + $comparePage = Join-Path $PSScriptRoot 'side-by-side-compare.html' + New-ComparePage -ConfidentialUrl $confUrl -StandardUrl $stdUrl -OutputPath $comparePage + Write-Success "Generated: $comparePage" + Write-Host "Opening in new browser window..." + Start-Process $comparePage + } +} + +# ============================================================================ +# Cleanup phase +# ============================================================================ +function Invoke-Cleanup { + Write-Header "Cleanup phase" + $cfg = Get-Config + if (-not $cfg) { Write-Warn2 "No acr-config.json - nothing to delete."; return } + + Write-Warn2 "About to DELETE resource group: $($cfg.resourceGroup)" + Write-Warn2 "This removes the ACR, the image, and all container groups in that RG." + $confirm = Read-Host "Type the resource group name to confirm" + if ($confirm -ne $cfg.resourceGroup) { + Write-Host "Cancelled." + return + } + az group delete --name $cfg.resourceGroup --yes --no-wait + if ($LASTEXITCODE -eq 0) { + Write-Success "Delete submitted (running in background)." + Remove-Item $ConfigPath -Force -ErrorAction SilentlyContinue + Get-ChildItem $PSScriptRoot -Filter 'deployment-params-*.json' -ErrorAction SilentlyContinue | Remove-Item -Force + } else { + throw "az group delete failed" + } +} + +# ============================================================================ +# Main +# ============================================================================ +switch ($PSCmdlet.ParameterSetName) { + 'Build' { Invoke-Build } + 'Deploy' { Invoke-Deploy } + 'Compare' { Invoke-Compare } + 'Cleanup' { Invoke-Cleanup } + default { + Get-Help $PSCommandPath -Detailed + } +} diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/Dockerfile b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/Dockerfile new file mode 100644 index 000000000..0279fd000 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/Dockerfile @@ -0,0 +1,42 @@ +# Visual Attestation Demo v2 image for Azure Container Instances. +# +# Single-container demo - no SKR sidecar. The Flask app: +# 1. runs the upstream `get-snp-report` tool (from +# microsoft/confidential-sidecar-containers, compiled in stage 1) against +# /dev/sev-guest to obtain a fresh AMD SEV-SNP attestation report, +# 2. loads the THIM cert chain + UVM endorsements from the +# UVM_SECURITY_CONTEXT_DIR mount that the ACI control plane provides, +# 3. POSTs the SNP report + cert chain + runtime data to MAA's +# /attest/SevSnpVm endpoint and renders the returned JWT claims. +# +# - ACI Confidential SKU -> /dev/sev-guest exists, MAA returns sevsnpvm token +# - ACI Standard SKU -> /dev/sev-guest is absent, fails before MAA call +# (educational failure mode) +# +# Built server-side by `az acr build`, no local Docker required. + +FROM debian:bookworm-slim AS build +ARG SIDECAR_TAG=v2.14 +RUN apt-get update \ + && apt-get install -y --no-install-recommends git make gcc libc6-dev ca-certificates \ + && rm -rf /var/lib/apt/lists/* +RUN git clone --depth 1 --branch ${SIDECAR_TAG} \ + https://github.com/microsoft/confidential-sidecar-containers.git /src +WORKDIR /src/tools/get-snp-report +RUN make bin/get-snp-report + +FROM python:3.11-slim +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PORT=80 + +WORKDIR /app +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +COPY --from=build /src/tools/get-snp-report/bin/get-snp-report /usr/local/bin/get-snp-report +COPY app.py /app/app.py +COPY templates/ /app/templates/ + +EXPOSE 80 +CMD ["python", "/app/app.py"] diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/README.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/README.md new file mode 100644 index 000000000..9a258a562 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/README.md @@ -0,0 +1,230 @@ +# Visual Attestation Demo v2 on Azure Container Instances + +A self-contained ACI port of the AKS confidential-node attestation web UI from +`aks-samples/azure-voting-app/attestation/`. This is the **v2** of the original +[`visual-attestation-demo`](../visual-attestation-demo/) - same goal, simpler +footprint, and adds a one-shot `-Compare` mode that deploys both Confidential +and Standard SKUs side-by-side. + +It demonstrates **runtime guest attestation** of an AMD SEV-SNP TEE via +Microsoft Azure Attestation (MAA), and shows how the **same image fails to +attest** when run on a non-confidential ACI SKU - the educational point of the +sample. + +The container image is built **server-side in Azure Container Registry** via +`az acr build`, so you do **not** need a local Docker daemon to ship the image. +You only need Docker for the Confidential SKU deploy, where the +`az confcom acipolicygen` extension uses Docker to compute the CCE policy hash. + +## How attestation works in this sample + +There is **no SKR sidecar**. The Flask app fetches the SNP report itself, then +talks directly to MAA: + +1. The app builds a small JSON `runtimeData = {nonce, client}` and sets + `REPORT_DATA = SHA-256(runtimeData) || 0x00...` (64 bytes total). +2. It runs `get-snp-report ` (the upstream tool from + [microsoft/confidential-sidecar-containers](https://github.com/microsoft/confidential-sidecar-containers/tree/main/tools/get-snp-report), + baked into the image), which opens `/dev/sev-guest` and issues + `SNP_GUEST_REQUEST`. The AMD Secure Processor returns the 1184-byte + attestation report signed by the chip's VCEK. +3. It loads the THIM cert chain (`host-amd-cert-base64`) and UVM endorsements + (`reference-info-base64`) from the `UVM_SECURITY_CONTEXT_DIR` directory the + ACI control plane mounts into Confidential container groups. +4. It POSTs `{report, runtimeData, nonce}` to + `https:///attest/SevSnpVm?api-version=2022-08-01`. MAA verifies the + SNP report against the AMD root CA chain and the UVM endorsements, then + returns a signed JWT whose `x-ms-sevsnpvm-*` claims describe the TEE state + at this instant. +5. The app decodes the JWT and renders every claim with a human-readable + explanation. + +On ACI **Standard** SKU there is no `/dev/sev-guest` - step 2 fails before +MAA is ever called, which is the deterministic, hardware-rooted failure the +demo is built to show. + +## What's in here + +| File | Purpose | +|------|---------| +| [Dockerfile](Dockerfile) | Multi-stage build. Stage 1 compiles `get-snp-report` from microsoft/confidential-sidecar-containers; stage 2 is `python:3.11-slim` with Flask + the static binary. | +| [app.py](app.py) | Flask app - runs `get-snp-report` against `/dev/sev-guest`, posts the report + UVM evidence to MAA, and renders the returned JWT. | +| [templates/index.html](templates/index.html) | UI with dark mode, claim explanations, single-layer `sevsnpvm` token rendering. | +| [requirements.txt](requirements.txt) | `flask` + `requests`. | +| [deployment-template-confidential.json](deployment-template-confidential.json) | Single-container ACI template (`cc-attest`), `sku=Confidential`, `ccePolicy` filled by `confcom` at deploy time. | +| [deployment-template-standard.json](deployment-template-standard.json) | Single-container ACI template, `sku=Standard` (attestation fails by design). | +| [Deploy-VisualAttestationV2.ps1](Deploy-VisualAttestationV2.ps1) | One script for `-Build`, `-Deploy`, `-Compare`, `-Cleanup`. | + +## Architecture + +``` + Azure subscription + +---------------------------+ + | --rg | + | | + | +----------------------+ | ++-----------+ | | Azure Container | | +| | | | Registry (Basic, | | +| az acr +---+->| admin enabled) | | +| build . | | | cc-attest:1.0 | | +| | | +----------+-----------+ | ++-----------+ | | | + | v | + | +----------+-----------+ | MAA + | | ACI Confidential SKU |--+--HTTPS-->/attest/SevSnpVm + | | (AMD SEV-SNP) | | (sharedeus.eus...) + | | /dev/sev-guest | | + | +----------------------+ | + | +----------------------+ | + | | ACI Standard SKU | | <- no /dev/sev-guest, fails + | | (no TEE) | | + | +----------------------+ | + +---------------------------+ +``` + +## Prerequisites + +- Azure CLI logged in: `az login` +- Subscription with quota for **Confidential ACI** (AMD SEV-SNP) in your + region. `eastus` is the default; `northeurope`, `westeurope`, + `southcentralus`, `eastus2` also work. +- For confidential deploys only: **Docker Desktop** running locally, plus the + `confcom` Azure CLI extension. The script auto-installs `confcom` for you. + +The sample does **not** require any role assignments +(`Microsoft.Authorization/*`) - ACR pulls happen via admin user credentials +embedded in `imageRegistryCredentials`. + +## Quick start + +### 1. Build the image (server-side, ~5 minutes) + +```powershell +./Deploy-VisualAttestationV2.ps1 -Build +``` + +This creates a resource group, an ACR, runs `az acr build` on the Dockerfile +in this directory, and persists the image coordinates to `acr-config.json`. + +### 2. Deploy on Confidential SKU (attestation will succeed) + +```powershell +./Deploy-VisualAttestationV2.ps1 -Deploy +``` + +Requires Docker running locally so `confcom` can compute the CCE policy. +Opens the UI in your browser - click **Attest** and you'll get a fully +populated MAA token with `x-ms-attestation-type=sevsnpvm` and +`x-ms-compliance-status=azure-compliant-uvm`. + +### 3. Deploy on Standard SKU (attestation will fail) + +```powershell +./Deploy-VisualAttestationV2.ps1 -Deploy -NoAcc +``` + +No Docker required. The same image runs unmodified, but with no SEV-SNP +hardware `/dev/sev-guest` is absent and `get-snp-report` fails before MAA is +ever called. The `/api/attest` endpoint returns that error and the script +prints the last 25 lines of container logs for inspection. + +### 4. Side-by-side comparison + +```powershell +./Deploy-VisualAttestationV2.ps1 -Compare +``` + +Deploys both flavors in the same resource group and opens a generated +`side-by-side-compare.html` page that embeds both live endpoints in one view. +Same image, same code path, opposite result - the cleanest way to demo why +attestation matters. + +### 5. Cleanup + +```powershell +./Deploy-VisualAttestationV2.ps1 -Cleanup +``` + +Confirms by re-typing the resource group name, then deletes everything +(`--no-wait`). + +## Demo screenshots + +The screenshots below were captured against a live `-Compare` deployment +(`cc-attest-conf-*` and `cc-attest-std-*` running side-by-side in the same +resource group, same image digest) by clicking **Attest** on each instance. + +### Confidential SKU - attestation succeeds + +`get-snp-report` reads the AMD SEV-SNP report from `/dev/sev-guest`, the app +posts it with the THIM cert chain and UVM endorsements to MAA, and the page +renders the decoded JWT. Notice `x-ms-attestation-type = sevsnpvm` and +`x-ms-compliance-status = azure-compliant-uvm`, plus the per-chip +`x-ms-sevsnpvm-chipid`, launch measurement, and TCB version - all rooted in +silicon. + +![Confidential SKU - successful SEV-SNP attestation via direct MAA call](images/screenshot-confidential.png) + +### Standard SKU - attestation fails (by design) + +Same image, no SEV-SNP hardware, no `/dev/sev-guest`. `get-snp-report` errors +out before MAA is ever called. That deterministic failure is the educational +contrast - it proves the success case really did need confidential hardware. + +![Standard SKU - attestation fails - /dev/sev-guest absent on non-CC SKU](images/screenshot-standard.png) + +### Side-by-side compare view + +The compare page below is generated automatically by +`Deploy-VisualAttestationV2.ps1 -Compare` and opens both live deployments in +one browser window. + +![Confidential and Standard side-by-side compare view](images/screenshot-side-by-side.png) + +## Why two SKUs? + +The point of this sample is **falsifiability**. A demo that only ever shows +attestation succeeding doesn't prove much - the same JSON response could be +mocked by any web server. By running the *exact same image* on a non-TEE host +and watching it fail in a specific, hardware-rooted way (no `/dev/sev-guest`, +no SEV-SNP guest report), you demonstrate that: + +1. The success case on Confidential SKU really did come from AMD silicon. +2. A relying party that pins `x-ms-attestation-type=sevsnpvm` and a specific + `x-ms-policy-hash` cannot be spoofed by a Standard-SKU deployment. + +## Differences vs. the AKS sample + +| Aspect | AKS sample | This sample | +|--------|------------|-------------| +| Bootstrap | ConfigMap mounts source at pod start | Source baked into image at build time | +| Attestation path | App opens `/dev/tpmrm0` directly via `cvm-attestation-tools` | App runs `get-snp-report` against `/dev/sev-guest`, then POSTs SNP report + THIM cert chain + UVM endorsements directly to MAA `/attest/SevSnpVm` (no SKR sidecar; ACI CC UVM does not expose a vTPM to the workload) | +| Token shape | Nested - outer with `x-ms-isolation-tee.x-ms-runtime` | Single-layer `sevsnpvm` token (claims at top level) | +| Privileges | `privileged: true`, `/dev/tpmrm0` host mount | None - `/dev/sev-guest` is auto-exposed in ACI Confidential SKU | +| Build | `az acr build` | `az acr build` | +| TEE | Per-node SEV-SNP (`Standard_DC2as_v5`) | Per-container-group SEV-SNP (Confidential ACI) | +| Failure demo | Add a non-CC nodepool | `-NoAcc` switch -> Standard SKU (no `/dev/sev-guest`, no TEE) | + +## Troubleshooting + +**`az confcom acipolicygen` complains Docker isn't running** +Start Docker Desktop, or use `-NoAcc` to skip confidential mode. + +**Container is `CrashLoopBackOff` on Confidential SKU** +View logs: `az container logs -g -n --container-name cc-attest`. +Two common causes: (a) `get-snp-report` failed because `/dev/sev-guest` is +not yet accessible (race with container start - retry once); (b) the THIM +cert chain at `UVM_SECURITY_CONTEXT_DIR/host-amd-cert-base64` was missing, +which means the ACI control plane did not provision it - confirm the +container group really came up as `sku=Confidential` and that +`confidentialComputeProperties.ccePolicy` was supplied. + +**MAA returns HTTP 400 / `Failed validation`** +The CCE policy hash MAA computed from `host-amd-cert-base64` and the report's +`HOST_DATA` did not match the `ccePolicy` stamped into the container group. +Re-run `Deploy-VisualAttestationV2.ps1 -Compare` so `confcom acipolicygen` +regenerates the policy for the current image. + +**HTTP timeout waiting for the container** +First-time pulls of the image can take 2-3 minutes on a cold ACI host. +Re-open the URL after a minute. diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/app.py b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/app.py new file mode 100644 index 000000000..9308ffbf1 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/app.py @@ -0,0 +1,342 @@ +""" +SEV-SNP runtime attestation web UI for ACI Confidential Containers. + +This app skips the SKR sidecar and talks to MAA itself: + + 1. `get-snp-report` (from microsoft/confidential-sidecar-containers, baked + into the image) opens /dev/sev-guest and asks the AMD Secure Processor + for a fresh 1184-byte attestation report bound to a caller-supplied + REPORT_DATA (sha256 of a per-request runtime data blob). + 2. The app reads the THIM cert chain and UVM reference info that the ACI + control plane drops into UVM_SECURITY_CONTEXT_DIR (or the equivalent + environment variables). + 3. The app POSTs report + cert chain + endorsements + runtime data to MAA's + /attest/SevSnpVm endpoint, which validates the hardware evidence, + measures the runtime data into REPORT_DATA, and returns a signed JWT. + 4. The app decodes the JWT and renders every claim with a human-readable + explanation. + + - ACI Confidential SKU -> /dev/sev-guest + UVM context, MAA returns sevsnpvm + - ACI Standard SKU -> no /dev/sev-guest, get-snp-report fails by design + +References: + https://github.com/microsoft/confidential-sidecar-containers/tree/main/tools/get-snp-report + https://learn.microsoft.com/rest/api/attestation/attestation/attest-sev-snp-vm + AMD SEV-SNP ABI specification (publication 56860) +""" + +import base64 +import hashlib +import json +import os +import secrets +import struct +import subprocess +import traceback +from datetime import datetime, timezone +from pathlib import Path + +import requests +from flask import Flask, jsonify, render_template, request + + +GET_SNP_REPORT = os.environ.get("GET_SNP_REPORT", "/usr/local/bin/get-snp-report") +DEFAULT_MAA = os.environ.get("MAA_ENDPOINT", "sharedeus.eus.attest.azure.net") +MAA_API_VERSION = os.environ.get("MAA_API_VERSION", "2022-08-01") + + +# --------------------------------------------------------------------------- +# Claim explanations for MAA SEV-SNP tokens. +# --------------------------------------------------------------------------- +CLAIM_EXPLANATIONS = { + "iss": "Issuer - the MAA endpoint that signed this token. Verifying the issuer URL pins the token to a specific Azure Attestation provider.", + "iat": "Issued At (Unix epoch seconds) - when MAA produced the token.", + "exp": "Expiration (Unix epoch seconds) - after this point the token must not be trusted.", + "nbf": "Not Before (Unix epoch seconds) - the token is not valid earlier than this time.", + "jti": "JWT ID - a unique identifier MAA assigns to this token; useful for replay detection.", + + "x-ms-ver": "MAA token schema version.", + "x-ms-attestation-type": "Type of TEE that produced the evidence ('sevsnpvm', 'tdxvm', 'azurevm', 'tpm', etc.).", + "x-ms-compliance-status": "MAA's overall verdict against its policy. 'azure-compliant-uvm' means the platform passed all Azure CC UVM policy checks.", + "x-ms-policy-hash": "SHA-256 of the MAA policy (base64url) that evaluated this evidence. Pin this in your relying party to detect policy drift.", + "x-ms-policy-signer": "If a custom JWS-signed MAA policy was used, this is the signer's certificate chain.", + "x-ms-runtime": "Caller-supplied runtime data that was bound into the hardware report's REPORT_DATA field.", + "x-ms-inittime": "Init-time data bound into the report (the CCE policy hash on ACI CC).", + "nonce": "Caller-supplied nonce echoed back inside x-ms-runtime to prove freshness.", + + "x-ms-sevsnpvm-authorkeydigest": "SHA-384 of the SEV-SNP author key (AKD). Zero on Azure-managed CVMs unless you bring your own ID/Author key.", + "x-ms-sevsnpvm-bootloader-svn": "SVN of the AMD SEV-SNP guest bootloader at launch.", + "x-ms-sevsnpvm-familyId": "Family ID supplied at SNP_LAUNCH_FINISH (16 bytes). On Azure CVMs this identifies the Azure VM family.", + "x-ms-sevsnpvm-guestsvn": "Guest Security Version Number stamped into the report. Allows monotonic anti-rollback in your policy.", + "x-ms-sevsnpvm-hostdata": "Host data the hypervisor injected at launch (sha256 of the CCE policy on ACI CC). Lets you bind the guest to a specific host configuration.", + "x-ms-sevsnpvm-idkeydigest": "SHA-384 of the SEV-SNP ID Key (IDK). On Azure CC nodes this is the Azure-managed launch ID key digest.", + "x-ms-sevsnpvm-imageId": "Image ID supplied at SNP_LAUNCH_FINISH (16 bytes).", + "x-ms-sevsnpvm-is-debuggable": "true means the guest was launched with the SNP debug policy bit set. For production CVMs this MUST be false.", + "x-ms-sevsnpvm-launchmeasurement": "SHA-384 of the initial guest memory contents measured by AMD-SP at launch. This is the cryptographic identity of the boot image.", + "x-ms-sevsnpvm-microcode-svn": "Microcode SVN of the AMD CPU at attestation time.", + "x-ms-sevsnpvm-migration-allowed": "true if the SNP guest policy permits migration between machines. Azure CVMs report false.", + "x-ms-sevsnpvm-reportdata": "Hex of REPORT_DATA - 64 bytes the guest itself supplied when requesting the report. MAA stuffs the SHA-256 of x-ms-runtime here so you can cryptographically link the token to caller-supplied data.", + "x-ms-sevsnpvm-reportid": "Per-launch report ID assigned by AMD-SP. Different across reboots.", + "x-ms-sevsnpvm-smt-allowed": "true means simultaneous multithreading was allowed at launch (per SNP guest policy).", + "x-ms-sevsnpvm-snpfw-svn": "SVN of the AMD SEV-SNP firmware (PSP) at attestation time.", + "x-ms-sevsnpvm-tee-svn": "SVN of the TEE component (always 0 for SEV-SNP today; reserved).", + "x-ms-sevsnpvm-vmpl": "Virtual Machine Privilege Level the report was generated at. Azure CVMs run the OS at VMPL0.", +} + + +def _explain(key: str) -> str: + if key in CLAIM_EXPLANATIONS: + return CLAIM_EXPLANATIONS[key] + if key.startswith("x-ms-sevsnpvm-"): + return "SEV-SNP attestation report field surfaced by MAA. See AMD SEV-SNP ABI spec for the underlying bit layout." + if key.startswith("x-ms-"): + return "MAA-issued claim. Refer to the Microsoft Azure Attestation claim-set documentation." + return "Standard JWT or caller-supplied claim." + + +# --------------------------------------------------------------------------- +# JWT helpers (display only). +# --------------------------------------------------------------------------- +def _b64url_decode(segment: str) -> bytes: + pad = "=" * (-len(segment) % 4) + return base64.urlsafe_b64decode(segment + pad) + + +def decode_jwt(token: str): + if isinstance(token, bytes): + token = token.decode("utf-8") + parts = token.split(".") + if len(parts) != 3: + raise ValueError(f"Token does not have 3 segments (got {len(parts)})") + return json.loads(_b64url_decode(parts[0])), json.loads(_b64url_decode(parts[1])) + + +def _format_timestamp(value): + try: + return datetime.fromtimestamp(int(value), tz=timezone.utc).isoformat() + except Exception: + return None + + +def annotate_claims(payload: dict): + rows = [] + for key, value in payload.items(): + row = {"key": key, "value": value, "explanation": _explain(key)} + if key in {"iat", "exp", "nbf"}: + row["timestamp"] = _format_timestamp(value) + rows.append(row) + rows.sort(key=lambda r: (not r["key"].startswith("x-ms-"), r["key"])) + return rows + + +# --------------------------------------------------------------------------- +# UVM information - THIM certs and reference info supplied by the ACI control +# plane. Same lookup order as the SKR sidecar: +# 1. UVM_SECURITY_CONTEXT_DIR (or auto-discovered /security-context-*) +# 2. UVM_HOST_AMD_CERTIFICATE / UVM_REFERENCE_INFO env vars (legacy). +# --------------------------------------------------------------------------- +def _find_security_context_dir() -> str | None: + explicit = os.environ.get("UVM_SECURITY_CONTEXT_DIR") + if explicit and os.path.isdir(explicit): + return explicit + try: + for entry in os.listdir("/"): + if entry.startswith("security-context-"): + full = os.path.join("/", entry) + if os.path.isdir(full): + return full + except OSError: + pass + return None + + +def load_uvm_information() -> dict: + """Returns dict with keys: host_amd_cert_b64, reference_info_b64, source.""" + ctx_dir = _find_security_context_dir() + if ctx_dir: + def _read(name): + p = Path(ctx_dir) / name + return p.read_text().strip() if p.exists() else "" + return { + "host_amd_cert_b64": _read("host-amd-cert-base64"), + "reference_info_b64": _read("reference-info-base64"), + "source": ctx_dir, + } + return { + "host_amd_cert_b64": os.environ.get("UVM_HOST_AMD_CERTIFICATE", ""), + "reference_info_b64": os.environ.get("UVM_REFERENCE_INFO", ""), + "source": "env", + } + + +def _b64url(b: bytes) -> str: + return base64.urlsafe_b64encode(b).decode("ascii") + + +def _build_maa_report(snp_report: bytes, vcek_cert_chain: bytes, endorsements_json: bytes | None) -> str: + """Pack hardware report + cert chain + endorsements into MAA's `report` blob.""" + inner = { + "SnpReport": _b64url(snp_report), + "VcekCertChain": _b64url(vcek_cert_chain), + } + if endorsements_json: + inner["Endorsements"] = _b64url(endorsements_json) + return _b64url(json.dumps(inner, separators=(",", ":")).encode("utf-8")) + + +# --------------------------------------------------------------------------- +# get-snp-report invocation. +# --------------------------------------------------------------------------- +REPORT_LEN = 0x4A0 # 1184 bytes + + +def fetch_snp_report(report_data: bytes) -> bytes: + if not (os.path.exists("/dev/sev-guest") or os.path.exists("/dev/sev")): + raise RuntimeError( + "Neither /dev/sev-guest nor /dev/sev is present in this container. " + "On ACI Standard SKU there is no AMD SEV-SNP hardware - this is the " + "expected failure mode for the demo." + ) + proc = subprocess.run( + [GET_SNP_REPORT, report_data.hex()], + capture_output=True, + timeout=15, + check=False, + ) + if proc.returncode != 0: + raise RuntimeError( + f"get-snp-report exited {proc.returncode}. " + f"stdout: {proc.stdout.decode('utf-8', 'replace')[:500]} " + f"stderr: {proc.stderr.decode('utf-8', 'replace')[:500]}" + ) + hex_output = "".join(c for c in proc.stdout.decode("ascii", "replace") if c in "0123456789abcdefABCDEF") + report = bytes.fromhex(hex_output) + if len(report) < REPORT_LEN: + raise RuntimeError(f"SNP report too short: {len(report)} bytes") + return report[:REPORT_LEN] + + +def parse_snp_report_summary(buf: bytes) -> dict: + """Just enough fields for a side-panel summary; MAA does the real validation.""" + version = struct.unpack_from(" dict: + nonce = user_nonce or secrets.token_hex(16) + + # Runtime data: arbitrary JSON the caller wants bound into REPORT_DATA. + runtime_obj = {"nonce": nonce, "client": "visual-attestation-demo-v2"} + runtime_bytes = json.dumps(runtime_obj, separators=(",", ":")).encode("utf-8") + + # MAA hashes runtime data into REPORT_DATA, but only after we hand it the + # report. So we must fetch the report with REPORT_DATA = sha256(runtime). + report_data = hashlib.sha256(runtime_bytes).digest() + b"\x00" * 32 + + snp_report = fetch_snp_report(report_data) + summary = parse_snp_report_summary(snp_report) + + # Load THIM cert chain + UVM endorsements from the security context dir. + uvm = load_uvm_information() + if not uvm["host_amd_cert_b64"]: + raise RuntimeError( + "UVM host AMD certificate not found. Expected security-context-*/host-amd-cert-base64 " + "or UVM_HOST_AMD_CERTIFICATE env var. ACI control plane usually injects this on Confidential SKU." + ) + thim_certs_raw = base64.b64decode(uvm["host_amd_cert_b64"]) + thim_certs = json.loads(thim_certs_raw) + vcek_chain = (thim_certs.get("vcekCert", "") + thim_certs.get("certificateChain", "")).encode("utf-8") + + endorsements_json: bytes | None = None + if uvm["reference_info_b64"]: + ref_info = base64.b64decode(uvm["reference_info_b64"]) + endorsements_json = json.dumps( + {"Uvm": [_b64url(ref_info)]}, separators=(",", ":") + ).encode("utf-8") + + body = { + "report": _build_maa_report(snp_report, vcek_chain, endorsements_json), + "runtimeData": {"data": _b64url(runtime_bytes), "dataType": "JSON"}, + "nonce": secrets.randbits(63), + } + + url = f"https://{DEFAULT_MAA}/attest/SevSnpVm?api-version={MAA_API_VERSION}" + resp = requests.post(url, json=body, timeout=30, headers={"User-Agent": "visual-attestation-demo-v2"}) + if resp.status_code != 200: + raise RuntimeError(f"MAA POST {url} returned HTTP {resp.status_code}: {resp.text[:600]}") + token = resp.json().get("token") + if not token: + raise RuntimeError(f"MAA response missing 'token': {resp.text[:600]}") + + header, payload = decode_jwt(token) + + return { + "endpoint": f"https://{DEFAULT_MAA}", + "region": DEFAULT_MAA.split(".")[0], + "isolation_type": "SEV_SNP", + "nonce": nonce, + "token": token, + "header": header, + "payload": payload, + "claims": annotate_claims(payload), + "hardware_evidence": { + "maa_endpoint": DEFAULT_MAA, + "uvm_source": uvm["source"], + "snp_report_size": len(snp_report), + "snp_report_hex": snp_report.hex(), + "snp_summary": summary, + "runtime_data": runtime_obj, + "runtime_data_sha256": hashlib.sha256(runtime_bytes).hexdigest(), + }, + } + + +# --------------------------------------------------------------------------- +# Flask app +# --------------------------------------------------------------------------- +app = Flask(__name__) + + +@app.route("/", methods=["GET"]) +def index(): + is_confidential = os.environ.get("ACI_SKU", "").lower() == "confidential" + return render_template("index.html", is_confidential=is_confidential) + + +@app.route("/healthz", methods=["GET"]) +def healthz(): + return "ok", 200 + + +@app.route("/api/attest", methods=["POST"]) +def api_attest(): + user_nonce = (request.json or {}).get("nonce") if request.is_json else request.form.get("nonce") + try: + result = perform_attestation(user_nonce or None) + except Exception as exc: + return ( + jsonify({"ok": False, "error": str(exc), "trace": traceback.format_exc()}), + 500, + ) + return jsonify({"ok": True, **result}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "80"))) diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-confidential-allowall.json b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-confidential-allowall.json new file mode 100644 index 000000000..7858d4c27 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-confidential-allowall.json @@ -0,0 +1,98 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "containerGroupName": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "appImage": { + "type": "string" + }, + "registryServer": { + "type": "string" + }, + "registryUsername": { + "type": "string" + }, + "registryPassword": { + "type": "securestring" + }, + "dnsNameLabel": { + "type": "string" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.ContainerInstance/containerGroups", + "apiVersion": "2023-05-01", + "name": "[parameters('containerGroupName')]", + "location": "[parameters('location')]", + "properties": { + "sku": "Confidential", + "confidentialComputeProperties": { + "ccePolicy": "cGFja2FnZSBwb2xpY3kKYXBpX3ZlcnNpb24gOj0gIjAuMS4wIgppbXBvcnQgZnV0dXJlLmtleXdvcmRzLmV2ZXJ5CmltcG9ydCBmdXR1cmUua2V5d29yZHMuaW4KZnJhZ21lbnRzIDo9IFtdCmNvbnRhaW5lcnMgOj0gW10KYWxsb3dfcHJvcGVydGllc19hY2Nlc3MgOj0gdHJ1ZQphbGxvd19kdW1wX3N0YWNrcyA6PSB0cnVlCmFsbG93X3J1bnRpbWVfbG9nZ2luZyA6PSB0cnVlCmFsbG93X2Vudmlyb25tZW50X3ZhcmlhYmxlX2Ryb3BwaW5nIDo9IHRydWUKYWxsb3dfdW5lbmNyeXB0ZWRfc2NyYXRjaCA6PSB0cnVlCmFsbG93X2NhcGFiaWxpdHlfZHJvcHBpbmcgOj0gdHJ1ZQ==" + }, + "containers": [ + { + "name": "cc-attest", + "properties": { + "image": "[parameters('appImage')]", + "ports": [ + { + "protocol": "TCP", + "port": 80 + } + ], + "environmentVariables": [ + { + "name": "ACI_SKU", + "value": "Confidential" + }, + { + "name": "MAA_ENDPOINT", + "value": "sharedeus.eus.attest.azure.net" + } + ], + "resources": { + "requests": { + "memoryInGB": 2, + "cpu": 1 + } + } + } + } + ], + "imageRegistryCredentials": [ + { + "server": "[parameters('registryServer')]", + "username": "[parameters('registryUsername')]", + "password": "[parameters('registryPassword')]" + } + ], + "ipAddress": { + "ports": [ + { + "protocol": "TCP", + "port": 80 + } + ], + "type": "Public", + "dnsNameLabel": "[parameters('dnsNameLabel')]" + }, + "osType": "Linux", + "restartPolicy": "Always" + } + } + ], + "outputs": { + "fqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', parameters('containerGroupName'))).ipAddress.fqdn]" + } + } +} diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-confidential.json b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-confidential.json new file mode 100644 index 000000000..7ce9c8cb1 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-confidential.json @@ -0,0 +1,98 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "containerGroupName": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "appImage": { + "type": "string" + }, + "registryServer": { + "type": "string" + }, + "registryUsername": { + "type": "string" + }, + "registryPassword": { + "type": "securestring" + }, + "dnsNameLabel": { + "type": "string" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.ContainerInstance/containerGroups", + "apiVersion": "2023-05-01", + "name": "[parameters('containerGroupName')]", + "location": "[parameters('location')]", + "properties": { + "sku": "Confidential", + "confidentialComputeProperties": { + "ccePolicy": "cGFja2FnZSBwb2xpY3kKCmltcG9ydCBmdXR1cmUua2V5d29yZHMuZXZlcnkKaW1wb3J0IGZ1dHVyZS5rZXl3b3Jkcy5pbgoKYXBpX3ZlcnNpb24gOj0gIjAuMTEuMCIKZnJhbWV3b3JrX3ZlcnNpb24gOj0gIjAuMi4zIgoKZnJhZ21lbnRzIDo9IFsKICB7CiAgICAiZmVlZCI6ICJtY3IubWljcm9zb2Z0LmNvbS9hY2kvYWNpLWNjLWluZnJhLWZyYWdtZW50IiwKICAgICJpbmNsdWRlcyI6IFsKICAgICAgImNvbnRhaW5lcnMiLAogICAgICAiZnJhZ21lbnRzIgogICAgXSwKICAgICJpc3N1ZXIiOiAiZGlkOng1MDk6MDpzaGEyNTY6SV9faXVMMjVvWEVWRmRUUF9hQkx4X2VUMVJQSGJDUV9FQ0JRZllacHQ5czo6ZWt1OjEuMy42LjEuNC4xLjMxMS43Ni41OS4xLjMiLAogICAgIm1pbmltdW1fc3ZuIjogIjQiCiAgfQpdCgpjb250YWluZXJzIDo9IFt7ImFsbG93X2VsZXZhdGVkIjpmYWxzZSwiYWxsb3dfc3RkaW9fYWNjZXNzIjpmYWxzZSwiY2FwYWJpbGl0aWVzIjp7ImFtYmllbnQiOltdLCJib3VuZGluZyI6WyJDQVBfQVVESVRfV1JJVEUiLCJDQVBfQ0hPV04iLCJDQVBfREFDX09WRVJSSURFIiwiQ0FQX0ZPV05FUiIsIkNBUF9GU0VUSUQiLCJDQVBfS0lMTCIsIkNBUF9NS05PRCIsIkNBUF9ORVRfQklORF9TRVJWSUNFIiwiQ0FQX05FVF9SQVciLCJDQVBfU0VURkNBUCIsIkNBUF9TRVRHSUQiLCJDQVBfU0VUUENBUCIsIkNBUF9TRVRVSUQiLCJDQVBfU1lTX0NIUk9PVCJdLCJlZmZlY3RpdmUiOlsiQ0FQX0FVRElUX1dSSVRFIiwiQ0FQX0NIT1dOIiwiQ0FQX0RBQ19PVkVSUklERSIsIkNBUF9GT1dORVIiLCJDQVBfRlNFVElEIiwiQ0FQX0tJTEwiLCJDQVBfTUtOT0QiLCJDQVBfTkVUX0JJTkRfU0VSVklDRSIsIkNBUF9ORVRfUkFXIiwiQ0FQX1NFVEZDQVAiLCJDQVBfU0VUR0lEIiwiQ0FQX1NFVFBDQVAiLCJDQVBfU0VUVUlEIiwiQ0FQX1NZU19DSFJPT1QiXSwiaW5oZXJpdGFibGUiOltdLCJwZXJtaXR0ZWQiOlsiQ0FQX0FVRElUX1dSSVRFIiwiQ0FQX0NIT1dOIiwiQ0FQX0RBQ19PVkVSUklERSIsIkNBUF9GT1dORVIiLCJDQVBfRlNFVElEIiwiQ0FQX0tJTEwiLCJDQVBfTUtOT0QiLCJDQVBfTkVUX0JJTkRfU0VSVklDRSIsIkNBUF9ORVRfUkFXIiwiQ0FQX1NFVEZDQVAiLCJDQVBfU0VUR0lEIiwiQ0FQX1NFVFBDQVAiLCJDQVBfU0VUVUlEIiwiQ0FQX1NZU19DSFJPT1QiXX0sImNvbW1hbmQiOlsicHl0aG9uIiwiL2FwcC9hcHAucHkiXSwiZW52X3J1bGVzIjpbeyJwYXR0ZXJuIjoiQUNJX1NLVT1Db25maWRlbnRpYWwiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5Ijoic3RyaW5nIn0seyJwYXR0ZXJuIjoiTUFBX0VORFBPSU5UPXNoYXJlZGV1cy5ldXMuYXR0ZXN0LmF6dXJlLm5ldCIsInJlcXVpcmVkIjpmYWxzZSwic3RyYXRlZ3kiOiJzdHJpbmcifSx7InBhdHRlcm4iOiJQQVRIPS91c3IvbG9jYWwvYmluOi91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsInJlcXVpcmVkIjpmYWxzZSwic3RyYXRlZ3kiOiJzdHJpbmcifSx7InBhdHRlcm4iOiJMQU5HPUMuVVRGLTgiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5Ijoic3RyaW5nIn0seyJwYXR0ZXJuIjoiR1BHX0tFWT1BMDM1QzhDMTkyMTlCQTgyMUVDRUE4NkI2NEU2MjhGOEQ2ODQ2OTZEIiwicmVxdWlyZWQiOmZhbHNlLCJzdHJhdGVneSI6InN0cmluZyJ9LHsicGF0dGVybiI6IlBZVEhPTl9WRVJTSU9OPTMuMTEuMTUiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5Ijoic3RyaW5nIn0seyJwYXR0ZXJuIjoiUFlUSE9OX1NIQTI1Nj0yNzIxNzlkZGQ5YTJlNDFhMGZjOGU0MmUzM2RmYmRjYTBiMzcxMWFhNWFiZjM3MmQzZjJkNTE1NDNkMDliNjI1IiwicmVxdWlyZWQiOmZhbHNlLCJzdHJhdGVneSI6InN0cmluZyJ9LHsicGF0dGVybiI6IlBZVEhPTkRPTlRXUklURUJZVEVDT0RFPTEiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5Ijoic3RyaW5nIn0seyJwYXR0ZXJuIjoiUFlUSE9OVU5CVUZGRVJFRD0xIiwicmVxdWlyZWQiOmZhbHNlLCJzdHJhdGVneSI6InN0cmluZyJ9LHsicGF0dGVybiI6IlBPUlQ9ODAiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5Ijoic3RyaW5nIn0seyJwYXR0ZXJuIjoiVEVSTT14dGVybSIsInJlcXVpcmVkIjpmYWxzZSwic3RyYXRlZ3kiOiJzdHJpbmcifSx7InBhdHRlcm4iOiIoP2kpKEZBQlJJQylfLis9LisiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5IjoicmUyIn0seyJwYXR0ZXJuIjoiSE9TVE5BTUU9LisiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5IjoicmUyIn0seyJwYXR0ZXJuIjoiVChFKT9NUD0uKyIsInJlcXVpcmVkIjpmYWxzZSwic3RyYXRlZ3kiOiJyZTIifSx7InBhdHRlcm4iOiJGYWJyaWNQYWNrYWdlRmlsZU5hbWU9LisiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5IjoicmUyIn0seyJwYXR0ZXJuIjoiSG9zdGVkU2VydmljZU5hbWU9LisiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5IjoicmUyIn0seyJwYXR0ZXJuIjoiSURFTlRJVFlfQVBJX1ZFUlNJT049LisiLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5IjoicmUyIn0seyJwYXR0ZXJuIjoiSURFTlRJVFlfSEVBREVSPS4rIiwicmVxdWlyZWQiOmZhbHNlLCJzdHJhdGVneSI6InJlMiJ9LHsicGF0dGVybiI6IklERU5USVRZX1NFUlZFUl9USFVNQlBSSU5UPS4rIiwicmVxdWlyZWQiOmZhbHNlLCJzdHJhdGVneSI6InJlMiJ9LHsicGF0dGVybiI6ImF6dXJlY29udGFpbmVyaW5zdGFuY2VfcmVzdGFydGVkX2J5PS4rIiwicmVxdWlyZWQiOmZhbHNlLCJzdHJhdGVneSI6InJlMiJ9XSwiZXhlY19wcm9jZXNzZXMiOltdLCJpZCI6ImFjcnFseXVzb2VnLmF6dXJlY3IuaW8vY2MtYXR0ZXN0OjEuMCIsImxheWVycyI6WyIxZjdjY2YzOTIxMTczOTMyZTU2ZjZmMzA4ZGE3Mjc5YWU3M2U0NDhhMmNiNDNkNGRhMDBkOTQ3ODRiNzAxZWI5IiwiMGRmN2Q2ZGI0NTBhZGRlOTIwNjk3ZjRiYWM2MjhiYWRmYjVmYWY3YTliM2FlMzYwMTU2ZTFhYzQxYmFiZjc1OSIsIjA4ZGExODBmZjJiNDk4YWYwMGI4ZmIxYzc3ZTIyZGFlZWI2MGExNDA4Y2MwMDlkMzIxOTE4Y2U4MWYwZjRkNGQiLCI4NjIyNjBlYmEzMmE5MGU3NDI5Mzk2ODExYmNkMWI2ZDAwNmIwOWZkYTEyZWM1YTJiMTY5YjcxNzdhNzAxMGVkIiwiMTRiMzgwZmQ1MDJlZWM0NDEwYjVlNDhkZWVjNzhkNzlmNTg4YmMyZTYzMDA2YjAwYWNkZjEyZjNlN2QxOGZmMCIsIjFjNmNhYTAzMzA4NDJhODFkN2NjODI2YmI2ZmUzNzBhYmFkZmFiYmIzNTNkYzBjMDcyOGJiZGU1NWE4NDRjOTEiLCJiNTMwOGIwZjYyZjVjMGI0MDNkOGE0ZDA5YThmZDllNTg0M2I4ZGZhNzU3MDZjY2I1MGIxNjhhZGNhZjRlNTQ2IiwiNDgwNWU0MjJlYTU3MWY4NGJkNTBlZWE4MTkwMTY0N2NkYzVjY2MxMWRkYWUxZWViNWNlZTMzYThjNzJmYzcyMyIsIjg1MTMwNmMxZWZkZWM2OGZkMjkzZWQ2MmE5YTk0YmQzODI5MzVmNjQwZjIyM2FmZDAzMWQwMGZiYWE1ZjFmZjkiLCI4NmVjOWFlOWVmMzUxZjMxYjU2MWRjYjE5ODdkODJmODE3YWM5MTIwMTU2OTA5NDc5ZDIzYTQwMGNjMzQ2YzgyIl0sIm1vdW50cyI6W3siZGVzdGluYXRpb24iOiIvZXRjL3Jlc29sdi5jb25mIiwib3B0aW9ucyI6WyJyYmluZCIsInJzaGFyZWQiLCJydyJdLCJzb3VyY2UiOiJzYW5kYm94Oi8vL3RtcC9hdGxhcy9yZXNvbHZjb25mLy4rIiwidHlwZSI6ImJpbmQifV0sIm5hbWUiOiJjYy1hdHRlc3QiLCJub19uZXdfcHJpdmlsZWdlcyI6ZmFsc2UsInNlY2NvbXBfcHJvZmlsZV9zaGEyNTYiOiIiLCJzaWduYWxzIjpbXSwidXNlciI6eyJncm91cF9pZG5hbWVzIjpbeyJwYXR0ZXJuIjoiIiwic3RyYXRlZ3kiOiJhbnkifV0sInVtYXNrIjoiMDAyMiIsInVzZXJfaWRuYW1lIjp7InBhdHRlcm4iOiIiLCJzdHJhdGVneSI6ImFueSJ9fSwid29ya2luZ19kaXIiOiIvYXBwIn0seyJhbGxvd19lbGV2YXRlZCI6ZmFsc2UsImFsbG93X3N0ZGlvX2FjY2VzcyI6ZmFsc2UsImNhcGFiaWxpdGllcyI6eyJhbWJpZW50IjpbXSwiYm91bmRpbmciOlsiQ0FQX0NIT1dOIiwiQ0FQX0RBQ19PVkVSUklERSIsIkNBUF9GU0VUSUQiLCJDQVBfRk9XTkVSIiwiQ0FQX01LTk9EIiwiQ0FQX05FVF9SQVciLCJDQVBfU0VUR0lEIiwiQ0FQX1NFVFVJRCIsIkNBUF9TRVRGQ0FQIiwiQ0FQX1NFVFBDQVAiLCJDQVBfTkVUX0JJTkRfU0VSVklDRSIsIkNBUF9TWVNfQ0hST09UIiwiQ0FQX0tJTEwiLCJDQVBfQVVESVRfV1JJVEUiXSwiZWZmZWN0aXZlIjpbIkNBUF9DSE9XTiIsIkNBUF9EQUNfT1ZFUlJJREUiLCJDQVBfRlNFVElEIiwiQ0FQX0ZPV05FUiIsIkNBUF9NS05PRCIsIkNBUF9ORVRfUkFXIiwiQ0FQX1NFVEdJRCIsIkNBUF9TRVRVSUQiLCJDQVBfU0VURkNBUCIsIkNBUF9TRVRQQ0FQIiwiQ0FQX05FVF9CSU5EX1NFUlZJQ0UiLCJDQVBfU1lTX0NIUk9PVCIsIkNBUF9LSUxMIiwiQ0FQX0FVRElUX1dSSVRFIl0sImluaGVyaXRhYmxlIjpbXSwicGVybWl0dGVkIjpbIkNBUF9DSE9XTiIsIkNBUF9EQUNfT1ZFUlJJREUiLCJDQVBfRlNFVElEIiwiQ0FQX0ZPV05FUiIsIkNBUF9NS05PRCIsIkNBUF9ORVRfUkFXIiwiQ0FQX1NFVEdJRCIsIkNBUF9TRVRVSUQiLCJDQVBfU0VURkNBUCIsIkNBUF9TRVRQQ0FQIiwiQ0FQX05FVF9CSU5EX1NFUlZJQ0UiLCJDQVBfU1lTX0NIUk9PVCIsIkNBUF9LSUxMIiwiQ0FQX0FVRElUX1dSSVRFIl19LCJjb21tYW5kIjpbIi9wYXVzZSJdLCJlbnZfcnVsZXMiOlt7InBhdHRlcm4iOiJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsInJlcXVpcmVkIjp0cnVlLCJzdHJhdGVneSI6InN0cmluZyJ9LHsicGF0dGVybiI6IlRFUk09eHRlcm0iLCJyZXF1aXJlZCI6ZmFsc2UsInN0cmF0ZWd5Ijoic3RyaW5nIn1dLCJleGVjX3Byb2Nlc3NlcyI6W10sImxheWVycyI6WyIxNmI1MTQwNTdhMDZhZDY2NWY5MmMwMjg2M2FjYTA3NGZkNTk3NmM3NTVkMjZiZmYxNjM2NTI5OTE2OWU4NDE1Il0sIm1vdW50cyI6W10sIm5hbWUiOiJwYXVzZS1jb250YWluZXIiLCJub19uZXdfcHJpdmlsZWdlcyI6ZmFsc2UsInNlY2NvbXBfcHJvZmlsZV9zaGEyNTYiOiIiLCJzaWduYWxzIjpbXSwidXNlciI6eyJncm91cF9pZG5hbWVzIjpbeyJwYXR0ZXJuIjoiIiwic3RyYXRlZ3kiOiJhbnkifV0sInVtYXNrIjoiMDAyMiIsInVzZXJfaWRuYW1lIjp7InBhdHRlcm4iOiIiLCJzdHJhdGVneSI6ImFueSJ9fSwid29ya2luZ19kaXIiOiIvIn1dCgphbGxvd19wcm9wZXJ0aWVzX2FjY2VzcyA6PSB0cnVlCmFsbG93X2R1bXBfc3RhY2tzIDo9IGZhbHNlCmFsbG93X3J1bnRpbWVfbG9nZ2luZyA6PSBmYWxzZQphbGxvd19lbnZpcm9ubWVudF92YXJpYWJsZV9kcm9wcGluZyA6PSB0cnVlCmFsbG93X3VuZW5jcnlwdGVkX3NjcmF0Y2ggOj0gZmFsc2UKYWxsb3dfY2FwYWJpbGl0eV9kcm9wcGluZyA6PSB0cnVlCgptb3VudF9kZXZpY2UgOj0gZGF0YS5mcmFtZXdvcmsubW91bnRfZGV2aWNlCnVubW91bnRfZGV2aWNlIDo9IGRhdGEuZnJhbWV3b3JrLnVubW91bnRfZGV2aWNlCm1vdW50X292ZXJsYXkgOj0gZGF0YS5mcmFtZXdvcmsubW91bnRfb3ZlcmxheQp1bm1vdW50X292ZXJsYXkgOj0gZGF0YS5mcmFtZXdvcmsudW5tb3VudF9vdmVybGF5CmNyZWF0ZV9jb250YWluZXIgOj0gZGF0YS5mcmFtZXdvcmsuY3JlYXRlX2NvbnRhaW5lcgpleGVjX2luX2NvbnRhaW5lciA6PSBkYXRhLmZyYW1ld29yay5leGVjX2luX2NvbnRhaW5lcgpleGVjX2V4dGVybmFsIDo9IGRhdGEuZnJhbWV3b3JrLmV4ZWNfZXh0ZXJuYWwKc2h1dGRvd25fY29udGFpbmVyIDo9IGRhdGEuZnJhbWV3b3JrLnNodXRkb3duX2NvbnRhaW5lcgpzaWduYWxfY29udGFpbmVyX3Byb2Nlc3MgOj0gZGF0YS5mcmFtZXdvcmsuc2lnbmFsX2NvbnRhaW5lcl9wcm9jZXNzCnBsYW45X21vdW50IDo9IGRhdGEuZnJhbWV3b3JrLnBsYW45X21vdW50CnBsYW45X3VubW91bnQgOj0gZGF0YS5mcmFtZXdvcmsucGxhbjlfdW5tb3VudApnZXRfcHJvcGVydGllcyA6PSBkYXRhLmZyYW1ld29yay5nZXRfcHJvcGVydGllcwpkdW1wX3N0YWNrcyA6PSBkYXRhLmZyYW1ld29yay5kdW1wX3N0YWNrcwpydW50aW1lX2xvZ2dpbmcgOj0gZGF0YS5mcmFtZXdvcmsucnVudGltZV9sb2dnaW5nCmxvYWRfZnJhZ21lbnQgOj0gZGF0YS5mcmFtZXdvcmsubG9hZF9mcmFnbWVudApzY3JhdGNoX21vdW50IDo9IGRhdGEuZnJhbWV3b3JrLnNjcmF0Y2hfbW91bnQKc2NyYXRjaF91bm1vdW50IDo9IGRhdGEuZnJhbWV3b3JrLnNjcmF0Y2hfdW5tb3VudApyd19tb3VudF9kZXZpY2UgOj0gZGF0YS5mcmFtZXdvcmsucndfbW91bnRfZGV2aWNlCgpyZWFzb24gOj0geyJlcnJvcnMiOiBkYXRhLmZyYW1ld29yay5lcnJvcnN9" + }, + "containers": [ + { + "name": "cc-attest", + "properties": { + "image": "[parameters('appImage')]", + "ports": [ + { + "protocol": "TCP", + "port": 80 + } + ], + "environmentVariables": [ + { + "name": "ACI_SKU", + "value": "Confidential" + }, + { + "name": "MAA_ENDPOINT", + "value": "sharedeus.eus.attest.azure.net" + } + ], + "resources": { + "requests": { + "memoryInGB": 2, + "cpu": 1 + } + } + } + } + ], + "imageRegistryCredentials": [ + { + "server": "[parameters('registryServer')]", + "username": "[parameters('registryUsername')]", + "password": "[parameters('registryPassword')]" + } + ], + "ipAddress": { + "ports": [ + { + "protocol": "TCP", + "port": 80 + } + ], + "type": "Public", + "dnsNameLabel": "[parameters('dnsNameLabel')]" + }, + "osType": "Linux", + "restartPolicy": "Always" + } + } + ], + "outputs": { + "fqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', parameters('containerGroupName'))).ipAddress.fqdn]" + } + } +} \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-standard.json b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-standard.json new file mode 100644 index 000000000..9c036f285 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/deployment-template-standard.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "containerGroupName": { "type": "string" }, + "location": { "type": "string", "defaultValue": "[resourceGroup().location]" }, + "appImage": { "type": "string" }, + "registryServer": { "type": "string" }, + "registryUsername": { "type": "string" }, + "registryPassword": { "type": "securestring" }, + "dnsNameLabel": { "type": "string" } + }, + "resources": [ + { + "type": "Microsoft.ContainerInstance/containerGroups", + "apiVersion": "2023-05-01", + "name": "[parameters('containerGroupName')]", + "location": "[parameters('location')]", + "properties": { + "sku": "Standard", + "containers": [ + { + "name": "cc-attest", + "properties": { + "image": "[parameters('appImage')]", + "ports": [ { "protocol": "TCP", "port": 80 } ], + "resources": { + "requests": { "memoryInGB": 2, "cpu": 1 } + } + } + } + ], + "imageRegistryCredentials": [ + { + "server": "[parameters('registryServer')]", + "username": "[parameters('registryUsername')]", + "password": "[parameters('registryPassword')]" + } + ], + "ipAddress": { + "ports": [ { "protocol": "TCP", "port": 80 } ], + "type": "Public", + "dnsNameLabel": "[parameters('dnsNameLabel')]" + }, + "osType": "Linux", + "restartPolicy": "Always" + } + } + ], + "outputs": { + "fqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', parameters('containerGroupName'))).ipAddress.fqdn]" + } + } +} diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-confidential.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-confidential.png new file mode 100644 index 000000000..115b6a344 Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-confidential.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-side-by-side.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-side-by-side.png new file mode 100644 index 000000000..4e8562a94 Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-side-by-side.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-standard.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-standard.png new file mode 100644 index 000000000..5feca13e3 Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/images/screenshot-standard.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/requirements.txt b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/requirements.txt new file mode 100644 index 000000000..3c017336c --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/requirements.txt @@ -0,0 +1,3 @@ +flask>=2.3 +requests + diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/side-by-side-compare.html b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/side-by-side-compare.html new file mode 100644 index 000000000..c1902153d --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/side-by-side-compare.html @@ -0,0 +1,137 @@ + + + + + + ACI Attestation Compare + + + +
+

ACI Runtime Attestation Compare

+
+ Left: Confidential SKU (expected attestation success) + Right: Standard SKU (expected attestation failure) +
+
+ +
+
+
+
Confidential
+
+ SEV-SNP expected + http://cc-attest-conf-07131653.eastus.azurecontainer.io +
+
+ +
+ +
+
+
Standard
+
+ Expected /dev/sev-guest missing + http://cc-attest-std-07131655vspm.eastus.azurecontainer.io +
+
+ +
+
+ + diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/templates/index.html b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/templates/index.html new file mode 100644 index 000000000..3946f42d6 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/resources/visual-attestation-demo-v2/templates/index.html @@ -0,0 +1,205 @@ + + + + + ACI CC - Runtime Attestation + + + + +
+ +

ACI {% if is_confidential %}Confidential{% else %}Standard{% endif %} Containers - Runtime Attestation

+ {% if is_confidential %} +

This container is running in an ACI Confidential Container group on AMD SEV-SNP hardware. Click Attest to fetch a fresh hardware-rooted attestation token from Microsoft Azure Attestation (MAA) and inspect the claims.

+ {% else %} +

This container is running in a Standard ACI container group - not on AMD SEV-SNP hardware and with no /dev/sev-guest device. Click Attest to see the expected hardware-rooted attestation failure.

+ {% endif %} +
+
+
+

The attestation flow runs inside this single container - no SKR sidecar - using the upstream get-snp-report tool baked into the image:

+
    +
  1. The app builds a runtime data blob ({nonce, client}) and computes REPORT_DATA = SHA-256(runtime_data) || 0×32.
  2. +
  3. get-snp-report opens /dev/sev-guest and issues SNP_GUEST_REQUEST; the AMD Secure Processor returns a 1184-byte report signed by this chip's VCEK and bound to the supplied REPORT_DATA.
  4. +
  5. The app reads the THIM cert chain and UVM endorsements from UVM_SECURITY_CONTEXT_DIR (injected by the ACI control plane) and POSTs {report, runtimeData, nonce} to https://<maa>/attest/SevSnpVm.
  6. +
  7. MAA verifies the SNP report against the AMD root CA chain and the UVM endorsements, then returns a signed JWT whose x-ms-sevsnpvm-* claims describe the TEE state at this moment.
  8. +
+

On ACI Standard SKU there is no SEV-SNP hardware and no /dev/sev-guest device, so step 2 fails before MAA is ever called - that is the educational contrast.

+

+ + +

+
+ +
+
+ + + + diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/solution-04.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/solution-04.md index 457a86966..7973a6a3d 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/solution-04.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04/solution-04.md @@ -1,636 +1,323 @@ -# Walkthrough Challenge 4 - Encryption in use with Azure Confidential Compute – VM +# Walkthrough Challenge 4 - Runtime attestation with Confidential ACI -[Previous Challenge Solution](../challenge-03/solution-03.md) - **[Home](../../Readme.md)** - [Next Challenge Solution](../challenge-05/solution-05.md) +[Previous Challenge Solution](../challenge-03/solution-03.md) - **[Home](../../Readme.md)** - [Challenge](../../challenges/challenge-04.md) - [Next Challenge Solution](../challenge-05/solution-05.md) -**Estimated Duration:** 90-120 minutes +**Estimated duration:** 30-45 minutes -> 💡 **Objective:** Learn how to implement and validate guest attestation on Azure Confidential VMs to ensure business logic only executes in trusted, hardware-backed confidential computing environments. You will deploy a Confidential VM, configure secure access through Azure Bastion, build and run attestation client applications, and verify cryptographic proof of VM integrity before processing sensitive workloads. +## Objective -## Prerequisites - -Please ensure that you successfully verified the [General prerequisites](../../Readme.md#general-prerequisites) before continuing with this challenge. - -- Azure subscription with Contributor permissions on your resource group -- Azure CLI >= 2.54 or access to Azure Portal -- **Linux/Bash environment** — Azure Cloud Shell (Bash), WSL2 on Windows, or a native Linux/macOS terminal -- Basic understanding of Azure Virtual Machines, networking, and SSH key authentication -- Basic understanding of confidential computing concepts - -## Scenario Context - -You are a security architect at a European financial services organization that processes highly sensitive customer data and must comply with strict data sovereignty and protection requirements. Your organization has determined that traditional encryption at rest and in transit is insufficient for protecting high-value workloads. - -Your mandate includes: - -- **Encryption in Use**: Data must be protected even while being processed, not just when stored or transmitted -- **Hardware-Based Trust**: Security guarantees must be rooted in hardware, not just software configurations -- **Attestation Requirements**: Workloads must verify they are running in a genuine confidential environment before processing sensitive data -- **Zero Trust Architecture**: Never assume trust; always verify the execution environment cryptographically -- **Regulatory Compliance**: Meet stringent requirements for data protection in financial services - -In this challenge, you'll implement Azure Confidential Computing using AMD SEV-SNP technology to create a hardware-based Trusted Execution Environment (TEE). You'll configure guest attestation to provide cryptographic proof that your workload is running in a protected environment before it processes any sensitive operations. - -### Understanding Guest Attestation - -Guest attestation helps you confirm that your confidential VM environment is secured by a genuine hardware-backed Trusted Execution Environment (TEE) with security features enabled for isolation and integrity. - -**You can use guest attestation to:** - -- **Verify hardware platform** - Confirm the confidential VM runs on expected AMD SEV-SNP hardware -- **Validate secure boot** - Verify secure boot is enabled, protecting firmware, bootloader, and kernel from malware -- **Provide cryptographic evidence** - Obtain JWT tokens proving the VM runs on confidential hardware -- **Prevent data exposure** - Ensure workloads refuse to start in untrusted environments - -### Attestation Workflow Pattern - -![Scenario diagram](./images/attestation-workflow.png) - -In this challenge, you will implement a common pattern where **attestation requests are made from inside the workload** at program startup. The workload verifies that it's running on the correct hardware platform before executing any sensitive business logic. - -**How it works:** - -1. Your workload starts on the Confidential VM -2. Before processing any sensitive data, the workload calls the attestation library -3. The attestation library contacts Microsoft Azure Attestation (MAA) to verify the TEE environment -4. The workload parses the attestation response (JWT token) to confirm: - - The VM runs on genuine AMD SEV-SNP confidential hardware - - Secure boot is enabled and validated - - The VM guest state is protected -5. Only after successful verification does the workload proceed with sensitive operations - -**Why this matters:** - -This pattern ensures your application never processes sensitive data in an untrusted environment. If attestation fails (e.g., the code is running on a standard VM or a compromised environment), the workload can refuse to start or handle data differently. - -### Learning Resources - -- [Azure Confidential Computing overview](https://learn.microsoft.com/azure/confidential-computing/overview) -- [Confidential VM concepts](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) -- [Guest attestation for confidential VMs](https://learn.microsoft.com/azure/confidential-computing/guest-attestation-confidential-vms) -- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/overview) - -### Original Source Materials - -This challenge is based on the **Confidential VM Guest Attestation Sample Application** from the Microsoft Azure Confidential Computing repository: - -- **Main Repository**: [Azure Confidential Computing CVM Guest Attestation](https://github.com/Azure/confidential-computing-cvm-guest-attestation) -- **Source Module**: [CVM Attestation Sample App](https://github.com/Azure/confidential-computing-cvm-guest-attestation/tree/main/cvm-attestation-sample-app) - -The sample application and deployment patterns have been adapted for this MicroHack challenge to provide a guided learning experience with Azure Confidential VMs. - ---- - -## Task 1: Understand the Deployment Architecture - -💡 **Before deploying resources, it's important to understand the security architecture and components you'll be creating.** - -### Resources Being Deployed - -The following resources will be created in the `North Europe` Azure region: - -- **1 Attestation Provider** - Microsoft Azure Attestation (MAA) service for verifying TEE integrity -- **1 Virtual Network** - Isolated network with VM and Bastion subnets -- **1 Confidential VM** - Ubuntu 22.04 with AMD SEV-SNP hardware encryption -- **1 Azure Bastion** - Secure remote access without public IP exposure -- **1 Azure Key Vault** - Secure storage for SSH keys -- **Associated Resources** - NSGs, NICs, OS Disks (auto-created) - -### Security Architecture - -This setup implements a zero-trust security model: - -- **No Public IPs on VMs** - VMs are not directly accessible from internet -- **Azure Bastion (Basic SKU)** - Provides secure RDP/SSH access through Azure Portal -- **Azure Key Vault** - Stores SSH private keys securely with RBAC controls -- **Fresh SSH Key Pair** - Generated specifically for this deployment, never stored locally -- **Hardware-Based Encryption** - AMD SEV-SNP encrypts VM memory at the hardware level -- **Secure Boot** - Protects boot chain integrity from firmware through kernel - -🔑 **Security Best Practice**: This architecture ensures VM access is authenticated through Azure AD, encrypted with TLS, and never exposes SSH directly to the internet. - ---- - -## Task 2: Set Up Your Environment and Deploy Infrastructure - -💡 **You'll create the foundational Azure resources including Resource Group, Key Vault, SSH keys, Attestation Provider, Virtual Network, Confidential VM, and Azure Bastion.** - -> [!IMPORTANT] -> **Prerequisite — Challenge 1 policy adjustment:** This challenge deploys a **public IP address** for Azure Bastion and creates resources that require tags. If you completed Challenge 1, make sure you ran the **"Preparing for Next Challenges"** section at the end of that walkthrough to switch the tag-requirement and public-IP-block policies to **DoNotEnforce** mode. Otherwise the deployments below will fail. +Deploy the newer Visual Attestation Demo v2 to both Confidential and Standard +ACI. The automation builds one image, generates its confidential-computing +enforcement policy, and deploys both variants for a falsifiable comparison. > [!IMPORTANT] -> The Azure CLI commands in this walkthrough use **bash** syntax and will not work directly in PowerShell. Use **Azure Cloud Shell (Bash)** for the best experience. If running locally on Windows, use **WSL2** (Windows Subsystem for Linux) to run a bash shell. You can install the Azure CLI inside WSL with: +> **Execution environment changes in this challenge.** +> Challenges 1-3 are completed in **Azure Cloud Shell (Bash)**. From Challenge 4 +> onward you must switch to a **local PowerShell 7+ session on your own machine** +> (Windows is the validated platform). > -> ```bash -> curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash -> ``` - ---- - -### Step 1: Configure Environment Variables - -Set up the variables that will be used throughout the deployment: - -```bash -# Set common variables -# Customize ATTENDEE_ID for each participant -RESOURCE_GROUP="labuser-xx" # Change this for each participant (e.g., labuser-01, labuser-02, ...) - -ATTENDEE_ID="${RESOURCE_GROUP}" -# Generate a short hash from ATTENDEE_ID with random component for uniqueness -HASH_SUFFIX=$(echo -n "${ATTENDEE_ID}-${RANDOM}-${RANDOM}" | md5sum | cut -c1-8) - -LOCATION="northeurope" -ADMIN_USERNAME="azureuser" -KEYVAULT_NAME="kv-cc-${HASH_SUFFIX}" # Must be globally unique -SSH_KEY_NAME="cc-${ATTENDEE_ID}-key" -ATTESTATION_NAME="attest${HASH_SUFFIX}" -``` - -🔑 **Best Practice**: Using hash-based suffixes ensures globally unique resource names even if multiple participants use similar attendee IDs. - -> [!WARNING] -> If your Azure Cloud Shell session times out (e.g. during a break), the variables defined above will be lost and must be re-defined before continuing. We recommend saving them in a local text file on your machine so you can quickly copy and paste them back into a new session. - -### Step 2: Create Key Vault - -Create a Key Vault with RBAC-based permissions: - -```bash -# Create Key Vault with Azure RBAC permission model -az keyvault create \ - --name $KEYVAULT_NAME \ - --resource-group $RESOURCE_GROUP \ - --location $LOCATION \ - --sku standard \ - --enable-rbac-authorization true \ - --enabled-for-deployment true \ - --enabled-for-template-deployment true - -# Get current user's object ID -CURRENT_USER_ID=$(az ad signed-in-user show --query id -o tsv) - -# Assign Key Vault Secrets Officer role to current user -az role assignment create \ - --role "Key Vault Secrets Officer" \ - --assignee $CURRENT_USER_ID \ - --scope $(az keyvault show --name $KEYVAULT_NAME --resource-group $RESOURCE_GROUP --query id -o tsv) - -# Wait for RBAC permissions to propagate -echo "Waiting for RBAC permissions to propagate..." -sleep 30 -``` - -🔑 **Best Practice**: Using Azure RBAC for Key Vault instead of access policies provides more granular control and better integration with Azure AD identity governance. - -### Step 3: Generate and Store SSH Keys - -Generate SSH key pair and store securely in Key Vault: - -```bash -# Generate SSH key pair using temporary file -SSH_TEMP_FILE="/tmp/cc_ssh_key_${ATTENDEE_ID}_${RANDOM}" -ssh-keygen -t rsa -b 4096 -f "$SSH_TEMP_FILE" -N "" -C "microhack-cc" - -# Store private key in Key Vault -az keyvault secret set \ - --vault-name $KEYVAULT_NAME \ - --name "ssh-private-key" \ - --file "$SSH_TEMP_FILE" - -# Store public key in Key Vault -az keyvault secret set \ - --vault-name $KEYVAULT_NAME \ - --name "ssh-public-key" \ - --file "${SSH_TEMP_FILE}.pub" - -# Read public key for VM creation -SSH_PUBLIC_KEY=$(cat "${SSH_TEMP_FILE}.pub") - -# Clean up temporary files -rm -f "$SSH_TEMP_FILE" "${SSH_TEMP_FILE}.pub" - -echo "SSH key pair generated and stored in Key Vault: $KEYVAULT_NAME" -echo "Public key: $SSH_PUBLIC_KEY" -``` - -🔑 **Security Insight**: SSH keys are never stored persistently on your local machine. They exist only in Azure Key Vault, reducing the risk of key compromise. - -### Step 4: Create Attestation Provider - -Create the Microsoft Azure Attestation provider: - -```bash -# Create Attestation Provider -az attestation create \ - --name $ATTESTATION_NAME \ - --resource-group $RESOURCE_GROUP \ - --location $LOCATION -``` - -💡 **Understanding MAA**: Microsoft Azure Attestation is a unified solution for remotely verifying the trustworthiness of platforms and integrity of binaries running inside them. - -### Step 5: Create Virtual Network with Subnets - -Create virtual network with separate subnets for VMs and Azure Bastion: - -```bash -# Create Virtual Network with VM and Bastion subnets for CVM -az network vnet create \ - --resource-group $RESOURCE_GROUP \ - --name "vm-ubuntu-cvm-vnet" \ - --location $LOCATION \ - --address-prefix "10.10.0.0/24" \ - --subnet-name "vm-subnet" \ - --subnet-prefix "10.10.0.0/26" - -# Create Bastion subnet (must be named AzureBastionSubnet) -az network vnet subnet create \ - --resource-group $RESOURCE_GROUP \ - --vnet-name "vm-ubuntu-cvm-vnet" \ - --name "AzureBastionSubnet" \ - --address-prefix "10.10.0.64/26" -``` - -🔑 **Networking Best Practice**: Separating VM and Bastion subnets provides network segmentation and allows for different security policies. - -### Step 6: Deploy Confidential VM - -Create the Confidential VM with AMD SEV-SNP hardware encryption: - -```bash -# Create Confidential VM - No public IP -az vm create \ - --resource-group $RESOURCE_GROUP \ - --name "vm-ubuntu-cvm" \ - --location $LOCATION \ - --size "Standard_DC2as_v6" \ - --admin-username $ADMIN_USERNAME \ - --ssh-key-value "$SSH_PUBLIC_KEY" \ - --authentication-type ssh \ - --enable-vtpm true \ - --image "Canonical:0001-com-ubuntu-confidential-vm-jammy:22_04-lts-cvm:latest" \ - --security-type "ConfidentialVM" \ - --os-disk-security-encryption-type "VMGuestStateOnly" \ - --enable-secure-boot true \ - --vnet-name "vm-ubuntu-cvm-vnet" \ - --subnet "vm-subnet" \ - --public-ip-address "" - -# Enable system-assigned managed identity for CVM -az vm identity assign \ - --resource-group $RESOURCE_GROUP \ - --name "vm-ubuntu-cvm" -``` - -💡 **Key Configuration Details**: - -- `--security-type "ConfidentialVM"` - Enables hardware-based confidential computing -- `--os-disk-security-encryption-type "VMGuestStateOnly"` - Encrypts VM guest state with platform-managed keys -- `--enable-secure-boot true` - Protects boot integrity -- `--enable-vtpm true` - Enables virtual Trusted Platform Module for attestation -- `--public-ip-address ""` - No public IP for enhanced security - -### Step 7: Deploy Azure Bastion - -Create Azure Bastion for secure remote access: - -```bash -# Create Public IP for Bastion -az network public-ip create \ - --resource-group $RESOURCE_GROUP \ - --name "bastion-ip" \ - --location $LOCATION \ - --sku "Standard" \ - --allocation-method "Static" - -# Create Azure Bastion (Basic SKU) - -echo "Bastion deployment initiated (this may take 5-10 minutes)" - -az network bastion create \ - --resource-group $RESOURCE_GROUP \ - --name "bastion" \ - --location $LOCATION \ - --vnet-name "vm-ubuntu-cvm-vnet" \ - --public-ip-address "bastion-ip" \ - --sku "Basic" -``` +> Azure Cloud Shell **cannot** complete this challenge: it does not provide a +> local Docker engine, and `az confcom acipolicygen` requires one to inspect the +> image layers when generating the CCE policy. See +> [why Challenge 4 requires `confcom` and Docker](CONFCOM-AND-CCE-POLICY.md). -⏱️ **Deployment Time**: Azure Bastion typically takes 5-10 minutes to deploy. You can proceed with reviewing the next sections while waiting. - ---- - -## Task 3: Connect to Confidential VM and Install Attestation Tools - -💡 **Now you'll connect to the Confidential VM through Azure Bastion and install the necessary dependencies to build and run the attestation client.** - -### Step 1: Connect via Azure Bastion - -> **📝 Note: These commands run ON the Linux VM itself after connecting via Bastion (not on your local machine or in Azure Cloud Shell)** +## Prerequisites -1. Navigate to the Azure Portal -2. Go to **Virtual Machines** > **vm-ubuntu-cvm** (select the one in your resource group) -3. Click **Connect** > **Connect via Bastion** -4. **Authentication Type**: SSH Private Key from Azure Key Vault -5. **Username**: `azureuser` -6. **Azure Key Vault Secret**: Select your Key Vault and choose `ssh-private-key` -7. Click **Connect** +- The [general MicroHack prerequisites](../../Readme.md#general-prerequisites). +- PowerShell 7 or later, running locally (not Cloud Shell). +- Azure CLI signed in to the target subscription. +- Docker Desktop installed **and running**. The `confcom` extension uses it to + calculate the CCE policy. +- Contributor access to the attendee resource group. +- Confidential ACI capacity in the selected region. This walkthrough uses + `northeurope`, which is validated for Confidential ACI. Confidential ACI is + available in fewer regions than standard ACI - if you change the region, + confirm support first in + [Confidential containers on ACI](https://learn.microsoft.com/azure/container-instances/container-instances-confidential-overview). -![Connect via Azure Bastion](./images/vm-connect-via-bastion.png) +### Verify your environment before you start -### Step 2: Install System Dependencies and Build Tools +Run this block and confirm every line succeeds: -Once connected to the VM via Bastion, run the following commands: +```powershell +# 1. PowerShell must be 7.0 or later +$PSVersionTable.PSVersion -```bash -# Install system dependencies -export DEBIAN_FRONTEND=noninteractive -export NEEDRESTART_MODE=a -export APT_LISTCHANGES_FRONTEND=none +# 2. Azure CLI present and signed in (run 'az login' if this fails) +az version +az account show --output table -sudo -E apt-get update -y -sudo -E apt-get upgrade -y +# 3. Docker Desktop must be running (this fails if the daemon is stopped) +docker info --format '{{.ServerVersion}}' -sudo -E apt-get install -y build-essential libcurl4-openssl-dev libjsoncpp-dev libboost-all-dev nlohmann-json3-dev cmake wget git jq +# 4. Register the resource providers used by this challenge (safe to re-run) +az provider register --namespace Microsoft.ContainerInstance +az provider register --namespace Microsoft.ContainerRegistry ``` -💡 **What's Being Installed**: -- Build tools (gcc, g++, make) for compiling C++ attestation client -- Cryptographic libraries (libcurl, jsoncpp) for HTTPS and JSON handling -- Boost libraries for advanced C++ functionality -- CMake for build orchestration -- jq for JSON parsing and token inspection +> [!NOTE] +> Provider registration is asynchronous and can take a few minutes on a new +> subscription. Check progress with: +> `az provider show --namespace Microsoft.ContainerInstance --query registrationState -o tsv` -### Step 3: Clone Attestation Repository +> [!IMPORTANT] +> The automation creates resources in the existing attendee resource group. It +> does not create or delete the resource group. -```bash -# Clone the attestation repository -git clone https://github.com/Azure/confidential-computing-cvm-guest-attestation.git -``` +## Task 0: Get the challenge files -### Step 4: Install Azure Guest Attestation Package +The deployment script is **not standalone**. It resolves the application source, +`Dockerfile`, and ARM templates from `resources/visual-attestation-demo-v2` +relative to its own location, so it must be run from inside a full clone of the +repository. -```bash -# Download the attestation package -wget https://packages.microsoft.com/repos/azurecore/pool/main/a/azguestattestation1/azguestattestation1_1.1.2_amd64.deb +```powershell +git clone https://github.com/yelamanchili-murali/MicroHack.git +cd MicroHack/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-04 -# Install the attestation package -sudo dpkg -i azguestattestation1_1.1.2_amd64.deb +# Confirm both the script and its resources directory are present +Get-ChildItem ``` -💡 **About the Package**: The `azguestattestation1` package provides the attestation client library that interfaces with the VM's vTPM and Microsoft Azure Attestation service. - ---- - -## Task 4: Build and Execute Attestation Client - -💡 **You'll compile the attestation client application and execute it to obtain cryptographic proof of the VM's confidential computing capabilities.** +You should see `Deploy-VisualAttestationV2.ps1` and a `resources` directory. +Running the script from any other location (for example your home directory) +fails with `The term './Deploy-VisualAttestationV2.ps1' is not recognized...`. -### Step 1: Build the Attestation Client +If PowerShell blocks the script with an `UnauthorizedAccess` or "running scripts +is disabled on this system" error, allow it for the current session only: -Install prerequisites by running these commands: +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -```bash -sudo apt install cmake - -sudo apt-get update -y && sudo apt-get install -y build-essential - -sudo apt-get install -y libcurl4-openssl-dev libjsoncpp-dev libboost-all-dev nlohmann-json3-dev jq +# If you downloaded the file instead of cloning, also clear the internet mark: +Unblock-File ./Deploy-VisualAttestationV2.ps1 ``` -### Generate build files - -```bash -# Navigate to the sample app directory -cd confidential-computing-cvm-guest-attestation/cvm-attestation-sample-app/ +## Task 1: Configure the MicroHack environment -# Generate build files with CMake -cmake . +> [!WARNING] +> **The variable syntax differs from Challenges 1-3.** Those challenges use Bash +> variables (`RESOURCE_GROUP="labuser-xx"`). PowerShell requires the `$env:` +> prefix shown below. Copying the Bash form into PowerShell fails silently and +> the script will not see your values. + +Use the same variable names as the other challenges. If these variables are +already present in your PowerShell session, do not generate a new suffix. + +```powershell +$env:RESOURCE_GROUP = "labuser-xx" +$env:ATTENDEE_ID = $env:RESOURCE_GROUP +$env:LOCATION = "northeurope" + +$seed = "$($env:ATTENDEE_ID)-$(Get-Random)-$(Get-Random)" +$bytes = [Text.Encoding]::UTF8.GetBytes($seed) +$hash = [Security.Cryptography.MD5]::HashData($bytes) +$env:HASH_SUFFIX = [Convert]::ToHexString($hash).Substring(0, 8).ToLowerInvariant() ``` -### Compile the application +Confirm the target resource group and subscription: -```bash -make +```powershell +az account show --query "{subscription:name, id:id}" --output table +az group show --name $env:RESOURCE_GROUP --query "{name:name, location:location}" --output table ``` -💡 **Build Process**: CMake analyzes dependencies and generates Makefiles, then `make` compiles the C++ source code into the `AttestationClient` executable. - -### Step 2: Run Attestation and Inspect JWT Token +## Task 2: Build and deploy the comparison -To use the dedicated Attestation Provider you created in Task 2 Step 4, you need to pass its URI using the `-a` argument. +> [!IMPORTANT] +> **If you have run Challenge 4 before with the same environment values - +> including a run that failed part-way - run `./Deploy-VisualAttestationV2.ps1 -Cleanup` +> first.** The script uses a clean build/deploy/cleanup lifecycle and does not +> update an existing run. -#### Retrieve Your Attestation URI via Azure CLI from your Azure CLI session in Cloud Shell or your local machine +From this walkthrough directory, run: -```bash -# Retrieve your attestation provider URI -ATTESTATION_URI=$(az attestation show \ - --name $ATTESTATION_NAME \ - --resource-group $RESOURCE_GROUP \ - --query "attestUri" \ - -o tsv) -echo $ATTESTATION_URI +```powershell +./Deploy-VisualAttestationV2.ps1 -Build +./Deploy-VisualAttestationV2.ps1 -Compare ``` -Alternatively, you may also retrieve the URL from the Azure portal (navigate to your resource group and click on the Attestation provider resource): - -![Attestation Client - Specified Provider](./images/attestation-provider.png) - -#### Run attestation with your custom provider (inside the Linux VM) - -```bash -ATTESTATION_URI= -# Run attestation with your custom provider -sudo ./AttestationClient -a $ATTESTATION_URI -o token | jq -R 'split(".") | .[0],.[1] | @base64d | fromjson' +`-Build` creates the registry and image. `-Compare` is the deployment step for +this walkthrough: it deploys both the Confidential and Standard container +groups. You do not need to run `-Deploy` before `-Compare`. + +The build phase confirms your environment variables, the target subscription and +resource group, then creates the ACR and starts the server-side image build: + +![PowerShell output of the build phase: environment variables set, subscription and resource group confirmed, Basic ACR created, and az acr build queued for server-side image build](./images/01-build-phase-start.png) + +The build finishes with the pushed image digest and tag, and the script prints +the next available commands: + +![PowerShell output showing the completed ACR run with provisioningState Succeeded, the pushed image digest for cc-attest:1.0, and the Build complete summary](./images/02-build-complete.png) + +> [!NOTE] +> **Expect several minutes of quiet output.** `az acr build` runs the image build +> server-side, and `az confcom acipolicygen` hashes every image layer locally. +> Both steps can each take a few minutes with little or no progress on screen. +> This is normal - do not interrupt the script. + +`-Compare` verifies the prerequisites, generates the CCE policy, deploys both +container groups, waits for each to respond, and prints a deployment summary +with both URLs: + +![PowerShell output of the compare phase: Docker and confcom prerequisite check passed, CCE policy generated, Confidential and Standard containers deployed, both responding, and a summary listing both endpoint URLs](./images/03-compare-deployment-summary.png) + +The script performs the time-consuming setup: + +1. Creates a Basic ACR in the existing resource group. +2. Runs `az acr build`; a local Docker build is not required. +3. Installs the `confcom` Azure CLI extension when needed. +4. Generates a CCE policy for the confidential deployment. +5. Deploys Confidential and Standard ACI from the same image. + +### What the automation is doing + +The container image is the packaged application. Building it in Azure +Container Registry (ACR) produces a specific set of read-only image layers. +Those layers, together with the container command, environment variables, +mounts, and other runtime settings, describe exactly what ACI is expected to +run. + +`confcom` is the Azure CLI extension for confidential container tooling. The +script runs `az confcom acipolicygen` to turn that expected container +configuration into a **confidential-computing enforcement (CCE) policy**. You +can think of this policy as an allow-list for the confidential container group: + +- which image layers may be loaded; +- which command may start; +- which environment variables and mounts are allowed; +- whether elevated privileges, debugging, and standard input/output are allowed. + +The policy helps prevent an operator or deployment change from silently +starting different code or changing the approved runtime configuration. If the +requested container does not match the policy, the confidential container +environment refuses to start it. + +The generated policy is written in **Rego**, the policy language used by +[Open Policy Agent](https://www.openpolicyagent.org/docs/policy-language). A +Rego document expresses rules that a system can evaluate as allow or deny +decisions. In this case, the confidential container runtime evaluates those +rules before allowing container operations. Among other details, the policy +records: + +- the cryptographic hashes of the permitted image layers; +- the container name, startup command, and working directory; +- allowed environment-variable patterns; +- permitted mounts, Linux capabilities, and executable processes; +- security choices such as standard I/O, elevated access, and runtime logging. + +The policy is generated from the **actual image content that `confcom` +inspects**, not only from an image name such as `cc-attest:1.0`. The part after +the colon is a tag. A tag is a convenient, human-readable label, but a registry +owner can move it so that it identifies different content later. + +When a container tool looks up an image tag, it obtains an image manifest and +the cryptographic digest and layer hashes for the content currently associated +with that tag. This lookup is sometimes called **resolving the image**: it maps +the friendly image name and tag to concrete, hash-identified content. +`confcom` records those immutable layer hashes in the policy. If someone later +points the same tag at different content, the new layer hashes will not match +the policy and the confidential environment will refuse to run it. The policy +therefore approves specific container content rather than trusting a mutable +name alone. + +The script does not maintain a separate hand-written policy file in the +repository. It first copies the confidential ARM template into a local working +directory so the source template remains unchanged. `confcom` then generates +the policy document, Base64-encodes it, and writes it into the template's +`ccePolicy` property. + +Only that local working copy is temporary. The policy itself is submitted to +Azure as part of the container-group deployment, stored with the deployed +resource, and enforced whenever its containers start. A hash of the policy is +also bound into the confidential environment's attestation evidence. This lets +a relying party check not only that SEV-SNP hardware was used, but also which +execution policy governed the workload. + +Docker Desktop is used only during this policy-generation step. `confcom` +inspects and hashes the Linux image layers through the local Docker engine. The +actual image build still runs remotely through `az acr build`. + +After deployment, the application asks the AMD SEV-SNP hardware for a signed +attestation report and sends that evidence to Microsoft Azure Attestation +(MAA). The report binds the running confidential environment to measurements +that include the enforcement policy. MAA verifies the evidence and returns the +signed token displayed by the application. + +The Standard ACI deployment uses the exact same application image but does not +have SEV-SNP hardware or `/dev/sev-guest`. Its expected attestation failure is a +control experiment: it demonstrates that the successful token came from the +confidential hardware and not merely from application code returning a +predefined response. + +The script waits for both applications to respond, prints a deployment summary, +and opens a generated `side-by-side-compare.html` page with both live endpoints. +To suppress browser launch, add `-SkipBrowser` to the `-Compare` command. + +The script generates the policy from a temporary copy of the ARM template. It +clears the copy's existing policy first and approves generated +environment-variable wildcard rules, so `confcom` does not pause for workshop +input. The repository's source template remains unchanged. + +## Task 3: Compare runtime attestation + +1. Open the **Confidential** URL and select **Attest**. +2. Confirm `x-ms-attestation-type` is `sevsnpvm`. +3. Confirm `x-ms-compliance-status` is `azure-compliant-uvm`. +4. Review the chip ID, launch measurement, policy hash, and TCB claims. +5. Open the **Standard** URL and select **Attest**. +6. Confirm attestation fails because `/dev/sev-guest` is absent. + +The generated `side-by-side-compare.html` page places both endpoints next to +each other. The Confidential pane returns a signed MAA token, while the Standard +pane fails because the SEV-SNP device is absent: + +![Side-by-side browser comparison. Left pane, Confidential SKU, shows an attestation result with TEE sevsnpvm and verdict azure-compliant-uvm. Right pane, Standard SKU, shows Attestation failed because neither /dev/sev-guest nor /dev/sev is present](./images/04-side-by-side-attestation.png) + +This negative control matters: identical application code cannot produce the +hardware report on a non-confidential host. + +## Troubleshooting + +| Message | Cause | Fix | +| --- | --- | --- | +| `The term './Deploy-VisualAttestationV2.ps1' is not recognized...` | Not in the challenge directory, or the repository was not cloned. | Complete Task 0 and `cd` into `walkthrough/challenge-04`. | +| `running scripts is disabled on this system` | PowerShell execution policy. | `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` | +| `Not logged in. Run: az login` | Azure CLI has no active session. | `az login`, then confirm with `az account show`. | +| `Docker is not running. Required by 'az confcom acipolicygen'...` | Docker Desktop is not started. | Start Docker Desktop, wait for it to report *Running*, verify with `docker info`. | +| `az confcom acipolicygen failed` | Usually Docker not ready, or the ACR login used for layer inspection expired. | Confirm `docker info` works, then run `-Cleanup` and retry from `-Build`. | +| Container group name already exists / conflicting resources | A previous Challenge 4 run was not cleaned up. | `./Deploy-VisualAttestationV2.ps1 -Cleanup`, then retry. | +| Confidential SKU capacity or SKU-not-available errors | The region has no Confidential ACI capacity. | Retry later, or select a region that supports Confidential ACI. | + +## Optional deployment modes + +For troubleshooting or a single-SKU demonstration, deploy only one variant +after the image is built. These commands are alternatives to `-Compare`, not +prerequisites for it: + +```powershell +./Deploy-VisualAttestationV2.ps1 -Deploy # Confidential +./Deploy-VisualAttestationV2.ps1 -Deploy -NoAcc # Standard ``` -![Attestation Client - Specified Provider](./images/attestation-specified-provider.png) - -🔑 **Why Use a Custom Attestation Provider?**: -- **Custom Policies**: Define organization-specific attestation policies -- **Audit Control**: Maintain your own attestation logs and policies -- **Compliance**: Meet regulatory requirements for attestation service ownership -- **Isolation**: Separate attestation infrastructure from shared services +## Task 4: Clean up -### Step 3: Understand the Attestation Token - -The JWT token contains multiple claims that cryptographically prove the VM's security posture: - -**Key Claims to Review**: - -- `x-ms-isolation-tee.x-ms-attestation-type` - Should show `sevsnpvm` (AMD SEV-SNP) -- `x-ms-isolation-tee.x-ms-compliance-status` - Should show `azure-compliant-cvm` -- `x-ms-isolation-tee.x-ms-sevsnpvm-is-debuggable` - Should be `false` (debugging disabled for security) -- `x-ms-policy-hash` - Hash of the attestation policy used -- `secureboot` - Should be `true` -- `x-ms-ver` - Attestation service version - -🔑 **Security Insight**: This JWT token is signed by Microsoft Azure Attestation. Any relying party can verify the signature to confirm the claims are authentic and the VM is running in a genuine confidential environment. - ---- - -## Task 5: Implement Attestation-Based Workload Protection (Conceptual) - -💡 **Understanding how to use attestation in production applications to enforce "only execute in confidential environment" policies.** - -### Production Implementation Pattern - -In a production scenario, your application would implement attestation checks before processing sensitive data (this is just an example for inspiration, do not run this on the Linux server): - -```python -# Pseudocode - Production attestation pattern -def process_sensitive_data(customer_data): - # Step 1: Perform attestation - attestation_token = get_attestation_token() - - # Step 2: Validate token with relying party - if not validate_attestation(attestation_token): - log_error("Attestation failed - not running in confidential environment") - raise SecurityException("Execution environment not trusted") - - # Step 3: Verify specific claims - claims = parse_jwt_claims(attestation_token) - if claims['x-ms-isolation-tee.x-ms-attestation-type'] != 'sevsnpvm': - raise SecurityException("Not running on AMD SEV-SNP hardware") - - if claims['x-ms-isolation-tee.x-ms-sevsnpvm-is-debuggable'] == 'true': - raise SecurityException("VM debugging is enabled - security risk") - - # Step 4: Only now proceed with sensitive operations - encrypted_results = process_pii_data(customer_data) - return encrypted_results +```powershell +./Deploy-VisualAttestationV2.ps1 -Cleanup ``` -### Use Cases for Attestation-Based Protection - -1. **Financial Services**: Verify trading algorithms run only in hardware-protected environments -2. **Healthcare**: Ensure patient data processing occurs only in attested confidential VMs -3. **Government**: Validate classified workloads execute in compliant infrastructure -4. **Multi-Party Computation**: Prove to partners that shared data is processed securely - -🔑 **Best Practice**: Attestation should be performed at application startup and periodically during long-running workloads to detect runtime tampering. - ---- - -## Key Takeaways - -In this challenge, you successfully implemented and validated Azure Confidential Computing with guest attestation. Here are the key concepts and best practices: - -### Confidential Computing Fundamentals - -✅ **Hardware-Based Trust** - AMD SEV-SNP provides hardware-level memory encryption that protects data in use, not just at rest or in transit - -✅ **Trusted Execution Environments (TEEs)** - Confidential VMs create isolated environments where even the cloud operator cannot access your data - -✅ **Virtual TPM (vTPM)** - Enables cryptographic attestation and secure boot validation - -### Attestation and Verification - -✅ **Guest Attestation** - Cryptographically proves that workloads run in genuine confidential computing environments before processing sensitive data - -✅ **Microsoft Azure Attestation (MAA)** - Provides centralized attestation services that validate TEE integrity and issue signed JWT tokens - -✅ **Zero Trust Validation** - Applications should verify execution environment before processing sensitive operations - -### Security Architecture - -✅ **Defense in Depth** - Combining multiple security layers (no public IPs, Azure Bastion, Key Vault, hardware encryption, attestation) - -✅ **Secure Key Management** - SSH keys stored exclusively in Azure Key Vault with RBAC controls, never persisted locally - -✅ **Network Isolation** - VMs without public IPs, accessed only through Azure Bastion with Azure AD authentication - -### Production Best Practices - -✅ **Attestation at Startup** - Perform attestation checks before processing any sensitive data - -✅ **Periodic Re-attestation** - Long-running workloads should re-attest periodically to detect runtime tampering - -✅ **JWT Token Validation** - Verify attestation tokens are signed by trusted authorities and contain expected claims - -✅ **Fail Securely** - Applications should refuse to start or handle data differently if attestation fails - -### Compliance and Governance - -✅ **Data Sovereignty** - Confidential computing ensures data remains encrypted even from cloud operators - -✅ **Regulatory Requirements** - Meets stringent requirements for financial services, healthcare, and government workloads - -✅ **Audit Trail** - Attestation tokens provide cryptographic proof for compliance auditing - ---- - -## Next Steps - -### Explore Advanced Confidential Computing Scenarios - -- **[Azure Confidential Containers](https://learn.microsoft.com/azure/confidential-computing/confidential-containers)** - Deploy containerized workloads with hardware-based confidential computing -- **[Confidential VMs with Customer-Managed Keys](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview#encryption-at-host-with-customer-managed-keys)** - Use your own encryption keys for additional control -- **[Confidential Computing on Azure Kubernetes Service](https://learn.microsoft.com/azure/aks/confidential-computing-azure)** - Orchestrate confidential containers at scale - -### Implement Application-Level Confidential Computing - -- **[Enclave Applications](https://learn.microsoft.com/azure/confidential-computing/application-development)** - Build applications that use Intel SGX or AMD SEV-SNP enclaves -- **[Confidential Inferencing](https://learn.microsoft.com/azure/machine-learning/how-to-machine-learning-confidential-containers)** - Protect ML models and data during inference -- **[Always Encrypted](https://learn.microsoft.com/sql/relational-databases/security/encryption/always-encrypted-database-engine)** - Combine confidential computing with SQL Server encryption - -### Learn More About Azure Security - -- **[Azure Security Benchmark for Confidential Computing](https://learn.microsoft.com/security/benchmark/azure/baselines/confidential-computing-security-baseline)** - Follow Microsoft's security recommendations -- **[Azure Confidential Ledger](https://learn.microsoft.com/azure/confidential-ledger/overview)** - Tamper-proof, immutable ledger for audit logs -- **[Microsoft Azure Attestation Documentation](https://learn.microsoft.com/azure/attestation/overview)** - Deep dive into attestation concepts and policies - -### Practice Confidential Computing Patterns - -- **Multi-Party Computation** - Build scenarios where multiple organizations jointly process data without exposing it to each other -- **Confidential AI** - Implement ML training and inference in confidential environments -- **Secure Enclaves** - Explore Intel SGX for process-level isolation within VMs - ---- - -## Additional Resources - -- [Azure Confidential Computing overview](https://learn.microsoft.com/azure/confidential-computing/overview) -- [Confidential VM concepts](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) -- [Guest attestation for confidential VMs](https://learn.microsoft.com/azure/confidential-computing/guest-attestation-confidential-vms) -- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/overview) -- [AMD SEV-SNP Technology](https://www.amd.com/en/developer/sev.html) -- [Azure Bastion Documentation](https://learn.microsoft.com/azure/bastion/bastion-overview) -- [Azure Key Vault Best Practices](https://learn.microsoft.com/azure/key-vault/general/best-practices) - ---- - -## Technical Notes - -1. **Attendee Identification**: - - Set `ATTENDEE_ID` to a unique value for each participant (e.g., attendee01, attendee02, etc.) - - A hash-based suffix is automatically generated from ATTENDEE_ID plus random numbers for globally unique resources - - The hash output is alphanumeric (hexadecimal) and safe for all Azure resource naming requirements - - This prevents resource name collisions even if multiple students use the same ATTENDEE_ID - -2. **SSH Key Generation**: - - Fresh SSH key pair is generated and stored directly in Key Vault - - No local SSH key files are created or retained - - Keys are only accessible through Key Vault - -3. **Security Compliance**: - - VMs have **no public IPs** - not directly accessible from internet - - Access only through Azure Bastion (Basic SKU) - - Private SSH key stored securely in Azure Key Vault - - Bastion uses Key Vault integration for authentication - -4. **Network Range**: - - VNet (North Europe): `10.10.0.0/24` - - VM Subnet: `10.10.0.0/26` - - Bastion Subnet: `10.10.0.64/26` - -5. **VM Pricing**: VMs use standard (pay-as-you-go) pricing for microhack reliability - -6. **Bastion Deployment**: Takes 5-10 minutes to complete. You can proceed with other steps while it deploys +The command removes the two named container groups, challenge ACR, generated +state, and the tagged local Docker image. The attendee resource group and +resources from other challenges are retained. Run cleanup before rebuilding +Challenge 4 with the same `HASH_SUFFIX`. -7. **Auto-Created Resources**: - - NSGs (Network Security Groups) - Created automatically by `az vm create` - - NICs (Network Interfaces) - Created automatically and named `VMNic` - - OS Disks - Created automatically with the VMs +## Source alignment -8. **Key Vault Access**: Ensure your Azure account has appropriate permissions to read secrets from Key Vault when using Bastion +The complete Visual Attestation Demo v2 source is retained as an unchanged +local snapshot under +[`resources/visual-attestation-demo-v2`](resources/visual-attestation-demo-v2/README.md). +The top-level deployment script is derived from that source with only the +MicroHack-specific changes listed in [UPSTREAM-SOURCE.md](UPSTREAM-SOURCE.md). +Use the top-level script for this walkthrough; the script inside `resources` +is retained only as the unchanged upstream reference. diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/CVM-ORCHESTRATOR.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/CVM-ORCHESTRATOR.md new file mode 100644 index 000000000..fb163a8cc --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/CVM-ORCHESTRATOR.md @@ -0,0 +1,157 @@ +# Confidential VM Orchestrator: when it is required + +This supplement explains the Microsoft-owned **Confidential VM Orchestrator** +enterprise application and, equally importantly, when it is not involved. + +## The short answer + +- Challenge 5 does not use Confidential VM Orchestrator. It creates an AKS node + pool on Confidential VM sizes and uses the platform-managed AKS provisioning + path. +- Challenge 5.5 uses Confidential VM Orchestrator because it creates a standalone + Confidential VM with confidential OS disk encryption backed by a + customer-managed key (CMK). +- Microsoft documents tenant opt-in of this service principal for confidential + disk encryption with a CMK. +- The application ID is Microsoft-owned: + `bf7b6499-ff71-4aa2-97a4-f372087be7f0`. +- Creating the service principal only makes that Microsoft application known in + the tenant. Access to a key is granted separately and should be least + privilege. + +## For a business reader + +### The problem being solved + +A customer-managed key gives an organization control over whether an encrypted +OS disk can be unlocked. That control is valuable only if Azure can create and +start the Confidential VM without turning the key into a broadly available +platform secret. + +The provisioning process therefore needs a recognizable, narrowly authorized +identity to participate in the confidential disk key-release workflow. The +Confidential VM Orchestrator enterprise application is that Microsoft-owned +service identity in the customer's tenant. + +A useful analogy is a named, audited courier. The organization keeps the key in +its vault. It explicitly recognizes the courier and gives it only the actions +needed for the delivery. The courier does not own the vault and does not receive +permission merely because it exists. + +### Business value + +This design provides: + +- explicit tenant consent before the Microsoft service identity can be used; +- a named identity for access reviews and audit records; +- separation between the human deploying the VM and the service completing the + confidential provisioning operation; +- least-privilege access to retrieve and release protected key material; +- automation without distributing a reusable user credential or client secret. + +### What happens without it + +If the tenant has not registered the service principal, Azure cannot assign the +required Key Vault permission to that identity. The CMK-backed confidential OS +disk provisioning path cannot complete. + +Granting broad access to users or applications as a workaround would weaken the +reason for using a customer-managed key. The correct remediation is the one-time +registration by an authorized Entra administrator, followed by narrowly scoped +Key Vault authorization. + +## For a technical reader + +### Enterprise application versus application registration + +The application is published and owned by Microsoft. The tenant-local service +principal, shown in Entra as an enterprise application, is the local identity +object for that application. + +Running `New-MgServicePrincipal` with the fixed Microsoft application ID does +not create new application code or transfer ownership to the attendee. It opts +the tenant into recognizing the Microsoft application. This is normally a +one-time tenant operation and requires privileged Microsoft Graph permission. + +Registration alone grants no Key Vault data access. Challenge 5.5 separately +adds a vault access policy granting the service principal `get` and `release`. + +### Why Challenge 5.5 needs it + +Challenge 5.5 selects all of these options together: + +- standalone AMD SEV-SNP Confidential VM; +- confidential OS disk encryption; +- `ConfidentialVmEncryptedWithCustomerKey` disk encryption set; +- HSM-backed customer-managed key with an attestation release policy. + +Azure's confidential provisioning path must obtain the protected disk key under +the release conditions so the confidential OS disk can be prepared and made +available to the attested VM. Microsoft documents Confidential VM Orchestrator +registration and Key Vault `get`/`release` permissions for this CMK scenario. + +The high-level flow is: + +1. The tenant recognizes the Microsoft-owned Orchestrator service principal. +2. Key Vault holds the HSM-backed disk key and its release policy. +3. The Orchestrator has only the Key Vault operations required by the + confidential disk provisioning workflow. +4. The disk encryption set references the versioned customer-managed key. +5. Azure validates the confidential platform state and performs secure key + release as part of VM creation. +6. The confidential OS disk is bound to the VM's protected state and vTPM. + +The service principal is retained during Challenge 5.5 cleanup because it is a +shared tenant-level identity, not a resource owned by one attendee deployment. +Per-deployment Key Vault access disappears when that lab vault is removed. + +### Why Challenge 5 does not need it + +Challenge 5 adds an AKS node pool using a Confidential VM SKU. AKS and Azure +Compute manage that node-pool provisioning path. The challenge does not create a +customer-managed confidential disk encryption set or grant a disk key to +Confidential VM Orchestrator. + +A successful AKS confidential node pool therefore does not prove that the +tenant-local Orchestrator service principal exists. Conversely, registering the +Orchestrator is not required to attest a pod running on those nodes. + +### Do not confuse the identities + +Challenge 5.5 uses several identities for different trust boundaries: + +| Identity | Purpose in Challenge 5.5 | +|----------|--------------------------| +| Confidential VM Orchestrator | Microsoft service identity for the CMK-backed confidential OS disk provisioning path; receives `get` and `release`. | +| Disk Encryption Set managed identity | Connects the disk encryption set to its Key Vault key; receives key operations used by the disk encryption integration. | +| VM user-assigned managed identity | Workload identity used by the running VM to request the separate application Secure Key Release key. | +| Human attendee | Creates resources through Azure Resource Manager; does not become the runtime workload identity. | + +These identities are deliberately separate. Passing hardware attestation does +not grant application authorization, and possessing a workload identity does +not prove that code is running inside a healthy Confidential VM. + +### Is it required for every Confidential VM? + +No. The requirement discussed here is tied to confidential OS disk encryption +using a customer-managed key. A Confidential VM can use other supported disk +protection choices, including platform-managed keys, or can be created without +confidential OS disk encryption where supported. Those choices have different +provisioning and key-management requirements. + +## Operational guidance + +- Treat service-principal registration as a one-time tenant prerequisite. +- Verify the fixed Microsoft application ID before granting access. +- Grant only the documented key operations and scope them as narrowly as the + chosen Key Vault authorization model permits. +- Do not delete the enterprise application during attendee cleanup; another + deployment in the tenant might depend on it. +- Do not use attendee credentials or a custom client secret as a substitute. + +## Official references + +- [Create a Confidential VM with a CMK using Azure CLI](https://learn.microsoft.com/azure/confidential-computing/quick-create-confidential-vm-azure-cli#create-confidential-virtual-machine-using-a-customer-managed-key) +- [Deploy a Confidential VM with a CMK using an ARM template](https://learn.microsoft.com/azure/confidential-computing/quick-create-confidential-vm-arm#deploy-confidential-vm-template-with-os-disk-confidential-encryption-via-customer-managed-key) +- [Create a Confidential VM in the Azure portal](https://learn.microsoft.com/azure/confidential-computing/quick-create-confidential-vm-portal) +- [Confidential VM and confidential OS disk encryption overview](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview#confidential-os-disk-encryption) diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/Deploy-VotingAppCC.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/Deploy-VotingAppCC.ps1 new file mode 100644 index 000000000..8f301687b --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/Deploy-VotingAppCC.ps1 @@ -0,0 +1,608 @@ +<# +.SYNOPSIS + Deploys the upstream Azure Voting App sample to confidential AKS nodes. + +.DESCRIPTION + Adapts the upstream Azure Voting App confidential-computing sample to the + MicroHack environment variables and existing attendee resource group. + +.EXAMPLE + ./Deploy-VotingAppCC.ps1 -Deploy + ./Deploy-VotingAppCC.ps1 -Cleanup +#> +# Hands-off script to build a small AKS cluster with an AMD SEV-SNP Confidential Computing node pool +# and deploy the public Microsoft "Azure Voting App" multi-container sample to it, exposed via a public +# LoadBalancer. Derived from the upstream sample with MicroHack naming and resource-group lifecycle. +# +# Simon Gallagher, ACC Product Group +# Use at your own risk, no warranties implied, test in a non-production environment first +# +# References: +# - https://learn.microsoft.com/azure/aks/use-cvm (CVM node pools on AKS) +# - https://learn.microsoft.com/azure/aks/confidential-computing-overview +# - https://learn.microsoft.com/azure/aks/auto-upgrade-cluster (auto-upgrade channel) +# - https://learn.microsoft.com/azure/aks/auto-upgrade-node-image (node OS auto-upgrade) +# - https://github.com/Azure-Samples/azure-voting-app-redis (the demo app) +# +# Usage: +# ./Deploy-VotingAppCC.ps1 -Deploy [-SkipSkuPreflight] +# ./Deploy-VotingAppCC.ps1 -Cleanup +# +# Requirements: +# - Azure PowerShell (Az) module + Azure CLI (az), both logged in to the same subscription +# - kubectl on PATH (az aks install-cli will install it if missing) + +[CmdletBinding(DefaultParameterSetName='Help')] +param ( + [Parameter(ParameterSetName='Deploy')][switch]$Deploy, + [Parameter(ParameterSetName='Cleanup')][switch]$Cleanup, + [Parameter(ParameterSetName='Deploy')][switch]$SkipSkuPreflight, + [string]$ResourceGroup = $env:RESOURCE_GROUP, + [string]$AttendeeId = $env:ATTENDEE_ID, + [string]$HashSuffix = $env:HASH_SUFFIX, + [string]$Location = $env:LOCATION, + [string]$CcVmSize = "Standard_DC2as_v5", # smallest AMD SEV-SNP CVM (2 vCPU / 8 GiB) + [string]$SystemVmSize = "Standard_D2as_v6", # tiny non-CC system pool (AMD, allowed by typical Allowed-VM-SKUs policies) + [ValidateRange(1, 10)][int]$CcNodeCount = 2 +) + +if ($PSCmdlet.ParameterSetName -eq 'Help') { + Get-Help $PSCommandPath -Detailed + return +} + +foreach ($requiredValue in @{ + RESOURCE_GROUP = $ResourceGroup + ATTENDEE_ID = $AttendeeId + HASH_SUFFIX = $HashSuffix + LOCATION = $Location + }.GetEnumerator()) { + if ([string]::IsNullOrWhiteSpace($requiredValue.Value)) { + throw "$($requiredValue.Key) is not set. Define the MicroHack environment variables before running this script." + } +} + +$nameSuffix = (($HashSuffix -replace '[^a-zA-Z0-9]', '').ToLowerInvariant()) +if ($nameSuffix.Length -gt 10) { $nameSuffix = $nameSuffix.Substring(0, 10) } +if (-not $nameSuffix) { throw 'HASH_SUFFIX must contain at least one letter or number.' } + +$subsID = (az account show --query id --output tsv 2>$null) +if ($LASTEXITCODE -ne 0 -or -not $subsID) { throw 'Azure CLI is not signed in. Run az login.' } +$basename = "cvmcluster$nameSuffix" +$description = "MicroHack Challenge 5" +$region = $Location +$ccVmSize = $CcVmSize +$systemVmSize = $SystemVmSize +$ccNodeCount = $CcNodeCount +$smoketest = $false +$sampleDirectory = Join-Path $PSScriptRoot 'resources/azure-voting-app' + +$startTime = Get-Date +$scriptName = $MyInvocation.MyCommand.Name + +# Get GitHub repository URL from git remote (used as a tag) +$gitRemoteUrl = "" +try { $gitRemoteUrl = (git remote get-url origin) -replace "\.git$","" } catch {} +if (-not $gitRemoteUrl) { $gitRemoteUrl = "[Originally from] https://github.com/Microsoft/confidential-computing" } + +# ACR names cannot contain hyphens, AKS cluster names should be conservative as well. +if ($basename -match '[^a-z0-9]') { + write-host "basename must contain only lowercase letters and digits (no hyphens, no uppercase). ACR does not allow hyphens." -ForegroundColor Red + exit 1 +} + +# MicroHack uses stable names so deployment and selective cleanup address the same cluster. +$resgrp = $ResourceGroup +$aksName = "aks-cvmcluster$nameSuffix" +$acrName = $basename + "acr" +$ccPoolName = "ccpool" # 12-char max, lowercase, AMD SEV-SNP node pool +$systemPool = "syspool" + +write-host "----------------------------------------------------------------------------------------------------------------" +write-host "Building AKS cluster '$aksName' with AMD SEV-SNP CC node pool in '$region' (subscription $subsID)" +write-host " System pool : $systemPool 1x $systemVmSize" +write-host " CC pool : $ccPoolName ${ccNodeCount}x $ccVmSize (AMD SEV-SNP)" +write-host " Resource Gp : $resgrp" +if ($smoketest) { write-host "SMOKETEST MODE: Resources will be auto-deleted after the front-end is verified" -ForegroundColor Yellow } +write-host "Script: $scriptName" +write-host "Repository URL: $gitRemoteUrl" +write-host "----------------------------------------------------------------------------------------------------------------" + +# Set subscription context for both Az and az CLI +Set-AzContext -SubscriptionId $subsID | Out-Null +if (!$?) { write-host "Failed to Set-AzContext to $subsID" -ForegroundColor Red; exit 1 } +az account set --subscription $subsID | Out-Null +if (!$?) { write-host "Failed to az account set --subscription $subsID" -ForegroundColor Red; exit 1 } + +$tmp = Get-AzContext +$ownername = $AttendeeId + +if ($Cleanup) { + az group show --name $resgrp --output none + if ($LASTEXITCODE -ne 0) { throw "Resource group '$resgrp' was not found." } + + az aks show --resource-group $resgrp --name $aksName --output none 2>$null + if ($LASTEXITCODE -eq 0) { + write-host "Deleting AKS cluster '$aksName'..." -ForegroundColor Cyan + az aks delete --resource-group $resgrp --name $aksName --yes --no-wait --output none + if ($LASTEXITCODE -ne 0) { throw "Failed to delete AKS cluster '$aksName'." } + } + write-host "Cleanup initiated. Resource group '$resgrp' was retained." -ForegroundColor Green + return +} + +# ---------- Pre-flight: SKU + quota check for the CC pool ----------------------------------------- +if ($SkipSkuPreflight) { + write-host "Pre-flight check SKIPPED (-SkipSkuPreflight)." -ForegroundColor Yellow +} else { + write-host "Pre-flight: confirming '$ccVmSize' is available in '$region' with sufficient AMD CVM vCPU quota..." -ForegroundColor Cyan + + # Hard-fail on Intel SGX SKUs - this script targets full-VM CC (SEV-SNP) + if ($ccVmSize -match '^Standard_DC\d+s_v[23]$') { + write-host "ERROR: '$ccVmSize' is an Intel SGX SKU; this script targets AMD SEV-SNP Confidential VM nodes." -ForegroundColor Red + exit 1 + } + if ($ccVmSize -notmatch '^Standard_(DC|EC)\d+a[a-z]*_v\d+$') { + write-host "Warning: '$ccVmSize' does not look like an AMD SEV-SNP CVM SKU (expected DCa*/ECa*v5 family)." -ForegroundColor Yellow + } + + $skuInfo = $null + try { + $skuInfo = Get-AzComputeResourceSku -Location $region -ErrorAction Stop | + Where-Object { $_.ResourceType -eq 'virtualMachines' -and $_.Name -eq $ccVmSize } | + Select-Object -First 1 + } catch { + write-host "Warning: Get-AzComputeResourceSku failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + + if ($null -eq $skuInfo) { + write-host "ERROR: '$ccVmSize' is not offered in '$region'." -ForegroundColor Red + write-host "Find available regions: Get-AzComputeResourceSku | ? { `$_.Name -eq '$ccVmSize' -and -not `$_.Restrictions } | Select Locations" -ForegroundColor Gray + exit 1 + } + + $subRestriction = $skuInfo.Restrictions | Where-Object { + $_.ReasonCode -eq 'NotAvailableForSubscription' -or + ($_.RestrictionInfo -and $_.RestrictionInfo.Locations -contains $region) + } + if ($subRestriction) { + $reason = ($skuInfo.Restrictions | ForEach-Object { $_.ReasonCode }) -join ', ' + write-host "ERROR: '$ccVmSize' is restricted for this subscription in '$region' (reason: $reason)." -ForegroundColor Red + exit 1 + } + + $skuVCpus = ($skuInfo.Capabilities | Where-Object Name -eq 'vCPUs' | Select-Object -First 1).Value -as [int] + if (-not $skuVCpus) { $skuVCpus = 2 } + $needed = $skuVCpus * $ccNodeCount + $skuFamily = $skuInfo.Family + 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 pool needs {5}." -f ` + $skuFamily, $region, $usage.CurrentValue, $usage.Limit, $available, $needed) -ForegroundColor Cyan + if ($available -lt $needed) { + write-host "ERROR: Insufficient AMD CVM vCPU quota in '$skuFamily' / '$region' ($needed needed, $available available)." -ForegroundColor Red + exit 1 + } + } + } catch { + write-host "Warning: Get-AzVMUsage failed: $($_.Exception.Message). Continuing." -ForegroundColor Yellow + } + write-host "Pre-flight passed: '$ccVmSize' available with quota in '$region'." -ForegroundColor Green +} + +# ---------- Resource group ------------------------------------------------------------------------ +$rgTags = @{ + owner = $ownername + BuiltBy = $scriptName + GitRepo = $gitRemoteUrl + Workload = "azure-voting-app" + CCType = "AMD-SEV-SNP" +} +if ($description -ne "") { $rgTags.Add("description", $description) } +if ($smoketest) { $rgTags.Add("smoketest", "true") } + +az group show --name $resgrp --output none +if ($LASTEXITCODE -ne 0) { throw "Resource group '$resgrp' was not found." } + +# ---------- AKS cluster --------------------------------------------------------------------------- +# Auto-patching strategy (no preview features required, safe defaults that reflect the recommended +# Azure Policies "Kubernetes clusters should have auto-upgrade enabled" and node-image auto-upgrade): +# --auto-upgrade-channel stable cluster K8s version auto-upgrades to stable +# --node-os-upgrade-channel NodeImage node OS images auto-upgrade weekly +# --enable-managed-identity system-assigned MI for the cluster +# --tier standard uptime SLA + financially-backed (cheap insurance) +# We deliberately keep local accounts enabled so 'az aks get-credentials' just works for the demo. +az aks show --resource-group $resgrp --name $aksName --output none 2>$null +if ($LASTEXITCODE -ne 0) { + write-host "Creating AKS cluster '$aksName' (this takes ~5 minutes)..." -ForegroundColor Cyan + az aks create ` + --resource-group $resgrp ` + --name $aksName ` + --location $region ` + --node-count 1 ` + --nodepool-name $systemPool ` + --node-vm-size $systemVmSize ` + --os-sku Ubuntu ` + --enable-managed-identity ` + --generate-ssh-keys ` + --auto-upgrade-channel stable ` + --node-os-upgrade-channel NodeImage ` + --tier standard ` + --network-plugin azure ` + --tags owner=$ownername BuiltBy=$scriptName Workload=azure-voting-app Challenge=05 ` + --only-show-errors + if ($LASTEXITCODE -ne 0) { write-host "az aks create failed" -ForegroundColor Red; exit 1 } +} + +# ---------- AMD SEV-SNP Confidential Computing node pool ------------------------------------------ +# AMD SEV-SNP CVM node pools require Ubuntu and a DCa*/ECa* v5 SKU. Secure Boot + vTPM are enabled +# implicitly by the platform when a CVM SKU is selected; no extra flags are needed. +az aks nodepool show --resource-group $resgrp --cluster-name $aksName --name $ccPoolName --output none 2>$null +if ($LASTEXITCODE -ne 0) { + write-host "Adding AMD SEV-SNP CC node pool '$ccPoolName' (${ccNodeCount}x $ccVmSize)..." -ForegroundColor Cyan + az aks nodepool add ` + --resource-group $resgrp ` + --cluster-name $aksName ` + --name $ccPoolName ` + --node-count $ccNodeCount ` + --node-vm-size $ccVmSize ` + --os-sku Ubuntu ` + --mode User ` + --labels workload=confidential sku=amd-sev-snp ` + --tags owner=$ownername CCType=AMD-SEV-SNP ` + --only-show-errors + if ($LASTEXITCODE -ne 0) { write-host "az aks nodepool add failed for CC pool" -ForegroundColor Red; exit 1 } +} + +# ---------- kubectl access ------------------------------------------------------------------------ +if (-not (Get-Command kubectl -ErrorAction SilentlyContinue)) { + write-host "kubectl not found - installing via 'az aks install-cli'" -ForegroundColor Yellow + az aks install-cli --only-show-errors | Out-Null +} +write-host "Fetching cluster credentials..." -ForegroundColor Cyan +az aks get-credentials --resource-group $resgrp --name $aksName --overwrite-existing --only-show-errors | Out-Null +if ($LASTEXITCODE -ne 0) { write-host "az aks get-credentials failed" -ForegroundColor Red; exit 1 } + +# Sanity: list nodes +kubectl get nodes -o wide +if ($LASTEXITCODE -ne 0) { write-host "kubectl get nodes failed - cluster not reachable" -ForegroundColor Red; exit 1 } + +# ---------- Deploy public Azure Voting App (multi-container) -------------------------------------- +# Source: https://github.com/Azure-Samples/azure-voting-app-redis - public images on mcr.microsoft.com +# We pin the front-end to the CC node pool via nodeSelector so the app actually runs inside SEV-SNP. +$votingManifest = @' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-back + labels: + app: azure-vote-back +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-back + template: + metadata: + labels: + app: azure-vote-back + spec: + nodeSelector: + kubernetes.io/os: linux + containers: + - name: azure-vote-back + image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 6379 + name: redis +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-back +spec: + ports: + - port: 6379 + selector: + app: azure-vote-back +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-front + labels: + app: azure-vote-front +spec: + replicas: 2 + selector: + matchLabels: + app: azure-vote-front + template: + metadata: + labels: + app: azure-vote-front + spec: + nodeSelector: + kubernetes.io/os: linux + workload: confidential + containers: + - name: azure-vote-front + # The previously-published mcr.microsoft.com/azuredocs/azure-vote-front:v1 image was + # removed from MCR. We bootstrap the same Flask app at runtime from the public source + # repo so the sample works first-time without requiring an attached ACR. + image: docker.io/library/python:3.9-slim + command: ["bash","-c"] + args: + - | + set -e + apt-get update -qq && apt-get install -y -qq --no-install-recommends git ca-certificates >/dev/null + rm -rf /src + git clone --depth 1 https://github.com/Azure-Samples/azure-voting-app-redis /src + rm -rf /app && mkdir -p /app + cp -r /src/azure-vote/azure-vote/. /app/ + cd /app + pip install --no-cache-dir flask redis >/dev/null + exec python -c "import sys; sys.path.insert(0,'.'); from main import app; app.run(host='0.0.0.0', port=80)" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + ports: + - containerPort: 80 + env: + - name: REDIS + value: "azure-vote-back" + startupProbe: + httpGet: { path: /, port: 80 } + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 60 + readinessProbe: + httpGet: { path: /, port: 80 } + periodSeconds: 10 + failureThreshold: 6 +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-front +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 80 + selector: + app: azure-vote-front +'@ + +$manifestFile = Join-Path $env:TEMP "azure-vote-$basename.yaml" +$votingManifest | Out-File -FilePath $manifestFile -Encoding utf8 -Force +write-host "Applying voting-app manifest from $manifestFile..." -ForegroundColor Cyan +kubectl apply -f $manifestFile +if ($LASTEXITCODE -ne 0) { write-host "kubectl apply failed" -ForegroundColor Red; exit 1 } + +# ---------- Wait for rollouts --------------------------------------------------------------------- +write-host "Waiting for deployments to become available..." -ForegroundColor Cyan +kubectl rollout status deployment/azure-vote-back --timeout=5m +if ($LASTEXITCODE -ne 0) { write-host "azure-vote-back rollout failed" -ForegroundColor Red; kubectl describe deployment azure-vote-back; exit 1 } +kubectl rollout status deployment/azure-vote-front --timeout=10m +if ($LASTEXITCODE -ne 0) { write-host "azure-vote-front rollout failed" -ForegroundColor Red; kubectl describe deployment azure-vote-front; kubectl get pods -l app=azure-vote-front -o wide; exit 1 } + +# ---------- Wait for LoadBalancer external IP ----------------------------------------------------- +write-host "Waiting for LoadBalancer to allocate a public IP (up to 5 minutes)..." -ForegroundColor Cyan +$externalIP = $null +for ($i = 1; $i -le 30; $i++) { + $externalIP = kubectl get service azure-vote-front -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>$null + if ($externalIP) { break } + Start-Sleep -Seconds 10 + write-host " ...still waiting for external IP (attempt $i/30)" +} +if (-not $externalIP) { + write-host "Timed out waiting for external IP" -ForegroundColor Red + kubectl describe service azure-vote-front + exit 1 +} +write-host "Voting app external IP: $externalIP" -ForegroundColor Green + +# ---------- Smoke test the front-end -------------------------------------------------------------- +write-host "Smoke testing http://$externalIP/ ..." -ForegroundColor Cyan +$ok = $false +for ($i = 1; $i -le 30; $i++) { + try { + $resp = Invoke-WebRequest -Uri "http://$externalIP/" -UseBasicParsing -TimeoutSec 10 + if ($resp.StatusCode -eq 200 -and $resp.Content -match 'Cats|Dogs|Azure Voting App') { + $ok = $true; break + } + } catch { } + Start-Sleep -Seconds 10 + write-host " ...front-end not responding yet (attempt $i/30)" +} +if (-not $ok) { + write-host "Front-end did not respond with expected content within 5 minutes" -ForegroundColor Red + kubectl get pods -o wide + kubectl logs -l app=azure-vote-front --tail=50 + exit 1 +} + +write-host "----------------------------------------------------------------------------------------------------------------" +write-host "SUCCESS: Azure Voting App is live at http://$externalIP/" -ForegroundColor Green +write-host "Cluster : $aksName" +write-host "Resource group : $resgrp" +write-host "CC node pool : $ccPoolName (${ccNodeCount}x $ccVmSize - AMD SEV-SNP)" +write-host "Auto-upgrade : cluster=stable node-os=NodeImage" +write-host "----------------------------------------------------------------------------------------------------------------" + +# ---------- Deploy the runtime-attestation web UI ------------------------------------------------- +# Wraps Azure/cvm-attestation-tools so a user can click "Attest" and see a fresh MAA-signed +# SEV-SNP attestation token with every claim explained. Bootstrapped at pod startup from a +# ConfigMap built from the local ./attestation/ folder (no ACR needed). +write-host "Deploying CC runtime-attestation web UI..." -ForegroundColor Cyan +$attestDir = Join-Path $sampleDirectory 'attestation' +foreach ($f in @('app.py','config_snp.json','templates/index.html')) { + if (-not (Test-Path (Join-Path $attestDir $f))) { + write-host "Missing $f under $attestDir - skipping attestation deployment." -ForegroundColor Yellow + $attestDir = $null + break + } +} + +if ($attestDir) { + # Build a single ConfigMap with three flat keys (app.py, config_snp.json, index.html). + $attestAppPath = Join-Path $attestDir 'app.py' + $attestConfigPath = Join-Path $attestDir 'config_snp.json' + $attestTemplatePath = Join-Path $attestDir 'templates/index.html' + $cmYaml = kubectl create configmap cc-attest-app ` + "--from-file=app.py=$attestAppPath" ` + "--from-file=config_snp.json=$attestConfigPath" ` + "--from-file=index.html=$attestTemplatePath" ` + --dry-run=client -o yaml + if ($LASTEXITCODE -ne 0) { write-host "Failed to build attestation ConfigMap" -ForegroundColor Red; exit 1 } + $cmFile = Join-Path $env:TEMP "cc-attest-cm-$basename.yaml" + $cmYaml | Out-File -FilePath $cmFile -Encoding utf8 -Force + kubectl apply -f $cmFile | Out-Null + + $attestManifest = @' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cc-attest + labels: { app: cc-attest } +spec: + replicas: 1 + selector: { matchLabels: { app: cc-attest } } + template: + metadata: + labels: { app: cc-attest } + spec: + nodeSelector: + kubernetes.io/os: linux + workload: confidential + containers: + - name: cc-attest + image: docker.io/library/python:3.11-slim + # Privileged + host TPM device passthrough so the upstream tool can read the + # SEV-SNP HCL report from the node vTPM. + securityContext: + privileged: true + env: + - name: POD_NAME + valueFrom: { fieldRef: { fieldPath: metadata.name } } + - name: NODE_NAME + valueFrom: { fieldRef: { fieldPath: spec.nodeName } } + - name: CVM_TOOLS_DIR + value: /opt/cvm-tools/cvm-attestation + ports: + - containerPort: 80 + command: ["bash","-c"] + args: + - | + set -e + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq --no-install-recommends git ca-certificates tpm2-tools >/dev/null + rm -rf /opt/cvm-tools + git clone --depth 1 https://github.com/Azure/cvm-attestation-tools.git /opt/cvm-tools + cd /opt/cvm-tools/cvm-attestation + pip install --no-cache-dir -r requirements.txt >/dev/null + pip install --no-cache-dir flask >/dev/null + mkdir -p /app/templates + cp /etc/attest-app/app.py /app/app.py + cp /etc/attest-app/config_snp.json /app/config_snp.json + cp /etc/attest-app/index.html /app/templates/index.html + exec python /app/app.py + volumeMounts: + - { name: tpmrm, mountPath: /dev/tpmrm0 } + - { name: tpm0, mountPath: /dev/tpm0 } + - { name: securityfs, mountPath: /sys/kernel/security, readOnly: true } + - { name: app, mountPath: /etc/attest-app } + startupProbe: + httpGet: { path: /healthz, port: 80 } + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 60 + readinessProbe: + httpGet: { path: /healthz, port: 80 } + periodSeconds: 10 + failureThreshold: 6 + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: 1, memory: 1Gi } + volumes: + - name: tpmrm + hostPath: { path: /dev/tpmrm0, type: CharDevice } + - name: tpm0 + hostPath: { path: /dev/tpm0, type: CharDevice } + - name: securityfs + hostPath: { path: /sys/kernel/security, type: Directory } + - name: app + configMap: + name: cc-attest-app +--- +apiVersion: v1 +kind: Service +metadata: + name: cc-attest +spec: + type: LoadBalancer + selector: { app: cc-attest } + ports: + - port: 80 + targetPort: 80 +'@ + + $attestManifestFile = Join-Path $env:TEMP "cc-attest-$basename.yaml" + $attestManifest | Out-File -FilePath $attestManifestFile -Encoding utf8 -Force + kubectl apply -f $attestManifestFile + if ($LASTEXITCODE -ne 0) { write-host "kubectl apply failed for attestation manifest" -ForegroundColor Red; exit 1 } + + # Restart deployment so any ConfigMap changes from re-runs are picked up. + kubectl rollout restart deployment/cc-attest | Out-Null + write-host "Waiting for cc-attest rollout (first run installs tpm2-tools + clones upstream)..." -ForegroundColor Cyan + kubectl rollout status deployment/cc-attest --timeout=10m + if ($LASTEXITCODE -ne 0) { + write-host "cc-attest rollout failed" -ForegroundColor Red + kubectl describe deployment cc-attest + kubectl logs -l app=cc-attest --tail=80 + } else { + # Wait for the attestation LoadBalancer. + $attestIP = $null + for ($i = 1; $i -le 30; $i++) { + $attestIP = kubectl get service cc-attest -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>$null + if ($attestIP) { break } + Start-Sleep -Seconds 10 + write-host " ...still waiting for cc-attest external IP (attempt $i/30)" + } + if ($attestIP) { + write-host "----------------------------------------------------------------------------------------------------------------" + write-host "CC Attestation UI live at http://$attestIP/" -ForegroundColor Green + write-host " Click 'Attest' to fetch a fresh MAA-signed SEV-SNP token with every claim explained." + write-host "----------------------------------------------------------------------------------------------------------------" + } else { + write-host "cc-attest LoadBalancer did not get an external IP in time." -ForegroundColor Yellow + } + } +} + +# ---------- MicroHack cleanup guidance ------------------------------------------------------------ +write-host "" +write-host "Resources created in resource group: $resgrp" +write-host "To clean up: ./Deploy-VotingAppCC.ps1 -Cleanup" + +$myTimeSpan = New-TimeSpan -Start $startTime -End (Get-Date) +Write-Output ("Execution time was {0} minutes and {1} seconds." -f $myTimeSpan.Minutes, $myTimeSpan.Seconds) diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/UPSTREAM-SOURCE.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/UPSTREAM-SOURCE.md new file mode 100644 index 000000000..2320a088a --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/UPSTREAM-SOURCE.md @@ -0,0 +1,37 @@ +# Challenge 5 upstream source + +The local [`resources/azure-voting-app`](resources/azure-voting-app/README.md) +directory is a byte-for-byte snapshot of the Azure Confidential Computing +Azure Voting App sample. + +- Repository: +- Source path: `aks-samples/azure-voting-app` +- Source commit: [`37f033361f1ded657e26382534f42b5f95a88054`](https://github.com/Azure/confidential-computing/tree/37f033361f1ded657e26382534f42b5f95a88054/aks-samples/azure-voting-app) +- Source commit date: 2026-06-04 + +Do not edit the snapshot when making MicroHack-specific changes. The +top-level `Deploy-VotingAppCC.ps1` starts from the upstream script and contains +the workshop adaptations. + +> [!IMPORTANT] +> Run the top-level script from the Challenge 5 walkthrough directory. The +> script inside `resources/azure-voting-app` is the unchanged upstream +> reference and creates and deletes its own resource group. + +## MicroHack adaptations + +1. Read `RESOURCE_GROUP`, `ATTENDEE_ID`, `HASH_SUFFIX`, and `LOCATION` from the + established MicroHack environment variables. +2. Derive the subscription from the active Azure CLI account and retain the + upstream requirement for the Az PowerShell context to target that same + subscription. +3. Reuse the attendee resource group instead of creating or deleting a resource + group. +4. Derive a deterministic AKS cluster name from `HASH_SUFFIX` and make cluster + and confidential node-pool creation rerunnable. +5. Resolve attestation assets from the unchanged local snapshot. +6. Delete only the Challenge 5 AKS cluster during cleanup. + +When updating the sample, replace the complete snapshot from one upstream +commit, update the commit above, and review the top-level script diff against +the new upstream `Deploy-VotingAppCC.ps1`. \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/01-deploy-start-preflight.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/01-deploy-start-preflight.png new file mode 100644 index 000000000..e679db55e Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/01-deploy-start-preflight.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/02-deployment-complete-urls.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/02-deployment-complete-urls.png new file mode 100644 index 000000000..79da3127b Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/02-deployment-complete-urls.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/03-aks-attestation-result.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/03-aks-attestation-result.png new file mode 100644 index 000000000..e8ebce020 Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/03-aks-attestation-result.png differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks-02.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks-02.png deleted file mode 100644 index 0e4f341c2..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks-02.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks-03.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks-03.png deleted file mode 100644 index 4654290ce..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks-03.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks.png deleted file mode 100644 index 2addc9644..000000000 Binary files a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/images/aks.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/portal-guide.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/portal-guide.md deleted file mode 100644 index 6c67b1e15..000000000 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/portal-guide.md +++ /dev/null @@ -1,656 +0,0 @@ -# Walkthrough Challenge 5 - Encryption in use with Confidential VMs/Node Pools in Azure Kubernetes Service (AKS) -# Azure Portal Guide - -**Estimated Duration:** 90-120 minutes - -> 💡 **Objective:** Learn how to deploy and validate guest attestation on Azure Confidential VMs in AKS to ensure business logic only executes in trusted, hardware-backed confidential computing environments. You will create an AKS cluster through the Azure Portal, add a Confidential VM node pool, deploy attestation workloads, and verify cryptographic proof of node integrity before processing sensitive operations. - ---- - -## Prerequisites - -Please ensure that you successfully verified the [General prerequisites](../../Readme.md#general-prerequisites) before continuing with this challenge. - -- Access to Azure Portal (https://portal.azure.com) -- Azure subscription with permissions to create AKS clusters, node pools, and register preview features -- Azure Cloud Shell access (for kubectl commands) -- Basic understanding of Kubernetes concepts (pods, deployments, node pools) -- Familiarity with Azure Portal navigation -- Basic understanding of confidential computing concepts - -**Configuration Variables:** Throughout this challenge, replace these placeholders with your values: -- **ATTENDEE_ID**: `labuser-xx` (customize for each participant, e.g., labuser-01, labuser-02) -- **Region**: North Europe -- **Resource Group**: `${ATTENDEE_ID}` -- **AKS Cluster Name**: `aks-cvmcluster-` (Azure will help ensure uniqueness) -- **Confidential Node Pool Name**: `cvmnodepool` -- **VM Size for Confidential Nodes**: `Standard_DC2as_v5` - -💡 **Note**: Whenever you see `${ATTENDEE_ID}` or `` in commands or configuration, replace them with your actual values. - -## Scenario Context - -You are a cloud security engineer at a European healthcare organization that processes sensitive patient data in containerized applications. Your organization has adopted Kubernetes (AKS) for container orchestration but must comply with strict data protection regulations including GDPR and healthcare privacy requirements. - -Your mandate includes: - -- **Encryption in Use**: Protect data while it's being processed in containers, not just at rest or in transit -- **Hardware-Based Trust**: Ensure security guarantees are rooted in hardware (AMD SEV-SNP), not just software isolation -- **Attestation for Workloads**: Verify that containerized applications run only on genuine confidential computing nodes -- **Zero Trust for Kubernetes**: Validate the integrity of worker nodes before deploying sensitive workloads -- **Compliance and Auditability**: Provide cryptographic evidence that workloads execute in compliant infrastructure - -In this challenge, you'll deploy Azure Confidential Computing node pools in AKS using the Azure Portal. You'll configure a confidential VM node pool with AMD SEV-SNP technology, deploy attestation-aware workloads, and verify that containers run in hardware-protected environments before processing sensitive operations. - -### Understanding Confidential VMs in AKS - -Azure Kubernetes Service (AKS) supports Confidential VM node pools, which provide: - -- **Hardware-based memory encryption** - AMD SEV-SNP encrypts node memory at the hardware level -- **Node attestation** - Cryptographic proof that worker nodes run on genuine confidential hardware -- **Workload isolation** - Containers benefit from the underlying confidential VM protections -- **Integration with existing Kubernetes workflows** - Use standard Kubernetes constructs (node selectors, taints/tolerations) - -**Security Architecture:** - -This setup implements defense-in-depth security: -- **Confidential VM Node Pool** - Worker nodes with AMD SEV-SNP hardware encryption -- **Attestation Verification** - Pods can verify they're running on confidential nodes -- **Node Selectors** - Target sensitive workloads to confidential node pools only -- **Microsoft Azure Attestation (MAA)** - Validates node integrity and issues signed JWT tokens - -### Learning Resources - -- [Azure Confidential Computing overview](https://azure.microsoft.com/solutions/confidential-compute/) -- [Confidential VMs on Azure](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) -- [AKS with Confidential Computing](https://learn.microsoft.com/azure/aks/use-confidential-computing) -- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/) - -### Original Source Materials - -This challenge is based on the **AKS with Confidential Computing Linux Sample** from the Microsoft Azure Confidential Computing repository: - -- **Main Repository**: [Azure Confidential Computing CVM Guest Attestation](https://github.com/Azure/confidential-computing-cvm-guest-attestation) -- **Source Module**: [AKS Linux Sample](https://github.com/Azure/confidential-computing-cvm-guest-attestation/tree/main/aks-linux-sample) - -The AKS deployment patterns and attestation verification workflows have been adapted for this MicroHack challenge to provide a guided learning experience with Confidential VM node pools in Azure Kubernetes Service using the Azure Portal. - ---- - -## Task 1: Enable Preview Features and Prepare Environment - -💡 **Before creating AKS clusters with Confidential VM node pools, you must register the required preview features.** - -### Step 1: Register the AzureLinuxCVMPreview Feature - -1. In the Azure Portal, search for **Subscriptions** in the top search bar -2. Select your subscription -3. In the left menu, under **Settings**, click **Preview features** - - ![Screenshot placeholder: Subscription Preview Features menu] - -4. In the search box, type: `AzureLinuxCVMPreview` -5. Select **AzureLinuxCVMPreview** from the results -6. Click **Register** - - ![Screenshot placeholder: Register AzureLinuxCVMPreview feature] - -7. Wait for the registration status to show **Registered** (this may take several minutes) -8. You can check the status by clicking **Refresh** periodically - - ![Screenshot placeholder: Feature registration status showing Registered] - ---- - ---- - -## Task 2: Create Resource Group - -💡 **Create the foundational resource group to organize all AKS-related resources.** - -1. In the Azure Portal, click **Create a resource** or search for **Resource groups** -2. Click **+ Create** -3. Fill in the following details: - - **Subscription**: Select your subscription - - **Resource group**: `${ATTENDEE_ID}` (e.g., `labuser-01`) - - **Region**: **North Europe** - - ![Screenshot placeholder: Create resource group form] - -4. Click **Review + create** -5. Click **Create** - - ![Screenshot placeholder: Resource group created successfully] - ---- - ---- - -## Task 3: Create AKS Cluster with Standard Node Pool - -💡 **Deploy the AKS cluster with a standard system node pool. You'll add the Confidential VM node pool in the next task.** - -1. In the Azure Portal, search for **Kubernetes services** in the top search bar -2. Click **+ Create** and select **Kubernetes cluster** - - ![Screenshot placeholder: Create Kubernetes cluster button] - -### Basics Tab - -3. Fill in the **Basics** tab: - - **Subscription**: Select your subscription - - **Resource group**: `${ATTENDEE_ID}` (select the one you just created) - - **Cluster preset configuration**: **Dev/Test** - - **Kubernetes cluster name**: `aks-cvmcluster-` (e.g., `aks-cvmcluster-abc123`) - - **Region**: **North Europe** - - **Availability zones**: None (or as preferred) - - **AKS pricing tier**: **Free** - - **Kubernetes version**: Default (latest stable version) - - **Automatic upgrade**: **Disabled** - - **Node security channel type**: **None** - - ![Screenshot placeholder: AKS Basics tab configuration] - -### Node pools Tab - -4. Click **Next: Node pools** -5. Keep the default node pool settings: - - **Node pool name**: `agentpool` (default system node pool) - - **Node size**: Default (e.g., `Standard_DS2_v2`) - - **Scale method**: **Manual** - - **Node count**: **1** - - ![Screenshot placeholder: AKS Node pools tab] - -### Networking Tab - -6. Click **Next: Networking** -7. Configure networking: - - **Network configuration**: **Azure CNI** - - **Network policy**: **None** (or **Calico** if preferred) - - Leave other settings as default - - ![Screenshot placeholder: AKS Networking tab] - -### Integrations, Advanced, and Tags Tabs - -8. Click **Next** through **Integrations**, **Advanced**, and **Tags** tabs -9. Keep the default settings or configure as needed - -### Review + Create - -10. Click **Review + create** -11. Wait for validation to complete -12. Click **Create** - - ![Screenshot placeholder: AKS cluster deployment in progress] - -13. Wait for the deployment to complete (this may take 5-10 minutes) - - ![Screenshot placeholder: AKS deployment completed] - ---- - ---- - -## Task 4: Connect to AKS Cluster - -💡 **Configure kubectl to connect to your AKS cluster using Azure Cloud Shell.** - -1. Once deployment is complete, click **Go to resource** -2. In the AKS cluster overview page, click **Connect** in the top menu - - ![Screenshot placeholder: AKS Connect button] - -3. Select **Azure CLI** tab -4. Click **Open Cloud Shell** - - ![Screenshot placeholder: Cloud Shell connection instructions] - -5. In Cloud Shell, run the provided `az aks get-credentials` command (replace with your values): - ```bash - az aks get-credentials --resource-group ${ATTENDEE_ID} --name aks-cvmcluster- - ``` - -6. Verify the connection: - ```bash - kubectl get nodes - ``` - - ![Screenshot placeholder: kubectl get nodes output in Cloud Shell] - ---- - ---- - -## Task 5: Add Confidential VM Node Pool - -💡 **Create a dedicated node pool with Confidential VM compute resources for running sensitive workloads.** - -### Using Azure Portal - -1. In your AKS cluster page, go to **Settings** > **Node pools** in the left menu - - ![Screenshot placeholder: Node pools menu] - -2. Click **+ Add node pool** - - ![Screenshot placeholder: Add node pool button] - -3. Configure the confidential node pool: - - **Node pool name**: `cvmnodepool` - - **Mode**: **User** - - **OS SKU**: **Azure Linux** (or **Ubuntu**) - - **Node size**: Click **Choose a size** - - In the size selector, search for: `DC2as_v5` - - Select **Standard_DC2as_v5** (Confidential VM size) - - Click **Select** - - ![Screenshot placeholder: Select VM size showing DC2as_v5] - -4. Configure scale settings: - - **Scale method**: **Manual** - - **Node count**: **1** - - ![Screenshot placeholder: Node pool configuration form] - -5. Leave other settings as default -6. Click **Add** - - ![Screenshot placeholder: Node pool being added] - -7. Wait for the node pool to be created (this may take 5-10 minutes) -8. The status will change from **Creating** to **Succeeded** - - ![Screenshot placeholder: Node pool list showing cvmnodepool] - -### Alternative: Using Cloud Shell - -If you prefer, you can add the node pool using Cloud Shell: - -```bash -az aks nodepool add \ - --resource-group ${ATTENDEE_ID} \ - --cluster-name aks-cvmcluster- \ - --name cvmnodepool \ - --node-count 1 \ - --node-vm-size Standard_DC2as_v5 -``` - ---- - ---- - -## Task 6: Verify Confidential Node Pool Configuration - -💡 **Validate that the Confidential VM node pool is properly configured and running.** - -### Using Azure Portal - -1. In your AKS cluster, go to **Settings** > **Node pools** -2. Click on **cvmnodepool** -3. Verify the following details: - - **Node size**: `Standard_DC2as_v5` - - **Node count**: 1 - - **Status**: Running - - ![Screenshot placeholder: Node pool details page] - -### Using Cloud Shell - -1. Open Cloud Shell from the top menu bar -2. Run the following commands to verify: - -```bash -# Verify the VM size -az aks nodepool show \ - --resource-group ${ATTENDEE_ID} \ - --cluster-name aks-cvmcluster- \ - --name cvmnodepool \ - --query 'vmSize' - -# Verify the node image version -az aks nodepool list \ - --resource-group ${ATTENDEE_ID} \ - --cluster-name aks-cvmcluster- \ - --query "[?name=='cvmnodepool'].nodeImageVersion" -o tsv -``` - -3. Check the nodes using kubectl: -```bash -kubectl get nodes -o wide -``` - -You should see two nodes: one from the default node pool and one confidential VM node. - -![Screenshot placeholder: kubectl get nodes showing cvmnodepool node] - ---- - ---- - -## Task 7: Deploy Attestation Verification Pod - -💡 **Deploy a sample pod that retrieves attestation data to prove it's running on a confidential VM node.** - -### Step 1: Upload the Attestation Pod YAML File - -1. The attestation pod YAML file is located at `walkthrough/challenge-5/resources/cvm-attestation-pod.yaml` in this repository. - -2. In Azure Cloud Shell, click the **Upload/Download files** button (📁) in the toolbar - -3. Select **Upload** and choose the `cvm-attestation-pod.yaml` file from your local machine - -4. Wait for the upload to complete - you should see a confirmation message - - ![Screenshot placeholder: Cloud Shell upload button] - -### Step 2: Deploy the Pod - -5. In Cloud Shell, apply the YAML file: - -```bash -kubectl apply -f cvm-attestation-pod.yaml -``` - - ![Screenshot placeholder: kubectl apply output] - -6. Check the pod status: - -```bash -kubectl get pods -``` - -Wait until the pod status shows **Running**. - -![Screenshot placeholder: kubectl get pods showing running status] - ---- - ---- - -## Task 8: Retrieve and Analyze Attestation Report - -💡 **Examine the attestation JWT token to verify the pod is running on genuine confidential hardware.** - -1. In Cloud Shell, retrieve the attestation logs: -```bash -kubectl logs cvm-attestation -``` - - ![Screenshot placeholder: kubectl logs output showing attestation report] - -### Expected Output - -The attestation report will contain JWT tokens with confidential computing verification data: - -```json -{ - "alg": "RS256", - "jku": "https://sharedeus2.eus2.attest.azure.net/certs", - "kid": "J0pAPdfXXHqWWimgrH853wMIdh5/fLe1z6uSXYPXCa0=", - "typ": "JWT" -} -{ - "exp": 1663376286, - "iat": 1663347486, - "iss": "https://sharedeus2.eus2.attest.azure.net", - "jti": "89a500344d9ecc081b14ff6c848fbc1d557694946e6f8d83687654a1139e055d", - "nbf": 1663347486, - "secureboot": true, - "x-ms-attestation-type": "azurevm", - "x-ms-azurevm-attestation-protocol-ver": "2.0", - "x-ms-azurevm-attested-pcrs": [0, 1, 2, 3, 4, 5, 6, 7], - "x-ms-azurevm-bootdebug-enabled": false, - "x-ms-azurevm-dbvalidated": true, - "x-ms-azurevm-dbxvalidated": true, - "x-ms-azurevm-debuggersdisabled": true, - "x-ms-azurevm-default-securebootkeysvalidated": true, - "x-ms-azurevm-elam-enabled": false, - "x-ms-azurevm-flightsigning-enabled": false, - "x-ms-azurevm-hvci-policy": 0, - "x-ms-azurevm-hypervisordebug-enabled": false, - "x-ms-azurevm-is-windows": false, - "x-ms-azurevm-kerneldebug-enabled": false, - "x-ms-azurevm-osbuild": "NotApplication", - "x-ms-azurevm-osdistro": "Ubuntu", - "x-ms-azurevm-ostype": "Linux", - "x-ms-azurevm-osversion-major": 18, - "x-ms-azurevm-osversion-minor": 4, - "x-ms-azurevm-signingdisabled": true, - "x-ms-azurevm-testsigning-enabled": false, - "x-ms-azurevm-vmid": "A80B7FE7-5B93-4027-9971-6CCEE468C2B3", - "x-ms-isolation-tee": { - "x-ms-attestation-type": "sevsnpvm", - "x-ms-compliance-status": "azure-compliant-cvm", - "x-ms-runtime": { - "keys": [...], - "vm-configuration": { - "console-enabled": true, - "current-time": 1662691445, - "secure-boot": true, - "tpm-enabled": true, - "vmUniqueId": "A80B7FE7-5B93-4027-9971-6CCEE468C2B3" - } - }, - "x-ms-sevsnpvm-authorkeydigest": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "x-ms-sevsnpvm-bootloader-svn": 2, - "x-ms-sevsnpvm-familyId": "01000000000000000000000000000000", - "x-ms-sevsnpvm-guestsvn": 2, - "x-ms-sevsnpvm-hostdata": "0000000000000000000000000000000000000000000000000000000000000000", - "x-ms-sevsnpvm-idkeydigest": "57486a447ec0f1958002a22a06b7673b9fd27d11e1c6527498056054c5fa92d23c50f9de44072760fe2b6fb89740b696", - "x-ms-sevsnpvm-imageId": "02000000000000000000000000000000", - "x-ms-sevsnpvm-is-debuggable": false, - "x-ms-sevsnpvm-launchmeasurement": "ad6de16ac59ee52351c6038df58d1be5aeaf41cd0f7c81b2279ecca0df6ef43a2b69d663ad6973d6dbb9db0ffd7a9023", - "x-ms-sevsnpvm-microcode-svn": 93, - "x-ms-sevsnpvm-migration-allowed": false, - "x-ms-sevsnpvm-reportdata": "d707bdbeeeb6c6e7fa42e94e71ec537e21c8d4c4316422c4011742f55ecc22c00000000000000000000000000000000000000000000000000000000000000000", - "x-ms-sevsnpvm-reportid": "afc01d4d5f22974bd00c2d993bc3354fcd3bf37c789c2611233da72df1712d82", - "x-ms-sevsnpvm-smt-allowed": true, - "x-ms-sevsnpvm-snpfw-svn": 6, - "x-ms-sevsnpvm-tee-svn": 0, - "x-ms-sevsnpvm-vmpl": 0 - }, - "x-ms-policy-hash": "wm9mHlvTU82e8UqoOy1Yj1FBRSNkfe99-69IYDq9eWs", - "x-ms-runtime": { - "client-payload": { - "nonce": "MTIzNA==" - }, - "keys": [...] - }, - "x-ms-ver": "1.0" -} -``` - -### Key Attestation Fields - -The attestation report contains important security validation fields: - -- **secureboot**: `true` - Secure Boot is enabled -- **x-ms-azurevm-attestation-protocol-ver**: Version of attestation protocol -- **x-ms-azurevm-attested-pcrs**: Platform Configuration Registers that were measured -- **x-ms-isolation-tee**: Trusted Execution Environment details -- **x-ms-compliance-status**: `"azure-compliant-cvm"` - Confirms Azure Confidential VM compliance -- **x-ms-sevsnpvm-is-debuggable**: `false` - VM is not debuggable (production setting) -- **x-ms-sevsnpvm-launchmeasurement**: Launch measurement for integrity verification - ---- - ---- - -## Task 9: Explore AKS Resources in Azure Portal - -💡 **Navigate the Azure Portal to view workloads, logs, and node pool configurations.** - -### View Workloads - -1. In your AKS cluster page, go to **Kubernetes resources** > **Workloads** in the left menu -2. Click on **Pods** tab -3. You should see the `cvm-attestation` pod listed - - ![Screenshot placeholder: AKS Workloads showing cvm-attestation pod] - -4. Click on the pod name to view details - - ![Screenshot placeholder: Pod details page] - -### View Logs in Portal - -5. In the pod details page, click on **Logs** tab -6. You can view the attestation report directly in the portal - - ![Screenshot placeholder: Pod logs showing attestation report in portal] - -### View Node Pool Scaling - -7. Go back to **Settings** > **Node pools** -8. Select **cvmnodepool** -9. You can scale the node pool up or down by clicking **Scale** - - ![Screenshot placeholder: Node pool scale options] - ---- - ---- - -## Task 10: Clean Up Resources - -💡 **Delete all resources to avoid ongoing charges.** - -### Using Azure Portal - -1. Go to **Resource groups** in the Azure Portal -2. Find and select your resource group `${ATTENDEE_ID}` -3. Click **Delete resource group** in the top menu - - ![Screenshot placeholder: Delete resource group button] - -4. Type the resource group name to confirm deletion -5. Click **Delete** - - ![Screenshot placeholder: Confirm delete resource group] - -6. Wait for the deletion to complete (this may take several minutes) - -### Using Cloud Shell - -Alternatively, you can delete the resource group using Cloud Shell: - -```bash -az group delete --name ${ATTENDEE_ID} --yes --no-wait -``` - ---- - -## Key Takeaways - -In this challenge, you successfully deployed and validated Azure Confidential Computing in AKS with guest attestation. Here are the key concepts and best practices: - -### Confidential Computing in Kubernetes - -✅ **Node-Level Encryption** - Confidential VM node pools provide hardware-based memory encryption (AMD SEV-SNP) for all containers running on those nodes - -✅ **Workload Targeting** - Use Kubernetes node selectors to schedule sensitive workloads exclusively on confidential node pools - -✅ **Transparent Integration** - Applications benefit from confidential computing protections without code changes (though attestation-aware apps provide stronger guarantees) - -### Attestation in Containerized Environments - -✅ **Node Attestation** - Containers can query the Instance Metadata Service to retrieve attestation tokens proving they run on confidential hardware - -✅ **Runtime Verification** - Applications should verify attestation at startup before processing sensitive data - -✅ **JWT Token Validation** - Attestation tokens are signed by Microsoft Azure Attestation and contain claims about hardware security features - -### AKS Architecture and Security - -✅ **Mixed Node Pools** - Combine standard and confidential node pools in the same cluster for cost optimization (use confidential nodes only for sensitive workloads) - -✅ **Preview Features** - Confidential VM support in AKS requires feature registration and may have regional limitations - -✅ **Portal and CLI Management** - AKS supports both Azure Portal and Azure CLI for managing confidential node pools - -### Production Best Practices - -✅ **Node Pool Sizing** - Start with DC-series VMs (e.g., Standard_DC2as_v5) and scale based on workload requirements - -✅ **Workload Isolation** - Use Kubernetes namespaces, network policies, and RBAC in addition to confidential computing - -✅ **Attestation Policies** - For production, consider custom Azure Attestation policies that enforce specific security requirements - -✅ **Monitoring and Logging** - Integrate AKS diagnostics with Azure Monitor to track confidential node pool health - -### Compliance and Governance - -✅ **Data Sovereignty** - Confidential computing ensures data in containers is encrypted even from cloud operators - -✅ **Regulatory Compliance** - Meets requirements for healthcare (HIPAA), financial services, and government workloads - -✅ **Audit Trail** - Attestation tokens provide cryptographic proof for compliance auditing - ---- - -## Next Steps - -### Explore Advanced AKS Confidential Computing Scenarios - -- **[Confidential Containers on AKS](https://learn.microsoft.com/azure/confidential-computing/confidential-containers)** - Deploy containers with Intel SGX enclaves for process-level isolation -- **[Azure Confidential Computing Add-on for AKS](https://learn.microsoft.com/azure/confidential-computing/confidential-nodes-aks-get-started)** - Explore the official Microsoft add-on for confidential computing -- **[Managed Identity with Confidential AKS](https://learn.microsoft.com/azure/aks/use-managed-identity)** - Integrate Azure AD workload identities with confidential workloads - -### Implement Production-Ready Confidential AKS - -- **Custom Attestation Policies** - Define organization-specific attestation requirements using Azure Attestation policies -- **Multi-Region Deployment** - Deploy confidential AKS clusters across multiple Azure regions for high availability -- **GitOps with Confidential Workloads** - Use Flux or ArgoCD to manage confidential workload deployments -- **Service Mesh Integration** - Combine Istio/Linkerd with confidential computing for end-to-end encryption - -### Secure Your AKS Workloads Further - -- **[Azure Key Vault Provider for Secrets Store CSI Driver](https://learn.microsoft.com/azure/aks/csi-secrets-store-driver)** - Securely inject secrets into confidential pods -- **[Azure Policy for AKS](https://learn.microsoft.com/azure/governance/policy/concepts/policy-for-kubernetes)** - Enforce governance policies on confidential node pools -- **[Network Policies](https://learn.microsoft.com/azure/aks/use-network-policies)** - Restrict network traffic to/from confidential workloads - -### Learn More About Confidential Computing - -- **[Confidential Inferencing](https://learn.microsoft.com/azure/machine-learning/how-to-machine-learning-confidential-containers)** - Deploy ML models in confidential containers on AKS -- **[Azure Confidential Ledger](https://learn.microsoft.com/azure/confidential-ledger/overview)** - Integrate tamper-proof audit logs with confidential AKS workloads -- **[Multi-Party Computation Patterns](https://learn.microsoft.com/azure/confidential-computing/use-cases-scenarios)** - Build collaborative data processing scenarios with confidential AKS - ---- - -## Additional Resources - -- [Azure Confidential Computing](https://azure.microsoft.com/solutions/confidential-compute/) -- [Confidential VMs on Azure](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) -- [AKS Documentation](https://learn.microsoft.com/azure/aks/) -- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/) - ---- - -## Troubleshooting Tips - -### Node Pool Creation Fails - -- Verify that the `AzureLinuxCVMPreview` feature is registered -- Check that the region (North Europe) supports Confidential VM sizes -- Ensure you have sufficient quota for `Standard_DC2as_v5` VMs - -### Pod Stays in Pending State - -- Check node pool status: `kubectl get nodes` -- Verify node selector matches: `kubectl describe pod cvm-attestation` -- Check for resource constraints: `kubectl describe nodes` - -### Cannot Access Cloud Shell - -- Ensure your subscription has Cloud Shell enabled -- Check that you have appropriate permissions -- Try using Azure CLI from your local machine instead - -### Attestation Report Not Showing - -- Verify the pod is running on the confidential node pool -- Check pod logs for errors: `kubectl logs cvm-attestation` -- Ensure the VM size is a Confidential VM SKU (DC-series) - diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/Deploy-VotingAppCC.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/Deploy-VotingAppCC.ps1 new file mode 100644 index 000000000..b42234653 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/Deploy-VotingAppCC.ps1 @@ -0,0 +1,570 @@ +# Hands-off script to build a small AKS cluster with an AMD SEV-SNP Confidential Computing node pool +# and deploy the public Microsoft "Azure Voting App" multi-container sample to it, exposed via a public +# LoadBalancer. Modeled on BuildRandomCVM.ps1 (random naming, tagging, smoketest, preflight). +# +# Simon Gallagher, ACC Product Group +# Use at your own risk, no warranties implied, test in a non-production environment first +# +# References: +# - https://learn.microsoft.com/azure/aks/use-cvm (CVM node pools on AKS) +# - https://learn.microsoft.com/azure/aks/confidential-computing-overview +# - https://learn.microsoft.com/azure/aks/auto-upgrade-cluster (auto-upgrade channel) +# - https://learn.microsoft.com/azure/aks/auto-upgrade-node-image (node OS auto-upgrade) +# - https://github.com/Azure-Samples/azure-voting-app-redis (the demo app) +# +# Usage: +# ./Deploy-VotingAppCC.ps1 -subsID -basename [-region ] ` +# [-description ] [-smoketest] [-SkipSkuPreflight] +# +# Requirements: +# - Azure PowerShell (Az) module + Azure CLI (az), both logged in to the same subscription +# - kubectl on PATH (az aks install-cli will install it if missing) + +param ( + [Parameter(Mandatory)]$subsID, + [Parameter(Mandatory)]$basename, + [Parameter(Mandatory=$false)]$description = "", + [Parameter(Mandatory=$false)][switch]$smoketest, + [Parameter(Mandatory=$false)]$region = "northeurope", + [Parameter(Mandatory=$false)]$ccVmSize = "Standard_DC2as_v5", # smallest AMD SEV-SNP CVM (2 vCPU / 8 GiB) + [Parameter(Mandatory=$false)]$systemVmSize = "Standard_D2as_v6", # tiny non-CC system pool (AMD, allowed by typical Allowed-VM-SKUs policies) + [Parameter(Mandatory=$false)][int]$ccNodeCount = 2, + [Parameter(Mandatory=$false)][switch]$SkipSkuPreflight +) + +if ($subsID -eq "" -or $basename -eq "") { + write-host "You must enter a subscription ID and a basename" + exit 1 +} + +$startTime = Get-Date +$scriptName = $MyInvocation.MyCommand.Name + +# Get GitHub repository URL from git remote (used as a tag) +$gitRemoteUrl = "" +try { $gitRemoteUrl = (git remote get-url origin) -replace "\.git$","" } catch {} +if (-not $gitRemoteUrl) { $gitRemoteUrl = "[Originally from] https://github.com/Microsoft/confidential-computing" } + +# ACR names cannot contain hyphens, AKS cluster names should be conservative as well. +if ($basename -match '[^a-z0-9]') { + write-host "basename must contain only lowercase letters and digits (no hyphens, no uppercase). ACR does not allow hyphens." -ForegroundColor Red + exit 1 +} + +# Random suffix in the same style as BuildRandomCVM.ps1 +$basename = $basename + -join ((97..122) | Get-Random -Count 5 | % {[char]$_}) +$resgrp = $basename +$aksName = $basename + "aks" +$acrName = $basename + "acr" +$ccPoolName = "ccpool" # 12-char max, lowercase, AMD SEV-SNP node pool +$systemPool = "syspool" + +write-host "----------------------------------------------------------------------------------------------------------------" +write-host "Building AKS cluster '$aksName' with AMD SEV-SNP CC node pool in '$region' (subscription $subsID)" +write-host " System pool : $systemPool 1x $systemVmSize" +write-host " CC pool : $ccPoolName ${ccNodeCount}x $ccVmSize (AMD SEV-SNP)" +write-host " Resource Gp : $resgrp" +if ($smoketest) { write-host "SMOKETEST MODE: Resources will be auto-deleted after the front-end is verified" -ForegroundColor Yellow } +write-host "Script: $scriptName" +write-host "Repository URL: $gitRemoteUrl" +write-host "----------------------------------------------------------------------------------------------------------------" + +# Set subscription context for both Az and az CLI +Set-AzContext -SubscriptionId $subsID | Out-Null +if (!$?) { write-host "Failed to Set-AzContext to $subsID" -ForegroundColor Red; exit 1 } +az account set --subscription $subsID | Out-Null +if (!$?) { write-host "Failed to az account set --subscription $subsID" -ForegroundColor Red; exit 1 } + +$tmp = Get-AzContext +$ownername = $tmp.Account.Id + +# ---------- Pre-flight: SKU + quota check for the CC pool ----------------------------------------- +if ($SkipSkuPreflight) { + write-host "Pre-flight check SKIPPED (-SkipSkuPreflight)." -ForegroundColor Yellow +} else { + write-host "Pre-flight: confirming '$ccVmSize' is available in '$region' with sufficient AMD CVM vCPU quota..." -ForegroundColor Cyan + + # Hard-fail on Intel SGX SKUs - this script targets full-VM CC (SEV-SNP) + if ($ccVmSize -match '^Standard_DC\d+s_v[23]$') { + write-host "ERROR: '$ccVmSize' is an Intel SGX SKU; this script targets AMD SEV-SNP Confidential VM nodes." -ForegroundColor Red + exit 1 + } + if ($ccVmSize -notmatch '^Standard_(DC|EC)\d+a[a-z]*_v\d+$') { + write-host "Warning: '$ccVmSize' does not look like an AMD SEV-SNP CVM SKU (expected DCa*/ECa*v5 family)." -ForegroundColor Yellow + } + + $skuInfo = $null + try { + $skuInfo = Get-AzComputeResourceSku -Location $region -ErrorAction Stop | + Where-Object { $_.ResourceType -eq 'virtualMachines' -and $_.Name -eq $ccVmSize } | + Select-Object -First 1 + } catch { + write-host "Warning: Get-AzComputeResourceSku failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + + if ($null -eq $skuInfo) { + write-host "ERROR: '$ccVmSize' is not offered in '$region'." -ForegroundColor Red + write-host "Find available regions: Get-AzComputeResourceSku | ? { `$_.Name -eq '$ccVmSize' -and -not `$_.Restrictions } | Select Locations" -ForegroundColor Gray + exit 1 + } + + $subRestriction = $skuInfo.Restrictions | Where-Object { + $_.ReasonCode -eq 'NotAvailableForSubscription' -or + ($_.RestrictionInfo -and $_.RestrictionInfo.Locations -contains $region) + } + if ($subRestriction) { + $reason = ($skuInfo.Restrictions | ForEach-Object { $_.ReasonCode }) -join ', ' + write-host "ERROR: '$ccVmSize' is restricted for this subscription in '$region' (reason: $reason)." -ForegroundColor Red + exit 1 + } + + $skuVCpus = ($skuInfo.Capabilities | Where-Object Name -eq 'vCPUs' | Select-Object -First 1).Value -as [int] + if (-not $skuVCpus) { $skuVCpus = 2 } + $needed = $skuVCpus * $ccNodeCount + $skuFamily = $skuInfo.Family + 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 pool needs {5}." -f ` + $skuFamily, $region, $usage.CurrentValue, $usage.Limit, $available, $needed) -ForegroundColor Cyan + if ($available -lt $needed) { + write-host "ERROR: Insufficient AMD CVM vCPU quota in '$skuFamily' / '$region' ($needed needed, $available available)." -ForegroundColor Red + exit 1 + } + } + } catch { + write-host "Warning: Get-AzVMUsage failed: $($_.Exception.Message). Continuing." -ForegroundColor Yellow + } + write-host "Pre-flight passed: '$ccVmSize' available with quota in '$region'." -ForegroundColor Green +} + +# ---------- Resource group ------------------------------------------------------------------------ +$rgTags = @{ + owner = $ownername + BuiltBy = $scriptName + GitRepo = $gitRemoteUrl + Workload = "azure-voting-app" + CCType = "AMD-SEV-SNP" +} +if ($description -ne "") { $rgTags.Add("description", $description) } +if ($smoketest) { $rgTags.Add("smoketest", "true") } + +New-AzResourceGroup -Name $resgrp -Location $region -Tag $rgTags -Force | Out-Null + +# ---------- AKS cluster --------------------------------------------------------------------------- +# Auto-patching strategy (no preview features required, safe defaults that reflect the recommended +# Azure Policies "Kubernetes clusters should have auto-upgrade enabled" and node-image auto-upgrade): +# --auto-upgrade-channel stable cluster K8s version auto-upgrades to stable +# --node-os-upgrade-channel NodeImage node OS images auto-upgrade weekly +# --enable-managed-identity system-assigned MI for the cluster +# --tier standard uptime SLA + financially-backed (cheap insurance) +# We deliberately keep local accounts enabled so 'az aks get-credentials' just works for the demo. +write-host "Creating AKS cluster '$aksName' (this takes ~5 minutes)..." -ForegroundColor Cyan +az aks create ` + --resource-group $resgrp ` + --name $aksName ` + --location $region ` + --node-count 1 ` + --nodepool-name $systemPool ` + --node-vm-size $systemVmSize ` + --os-sku Ubuntu ` + --enable-managed-identity ` + --generate-ssh-keys ` + --auto-upgrade-channel stable ` + --node-os-upgrade-channel NodeImage ` + --tier standard ` + --network-plugin azure ` + --tags owner=$ownername BuiltBy=$scriptName Workload=azure-voting-app ` + --only-show-errors +if ($LASTEXITCODE -ne 0) { write-host "az aks create failed" -ForegroundColor Red; exit 1 } + +# ---------- AMD SEV-SNP Confidential Computing node pool ------------------------------------------ +# AMD SEV-SNP CVM node pools require Ubuntu and a DCa*/ECa* v5 SKU. Secure Boot + vTPM are enabled +# implicitly by the platform when a CVM SKU is selected; no extra flags are needed. +write-host "Adding AMD SEV-SNP CC node pool '$ccPoolName' (${ccNodeCount}x $ccVmSize)..." -ForegroundColor Cyan +az aks nodepool add ` + --resource-group $resgrp ` + --cluster-name $aksName ` + --name $ccPoolName ` + --node-count $ccNodeCount ` + --node-vm-size $ccVmSize ` + --os-sku Ubuntu ` + --mode User ` + --labels workload=confidential sku=amd-sev-snp ` + --tags owner=$ownername CCType=AMD-SEV-SNP ` + --only-show-errors +if ($LASTEXITCODE -ne 0) { write-host "az aks nodepool add failed for CC pool" -ForegroundColor Red; exit 1 } + +# ---------- kubectl access ------------------------------------------------------------------------ +if (-not (Get-Command kubectl -ErrorAction SilentlyContinue)) { + write-host "kubectl not found - installing via 'az aks install-cli'" -ForegroundColor Yellow + az aks install-cli --only-show-errors | Out-Null +} +write-host "Fetching cluster credentials..." -ForegroundColor Cyan +az aks get-credentials --resource-group $resgrp --name $aksName --overwrite-existing --only-show-errors | Out-Null +if ($LASTEXITCODE -ne 0) { write-host "az aks get-credentials failed" -ForegroundColor Red; exit 1 } + +# Sanity: list nodes +kubectl get nodes -o wide +if ($LASTEXITCODE -ne 0) { write-host "kubectl get nodes failed - cluster not reachable" -ForegroundColor Red; exit 1 } + +# ---------- Deploy public Azure Voting App (multi-container) -------------------------------------- +# Source: https://github.com/Azure-Samples/azure-voting-app-redis - public images on mcr.microsoft.com +# We pin the front-end to the CC node pool via nodeSelector so the app actually runs inside SEV-SNP. +$votingManifest = @' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-back + labels: + app: azure-vote-back +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-back + template: + metadata: + labels: + app: azure-vote-back + spec: + nodeSelector: + kubernetes.io/os: linux + containers: + - name: azure-vote-back + image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 6379 + name: redis +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-back +spec: + ports: + - port: 6379 + selector: + app: azure-vote-back +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-front + labels: + app: azure-vote-front +spec: + replicas: 2 + selector: + matchLabels: + app: azure-vote-front + template: + metadata: + labels: + app: azure-vote-front + spec: + nodeSelector: + kubernetes.io/os: linux + workload: confidential + containers: + - name: azure-vote-front + # The previously-published mcr.microsoft.com/azuredocs/azure-vote-front:v1 image was + # removed from MCR. We bootstrap the same Flask app at runtime from the public source + # repo so the sample works first-time without requiring an attached ACR. + image: docker.io/library/python:3.9-slim + command: ["bash","-c"] + args: + - | + set -e + apt-get update -qq && apt-get install -y -qq --no-install-recommends git ca-certificates >/dev/null + rm -rf /src + git clone --depth 1 https://github.com/Azure-Samples/azure-voting-app-redis /src + rm -rf /app && mkdir -p /app + cp -r /src/azure-vote/azure-vote/. /app/ + cd /app + pip install --no-cache-dir flask redis >/dev/null + exec python -c "import sys; sys.path.insert(0,'.'); from main import app; app.run(host='0.0.0.0', port=80)" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + ports: + - containerPort: 80 + env: + - name: REDIS + value: "azure-vote-back" + startupProbe: + httpGet: { path: /, port: 80 } + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 60 + readinessProbe: + httpGet: { path: /, port: 80 } + periodSeconds: 10 + failureThreshold: 6 +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-front +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 80 + selector: + app: azure-vote-front +'@ + +$manifestFile = Join-Path $env:TEMP "azure-vote-$basename.yaml" +$votingManifest | Out-File -FilePath $manifestFile -Encoding utf8 -Force +write-host "Applying voting-app manifest from $manifestFile..." -ForegroundColor Cyan +kubectl apply -f $manifestFile +if ($LASTEXITCODE -ne 0) { write-host "kubectl apply failed" -ForegroundColor Red; exit 1 } + +# ---------- Wait for rollouts --------------------------------------------------------------------- +write-host "Waiting for deployments to become available..." -ForegroundColor Cyan +kubectl rollout status deployment/azure-vote-back --timeout=5m +if ($LASTEXITCODE -ne 0) { write-host "azure-vote-back rollout failed" -ForegroundColor Red; kubectl describe deployment azure-vote-back; exit 1 } +kubectl rollout status deployment/azure-vote-front --timeout=10m +if ($LASTEXITCODE -ne 0) { write-host "azure-vote-front rollout failed" -ForegroundColor Red; kubectl describe deployment azure-vote-front; kubectl get pods -l app=azure-vote-front -o wide; exit 1 } + +# ---------- Wait for LoadBalancer external IP ----------------------------------------------------- +write-host "Waiting for LoadBalancer to allocate a public IP (up to 5 minutes)..." -ForegroundColor Cyan +$externalIP = $null +for ($i = 1; $i -le 30; $i++) { + $externalIP = kubectl get service azure-vote-front -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>$null + if ($externalIP) { break } + Start-Sleep -Seconds 10 + write-host " ...still waiting for external IP (attempt $i/30)" +} +if (-not $externalIP) { + write-host "Timed out waiting for external IP" -ForegroundColor Red + kubectl describe service azure-vote-front + exit 1 +} +write-host "Voting app external IP: $externalIP" -ForegroundColor Green + +# ---------- Smoke test the front-end -------------------------------------------------------------- +write-host "Smoke testing http://$externalIP/ ..." -ForegroundColor Cyan +$ok = $false +for ($i = 1; $i -le 30; $i++) { + try { + $resp = Invoke-WebRequest -Uri "http://$externalIP/" -UseBasicParsing -TimeoutSec 10 + if ($resp.StatusCode -eq 200 -and $resp.Content -match 'Cats|Dogs|Azure Voting App') { + $ok = $true; break + } + } catch { } + Start-Sleep -Seconds 10 + write-host " ...front-end not responding yet (attempt $i/30)" +} +if (-not $ok) { + write-host "Front-end did not respond with expected content within 5 minutes" -ForegroundColor Red + kubectl get pods -o wide + kubectl logs -l app=azure-vote-front --tail=50 + exit 1 +} + +write-host "----------------------------------------------------------------------------------------------------------------" +write-host "SUCCESS: Azure Voting App is live at http://$externalIP/" -ForegroundColor Green +write-host "Cluster : $aksName" +write-host "Resource group : $resgrp" +write-host "CC node pool : $ccPoolName (${ccNodeCount}x $ccVmSize - AMD SEV-SNP)" +write-host "Auto-upgrade : cluster=stable node-os=NodeImage" +write-host "----------------------------------------------------------------------------------------------------------------" + +# ---------- Deploy the runtime-attestation web UI ------------------------------------------------- +# Wraps Azure/cvm-attestation-tools so a user can click "Attest" and see a fresh MAA-signed +# SEV-SNP attestation token with every claim explained. Bootstrapped at pod startup from a +# ConfigMap built from the local ./attestation/ folder (no ACR needed). +write-host "Deploying CC runtime-attestation web UI..." -ForegroundColor Cyan +$attestDir = Join-Path $PSScriptRoot 'attestation' +foreach ($f in @('app.py','config_snp.json','templates/index.html')) { + if (-not (Test-Path (Join-Path $attestDir $f))) { + write-host "Missing $f under $attestDir - skipping attestation deployment." -ForegroundColor Yellow + $attestDir = $null + break + } +} + +if ($attestDir) { + # Build a single ConfigMap with three flat keys (app.py, config_snp.json, index.html). + $cmYaml = kubectl create configmap cc-attest-app ` + --from-file=app.py=(Join-Path $attestDir 'app.py') ` + --from-file=config_snp.json=(Join-Path $attestDir 'config_snp.json') ` + --from-file=index.html=(Join-Path $attestDir 'templates/index.html') ` + --dry-run=client -o yaml + if ($LASTEXITCODE -ne 0) { write-host "Failed to build attestation ConfigMap" -ForegroundColor Red; exit 1 } + $cmFile = Join-Path $env:TEMP "cc-attest-cm-$basename.yaml" + $cmYaml | Out-File -FilePath $cmFile -Encoding utf8 -Force + kubectl apply -f $cmFile | Out-Null + + $attestManifest = @' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cc-attest + labels: { app: cc-attest } +spec: + replicas: 1 + selector: { matchLabels: { app: cc-attest } } + template: + metadata: + labels: { app: cc-attest } + spec: + nodeSelector: + kubernetes.io/os: linux + workload: confidential + containers: + - name: cc-attest + image: docker.io/library/python:3.11-slim + # Privileged + host TPM device passthrough so the upstream tool can read the + # SEV-SNP HCL report from the node vTPM. + securityContext: + privileged: true + env: + - name: POD_NAME + valueFrom: { fieldRef: { fieldPath: metadata.name } } + - name: NODE_NAME + valueFrom: { fieldRef: { fieldPath: spec.nodeName } } + - name: CVM_TOOLS_DIR + value: /opt/cvm-tools/cvm-attestation + ports: + - containerPort: 80 + command: ["bash","-c"] + args: + - | + set -e + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq --no-install-recommends git ca-certificates tpm2-tools >/dev/null + rm -rf /opt/cvm-tools + git clone --depth 1 https://github.com/Azure/cvm-attestation-tools.git /opt/cvm-tools + cd /opt/cvm-tools/cvm-attestation + pip install --no-cache-dir -r requirements.txt >/dev/null + pip install --no-cache-dir flask >/dev/null + mkdir -p /app/templates + cp /etc/attest-app/app.py /app/app.py + cp /etc/attest-app/config_snp.json /app/config_snp.json + cp /etc/attest-app/index.html /app/templates/index.html + exec python /app/app.py + volumeMounts: + - { name: tpmrm, mountPath: /dev/tpmrm0 } + - { name: tpm0, mountPath: /dev/tpm0 } + - { name: securityfs, mountPath: /sys/kernel/security, readOnly: true } + - { name: app, mountPath: /etc/attest-app } + startupProbe: + httpGet: { path: /healthz, port: 80 } + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 60 + readinessProbe: + httpGet: { path: /healthz, port: 80 } + periodSeconds: 10 + failureThreshold: 6 + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: 1, memory: 1Gi } + volumes: + - name: tpmrm + hostPath: { path: /dev/tpmrm0, type: CharDevice } + - name: tpm0 + hostPath: { path: /dev/tpm0, type: CharDevice } + - name: securityfs + hostPath: { path: /sys/kernel/security, type: Directory } + - name: app + configMap: + name: cc-attest-app +--- +apiVersion: v1 +kind: Service +metadata: + name: cc-attest +spec: + type: LoadBalancer + selector: { app: cc-attest } + ports: + - port: 80 + targetPort: 80 +'@ + + $attestManifestFile = Join-Path $env:TEMP "cc-attest-$basename.yaml" + $attestManifest | Out-File -FilePath $attestManifestFile -Encoding utf8 -Force + kubectl apply -f $attestManifestFile + if ($LASTEXITCODE -ne 0) { write-host "kubectl apply failed for attestation manifest" -ForegroundColor Red; exit 1 } + + # Restart deployment so any ConfigMap changes from re-runs are picked up. + kubectl rollout restart deployment/cc-attest | Out-Null + write-host "Waiting for cc-attest rollout (first run installs tpm2-tools + clones upstream)..." -ForegroundColor Cyan + kubectl rollout status deployment/cc-attest --timeout=10m + if ($LASTEXITCODE -ne 0) { + write-host "cc-attest rollout failed" -ForegroundColor Red + kubectl describe deployment cc-attest + kubectl logs -l app=cc-attest --tail=80 + } else { + # Wait for the attestation LoadBalancer. + $attestIP = $null + for ($i = 1; $i -le 30; $i++) { + $attestIP = kubectl get service cc-attest -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>$null + if ($attestIP) { break } + Start-Sleep -Seconds 10 + write-host " ...still waiting for cc-attest external IP (attempt $i/30)" + } + if ($attestIP) { + write-host "----------------------------------------------------------------------------------------------------------------" + write-host "CC Attestation UI live at http://$attestIP/" -ForegroundColor Green + write-host " Click 'Attest' to fetch a fresh MAA-signed SEV-SNP token with every claim explained." + write-host "----------------------------------------------------------------------------------------------------------------" + } else { + write-host "cc-attest LoadBalancer did not get an external IP in time." -ForegroundColor Yellow + } + } +} + +# ---------- Smoketest cleanup -------------------------------------------------------------------- +if ($smoketest) { + write-host "SMOKETEST MODE: Automatically removing all created resources..." -ForegroundColor Yellow + write-host "Resource group: $resgrp" + write-host "WARNING: RESOURCES ARE NOT RECOVERABLE." -ForegroundColor Red + write-host "Press ANY KEY to cancel deletion, or wait 10 seconds to proceed..." -ForegroundColor Yellow + + $timeout = 10 + $timer = [System.Diagnostics.Stopwatch]::StartNew() + $cancelled = $false + while ($timer.Elapsed.TotalSeconds -lt $timeout) { + if ([Console]::KeyAvailable) { [Console]::ReadKey($true) | Out-Null; $cancelled = $true; break } + Start-Sleep -Milliseconds 100 + $remaining = [math]::Ceiling($timeout - $timer.Elapsed.TotalSeconds) + Write-Host "`rDeletion in $remaining seconds... (Press any key to cancel)" -NoNewline -ForegroundColor Yellow + } + $timer.Stop() + if ($cancelled) { + write-host "`nDeletion cancelled. To clean up later: Remove-AzResourceGroup -Name $resgrp -Force" -ForegroundColor Green + } else { + write-host "`nProceeding with resource deletion..." + Remove-AzResourceGroup -Name $resgrp -Force -AsJob | Out-Null + write-host "Resource group deletion initiated in background." -ForegroundColor Green + } +} else { + write-host "" + write-host "Resources created in resource group: $resgrp" + write-host "To clean up: Remove-AzResourceGroup -Name $resgrp -Force" +} + +$myTimeSpan = New-TimeSpan -Start $startTime -End (Get-Date) +Write-Output ("Execution time was {0} minutes and {1} seconds." -f $myTimeSpan.Minutes, $myTimeSpan.Seconds) diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/README.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/README.md new file mode 100644 index 000000000..f7dc2c4af --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/README.md @@ -0,0 +1,84 @@ +# Azure Voting App on AKS with AMD SEV-SNP Confidential Computing nodes + +`Deploy-VotingAppCC.ps1` builds a randomly-named AKS cluster with the **smallest possible AMD +SEV-SNP confidential computing node pool** (2 nodes, `Standard_DC2as_v5`) and deploys the public +multi-container [Azure Voting App](https://github.com/Azure-Samples/azure-voting-app-redis) sample +to it, exposed via a public LoadBalancer. + +The script follows the same conventions as [`vm-samples/BuildRandomCVM.ps1`](../../vm-samples/BuildRandomCVM.ps1): +random 5-letter suffix on the basename, full resource-group tagging (owner, BuiltBy, GitRepo, +description, smoketest), CC SKU + AMD CVM vCPU quota preflight, and an optional `-smoketest` flag +that auto-deletes everything once the front-end is verified. + +## What gets created + +| Resource | Detail | +|----------|--------| +| Resource group | `<5 random letters>` with full tagging | +| AKS cluster | 1x `Standard_D2as_v6` system pool, Azure CNI, managed identity, Standard tier | +| CC node pool | `ccpool` - 2x `Standard_DC2as_v5` (AMD SEV-SNP), labelled `workload=confidential` | +| Auto-patching | `--auto-upgrade-channel stable` + `--node-os-upgrade-channel NodeImage` | +| Voting app | `azure-vote-back` (Redis) + `azure-vote-front` (Flask) pinned to CC nodes | +| Public ingress | `azure-vote-front` Service of type `LoadBalancer` on port 80 | + +The front-end pods are pinned to the CC pool via `nodeSelector: workload=confidential`, so the user- +facing workload actually runs inside AMD SEV-SNP TEEs. + +> **Note on the front-end image.** The previously-published image +> `mcr.microsoft.com/azuredocs/azure-vote-front:v1` was removed from MCR. To keep the sample +> self-contained (no ACR / no role assignments / no `--attach-acr` permissions required), the +> script bootstraps the same Flask app at pod startup from the public +> [`Azure-Samples/azure-voting-app-redis`](https://github.com/Azure-Samples/azure-voting-app-redis) +> repo using the public `python:3.9-slim` image. First rollout takes ~2 minutes per pod +> (apt + pip install); subsequent restarts re-bootstrap the same way. + +## Auto-patching policy choices + +The script reflects the recommended Azure Policies for AKS: + +- **`Kubernetes clusters should have auto-upgrade enabled`** → `--auto-upgrade-channel stable` +- **AKS node OS image auto-upgrade** → `--node-os-upgrade-channel NodeImage` (weekly node-image + refresh, no version drift) +- **`Azure Kubernetes Service Clusters should use managed identities`** → `--enable-managed-identity` +- **Standard tier** for the financially-backed uptime SLA on the API server + +These are all GA features and require no preview registrations, so the deployment works first time +in any standard subscription with quota for the AMD CVM family. + +## Usage + +```powershell +# Smoke test - auto-cleans up after success (10s cancel window) +./Deploy-VotingAppCC.ps1 -subsID -basename sgall -smoketest + +# Persistent deployment in a non-default region +./Deploy-VotingAppCC.ps1 -subsID -basename sgall -region westeurope -description "demo" +``` + +### Parameters + +| Parameter | Default | Notes | +|-----------|---------|-------| +| `-subsID` | _required_ | Target subscription ID | +| `-basename` | _required_ | Lowercase letters / digits only (ACR forbids hyphens). 5 random letters are appended. | +| `-region` | `northeurope` | Must support `Standard_DC2as_v5` | +| `-ccVmSize` | `Standard_DC2as_v5` | Smallest AMD SEV-SNP CVM (2 vCPU / 8 GiB) | +| `-systemVmSize` | `Standard_D2as_v6` | Tiny non-CC system pool | +| `-ccNodeCount` | `2` | Minimum the user requested | +| `-description` | _empty_ | Added as a tag on the resource group | +| `-smoketest` | _off_ | Auto-deletes the resource group after the front-end is verified | +| `-SkipSkuPreflight` | _off_ | Skip SKU/quota check (ARM will validate at deploy time) | + +## Prerequisites + +- Azure PowerShell (`Az`) and Azure CLI (`az`), both signed in to the same subscription +- `kubectl` on PATH (the script will install it via `az aks install-cli` if missing) +- AMD CVM (`standardDCASv5Family`) vCPU quota of **at least 4** in the chosen region + +## Cleanup + +If you didn't use `-smoketest`: + +```powershell +Remove-AzResourceGroup -Name <5 random letters> -Force +``` diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/Dockerfile b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/Dockerfile new file mode 100644 index 000000000..2455d0262 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/Dockerfile @@ -0,0 +1,36 @@ +# Optional pre-built image for the attestation web UI. +# +# This image is NOT required by Deploy-VotingAppCC.ps1 - that script bootstraps +# the same code at pod startup from a ConfigMap so the sample works without an +# attached ACR. Use this Dockerfile if you want to bake the app into a real +# image and push it to your own registry. +# +# Build: docker build -t /cc-attestation-web:1.0 . +# Run: docker run --rm -p 8080:80 --device=/dev/tpmrm0 +FROM python:3.11-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + CVM_TOOLS_DIR=/opt/cvm-tools/cvm-attestation + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + git ca-certificates tpm2-tools \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 https://github.com/Azure/cvm-attestation-tools.git /opt/cvm-tools + +WORKDIR /opt/cvm-tools/cvm-attestation +RUN pip install --no-cache-dir -r requirements.txt + +WORKDIR /app +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +COPY app.py /app/app.py +COPY config_snp.json /app/config_snp.json +COPY templates/ /app/templates/ + +EXPOSE 80 +CMD ["python", "/app/app.py"] diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/README.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/README.md new file mode 100644 index 000000000..c605b3b92 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/README.md @@ -0,0 +1,55 @@ +# Runtime attestation web UI for AKS CC nodes + +A Flask web app that wraps [Azure/cvm-attestation-tools](https://github.com/Azure/cvm-attestation-tools) +to perform a **runtime SEV-SNP attestation** against Microsoft Azure Attestation (MAA) and +present the resulting signed token claims with human-readable explanations. + +Deployed automatically by [`../Deploy-VotingAppCC.ps1`](../Deploy-VotingAppCC.ps1) alongside +the voting app, on the same AMD SEV-SNP node pool. + +## What it does + +When the user clicks **Attest**, the pod: + +1. Reads the AMD SEV-SNP attestation report from the node's vTPM (HCL report) via `/dev/tpmrm0`. +2. POSTs the hardware evidence + a freshly generated nonce to the regional MAA endpoint + (auto-discovered from IMDS). +3. Receives a signed JWT and decodes it. +4. Renders every claim in a table with a plain-English explanation of what it means - SEV-SNP + measurement registers, TCB SVNs, VMPL, REPORT_DATA binding, MAA policy hash, etc. + +Because the JWT is signed by MAA after MAA itself verified the hardware evidence, a green +`x-ms-compliance-status: azure-compliant-cvm` is your end-to-end proof that the workload is +running inside a genuine, policy-compliant AMD SEV-SNP TEE in Azure right now. + +## Requirements at runtime + +| Requirement | Why | +|-------------|-----| +| Pod scheduled on an SEV-SNP node | Otherwise the vTPM doesn't expose an SNP HCL report | +| `/dev/tpmrm0` mounted (privileged) | Upstream tool reads the HCL report via tpm2-tools | +| Egress to `*.attest.azure.net` + `169.254.169.254` (IMDS) | MAA endpoint discovery and call | + +The Deploy script handles all of the above. + +## Building a real image (optional) + +The default deployment uses runtime bootstrap (no registry needed). To bake an image instead: + +```bash +docker build -t /cc-attestation-web:1.0 . +docker push /cc-attestation-web:1.0 +``` + +Then edit the Deployment in `Deploy-VotingAppCC.ps1` to use that image and drop the bootstrap +`command`/`args`. + +## Files + +| File | Purpose | +|------|---------| +| `app.py` | Flask app + JWT decode + claim-explanation dictionary | +| `templates/index.html` | Single-page UI with the **Attest** button and result rendering | +| `config_snp.json` | MAA SEV-SNP config consumed by the upstream client (nonce injected per request) | +| `requirements.txt` | Python deps for the Flask app + upstream client | +| `Dockerfile` | Optional pre-built image | diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/app.py b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/app.py new file mode 100644 index 000000000..425f7e51e --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/app.py @@ -0,0 +1,332 @@ +""" +SEV-SNP / TDX runtime attestation web UI for AKS Confidential Computing nodes. + +Wraps Azure/cvm-attestation-tools (https://github.com/Azure/cvm-attestation-tools) +to perform a guest attestation against Microsoft Azure Attestation (MAA) and +present the resulting JWT claims with human-readable explanations. + +Expected runtime layout (matches Dockerfile and the in-cluster ConfigMap bootstrap): + + /opt/cvm-tools/cvm-attestation/ <- upstream cvm-attestation-tools clone + /app/app.py <- this file + /app/templates/ <- index.html / result.html + /app/config_snp.json <- MAA SEV-SNP config copied here at startup + +The pod must run with access to /dev/tpmrm0 (TPM Resource Manager) so the upstream +client can fetch the HCL report from the vTPM. +""" + +import base64 +import json +import os +import secrets +import sys +import traceback +from datetime import datetime, timezone + +from flask import Flask, jsonify, render_template, request + +# Make the upstream cvm-attestation package importable. +CVM_TOOLS_DIR = os.environ.get("CVM_TOOLS_DIR", "/opt/cvm-tools/cvm-attestation") +if CVM_TOOLS_DIR not in sys.path: + sys.path.insert(0, CVM_TOOLS_DIR) + +# The upstream tool reads its endpoint table from the current working dir. +os.chdir(CVM_TOOLS_DIR) + +from src.attestation_client import ( # noqa: E402 (import after sys.path tweak) + AttestationClient, + AttestationClientParameters, + Verifier, +) +from src.endpoint_selector import EndpointSelector # noqa: E402 +from src.imds_client import ImdsClient # noqa: E402 +from src.isolation import IsolationType # noqa: E402 +from src.logger import Logger # noqa: E402 + + +# --------------------------------------------------------------------------- +# Claim explanations (MAA SEV-SNP and TDX tokens) +# --------------------------------------------------------------------------- +# References: +# https://learn.microsoft.com/azure/attestation/claim-sets +# https://learn.microsoft.com/azure/attestation/claim-rule-grammar +# AMD SEV-SNP ABI specification (publication 56860) +CLAIM_EXPLANATIONS = { + # Standard JWT + "iss": "Issuer - the MAA endpoint that signed this token. Verifying the issuer URL pins the token to a specific Azure Attestation provider.", + "iat": "Issued At (Unix epoch seconds) - when MAA produced the token.", + "exp": "Expiration (Unix epoch seconds) - after this point the token must not be trusted.", + "nbf": "Not Before (Unix epoch seconds) - the token is not valid earlier than this time.", + "jti": "JWT ID - a unique identifier MAA assigns to this token; useful for replay detection.", + + # MAA generic + "x-ms-ver": "MAA token schema version.", + "x-ms-attestation-type": "Type of TEE that produced the evidence ('sevsnpvm', 'tdxvm', 'azurevm', 'tpm', etc.). On AMD CC AKS nodes the OUTER token is 'azurevm' (Azure HCL envelope) and the INNER x-ms-isolation-tee block reports 'sevsnpvm'.", + "x-ms-compliance-status": "MAA's overall verdict against its policy. 'azure-compliant-cvm' means the platform passed all Azure CVM policy checks.", + "x-ms-policy-hash": "SHA-256 of the MAA policy (base64url) that evaluated this evidence. Pin this in your relying party to detect policy drift.", + "x-ms-policy-signer": "If a custom JWS-signed MAA policy was used, this is the signer's certificate chain.", + "x-ms-runtime": "Caller-supplied runtime data that was bound into the hardware report's REPORT_DATA field. For Azure CVMs this also carries the HCL-generated vTPM keys (HCLAkPub, HCLEkPub) and the VM configuration snapshot.", + "x-ms-inittime": "Init-time data bound into the report (rarely used for IaaS CVMs).", + "x-ms-isolation-tee": "Nested sub-token containing the hardware TEE evidence MAA verified. For SEV-SNP this holds every x-ms-sevsnpvm-* field plus its own x-ms-attestation-type and x-ms-compliance-status. This is the cryptographic root of trust for the whole token.", + "nonce": "Caller-supplied nonce echoed back inside x-ms-runtime to prove freshness.", + "secureboot": "true if UEFI Secure Boot was enabled on the guest at attest time.", + + # SEV-SNP specific + "x-ms-sevsnpvm-authorkeydigest": "SHA-384 of the SEV-SNP author key (AKD). Zero on Azure-managed CVMs unless you bring your own ID/Author key.", + "x-ms-sevsnpvm-bootloader-svn": "SVN of the AMD SEV-SNP guest bootloader at launch.", + "x-ms-sevsnpvm-chip-family": "AMD CPU family that produced this report (e.g. 'Milan' = 3rd-gen EPYC, 'Genoa' = 4th-gen EPYC).", + "x-ms-sevsnpvm-chipid": "Per-chip unique identifier (CHIP_ID) burned into the AMD SP. Lets you identify the exact physical socket the CVM is running on.", + "x-ms-sevsnpvm-ciphertext-hiding-dram-enabled": "true if AMD ciphertext-hiding (CT-Hide) DRAM feature is active (mitigates ciphertext side channels).", + "x-ms-sevsnpvm-cxl-allowed": "Whether CXL.mem devices were permitted for this guest at launch.", + "x-ms-sevsnpvm-familyId": "Family ID supplied at SNP_LAUNCH_FINISH (16 bytes). On Azure CVMs this identifies the Azure VM family.", + "x-ms-sevsnpvm-guestsvn": "Guest Security Version Number stamped into the report. Allows monotonic anti-rollback in your policy.", + "x-ms-sevsnpvm-hostdata": "Host data the hypervisor injected at launch (e.g. a SHA-256 of an external policy). Lets you bind the guest to a specific host configuration.", + "x-ms-sevsnpvm-idkeydigest": "SHA-384 of the SEV-SNP ID Key (IDK). On Azure CC nodes this is the Azure-managed launch ID key digest.", + "x-ms-sevsnpvm-imageId": "Image ID supplied at SNP_LAUNCH_FINISH (16 bytes).", + "x-ms-sevsnpvm-is-debuggable": "true means the guest was launched with the SNP debug policy bit set. For production CVMs this MUST be false.", + "x-ms-sevsnpvm-launchmeasurement": "SHA-384 of the initial guest memory contents measured by AMD-SP at launch. This is the cryptographic identity of the boot image that came up; a relying party pins expected values here.", + "x-ms-sevsnpvm-mem-aes256-xts-required": "Whether AES-256-XTS memory encryption was required (vs the older AES-128 mode).", + "x-ms-sevsnpvm-microcode-svn": "Microcode SVN of the AMD CPU at attestation time.", + "x-ms-sevsnpvm-migration-allowed": "true if the SNP guest policy permits migration between machines. Azure CVMs report false.", + "x-ms-sevsnpvm-page-swap-disabled": "Whether host-initiated page swapping of CVM memory is disabled.", + "x-ms-sevsnpvm-rapl-disabled": "Whether the AMD RAPL (Running Average Power Limit) interface is disabled for this guest (mitigates power side-channels).", + "x-ms-sevsnpvm-reportdata": "Hex of REPORT_DATA - 64 bytes the guest itself supplied when requesting the report. The Azure HCL stuffs a TPM-backed runtime hash here so you can cryptographically link the MAA token to a vTPM nonce.", + "x-ms-sevsnpvm-reportid": "Per-launch report ID assigned by AMD-SP. Different across reboots.", + "x-ms-sevsnpvm-singlesocket": "true if the SNP guest policy required a single-socket host.", + "x-ms-sevsnpvm-smt-allowed": "true means simultaneous multithreading was allowed at launch (per SNP guest policy).", + "x-ms-sevsnpvm-snpfw-svn": "SVN of the AMD SEV-SNP firmware (PSP) at attestation time.", + "x-ms-sevsnpvm-tee-svn": "SVN of the TEE component (always 0 for SEV-SNP today; reserved).", + "x-ms-sevsnpvm-vmpl": "Virtual Machine Privilege Level the report was generated at. Azure CVMs run the OS at VMPL0; reports about the OS itself therefore come from VMPL0.", + + # vTPM / HCL + "x-ms-azurevm-attestation-protocol-ver": "Version of the Azure HCL attestation protocol used to build this token.", + "x-ms-azurevm-attested-pcr-values": "Values of the vTPM PCRs that were quoted and signed inside the HCL report. The relying party can match these against known-good measurements.", + "x-ms-azurevm-attested-pcrs": "List of vTPM PCR indices that contributed to the quote.", + "x-ms-azurevm-bootdebug-enabled": "true if the Windows boot debugger was enabled at boot.", + "x-ms-azurevm-dbvalidated": "true if the UEFI Secure Boot 'db' (allowed signers) database is intact and validated.", + "x-ms-azurevm-dbxvalidated": "true if the UEFI Secure Boot 'dbx' (revoked signers) database is intact and validated.", + "x-ms-azurevm-default-securebootkeysvalidated": "MAA confirmed the default Azure Secure Boot keys are present and validated by the HCL.", + "x-ms-azurevm-debuggersdisabled": "Kernel debuggers are disabled.", + "x-ms-azurevm-elam-enabled": "Early Launch Anti-Malware was active during boot (Windows guests).", + "x-ms-azurevm-flightsigning-enabled": "Whether Windows test/flight signing was permitted.", + "x-ms-azurevm-hvci-policy": "Numeric HVCI (Hypervisor-protected Code Integrity) policy state.", + "x-ms-azurevm-hypervisordebug-enabled": "Whether the hypervisor debugger was enabled.", + "x-ms-azurevm-is-windows": "true if the guest was identified as Windows by the HCL.", + "x-ms-azurevm-kerneldebug-enabled": "Whether kernel debugging was enabled at attest time.", + "x-ms-azurevm-osbuild": "Reported guest OS build string.", + "x-ms-azurevm-osdistro": "Reported Linux distribution (e.g. 'Ubuntu', 'Mariner').", + "x-ms-azurevm-ostype": "Linux or Windows.", + "x-ms-azurevm-osversion-major": "Guest OS major version.", + "x-ms-azurevm-osversion-minor": "Guest OS minor version.", + "x-ms-azurevm-signingdisabled": "Whether driver signing enforcement was disabled.", + "x-ms-azurevm-testsigning-enabled": "Whether unsigned/test-signed binaries were permitted.", + "x-ms-azurevm-vmid": "Azure VM ID (GUID) reported by IMDS at attest time. Useful for correlating with control-plane logs.", + + # TDX (in case the workload runs on Intel TDX nodes) + "x-ms-tdxvm-tdreport": "Raw Intel TD-Report fields measured by Intel TDX Module.", + "x-ms-tdxvm-mrtd": "Measurement of the initial TD - the TDX equivalent of LAUNCH_MEASUREMENT.", + "x-ms-tdxvm-rtmrs": "Runtime extendable measurement registers (RTMR0..RTMR3).", +} + + +def _explain(claim_key: str) -> str: + """Return a human explanation for a claim key, or a generic fallback.""" + if claim_key in CLAIM_EXPLANATIONS: + return CLAIM_EXPLANATIONS[claim_key] + if claim_key.startswith("x-ms-sevsnpvm-"): + return "SEV-SNP attestation report field surfaced by MAA. See AMD SEV-SNP ABI spec for the underlying bit layout." + if claim_key.startswith("x-ms-tdxvm-"): + return "Intel TDX attestation field surfaced by MAA." + if claim_key.startswith("x-ms-azurevm-"): + return "Azure HCL / VM-level claim derived from the vTPM-anchored runtime report." + if claim_key.startswith("x-ms-"): + return "MAA-issued claim. Refer to the Microsoft Azure Attestation claim-set documentation." + return "Standard JWT or caller-supplied claim." + + +# --------------------------------------------------------------------------- +# JWT helpers (display only - no signature verification here; MAA already +# verified the hardware evidence and signed the token) +# --------------------------------------------------------------------------- +def _b64url_decode(segment: str) -> bytes: + pad = "=" * (-len(segment) % 4) + return base64.urlsafe_b64decode(segment + pad) + + +def decode_jwt(token): + if isinstance(token, bytes): + token = token.decode("utf-8") + parts = token.split(".") + if len(parts) != 3: + raise ValueError(f"Token does not have 3 segments (got {len(parts)})") + header = json.loads(_b64url_decode(parts[0])) + payload = json.loads(_b64url_decode(parts[1])) + return header, payload + + +def _format_timestamp(value): + try: + return datetime.fromtimestamp(int(value), tz=timezone.utc).isoformat() + except Exception: + return None + + +def annotate_claims(payload: dict): + """Flatten the payload into [{key, value, explanation, timestamp?, section}] rows. + + The MAA 'guest' attestation token for AMD CC AKS nodes nests every SEV-SNP claim + under x-ms-isolation-tee. We surface that subtree as its own section so each + sevsnpvm-* field gets its own explained row instead of being one giant JSON blob. + """ + rows = [] + + def add_row(key, value, section): + row = { + "key": key, + "value": value, + "explanation": _explain(key), + "section": section, + } + if key in {"iat", "exp", "nbf"}: + row["timestamp"] = _format_timestamp(value) + rows.append(row) + + isolation_tee = None + for key, value in payload.items(): + if key == "x-ms-isolation-tee" and isinstance(value, dict): + isolation_tee = value + # Still surface the parent so its purpose is explained. + add_row(key, "(see SEV-SNP / Hardware TEE section below)", "Outer MAA token") + continue + add_row(key, value, "Outer MAA token") + + if isolation_tee is not None: + for key, value in isolation_tee.items(): + add_row(key, value, "x-ms-isolation-tee (SEV-SNP hardware TEE evidence)") + + # Sort: hardware TEE block first, then outer; within each, x-ms-* first. + section_order = { + "x-ms-isolation-tee (SEV-SNP hardware TEE evidence)": 0, + "Outer MAA token": 1, + } + rows.sort(key=lambda r: (section_order.get(r["section"], 2), not r["key"].startswith("x-ms-"), r["key"])) + return rows + + +# --------------------------------------------------------------------------- +# Attestation invocation +# --------------------------------------------------------------------------- +def perform_attestation(user_nonce: str | None): + """Run a guest attestation and return (jwt_token, header, payload, hw_evidence_summary).""" + logger = Logger("cc-attestation-web").get_logger() + + config_path = os.environ.get("ATTEST_CONFIG", "/app/config_snp.json") + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + nonce = user_nonce or secrets.token_hex(16) + config.setdefault("claims", {}).setdefault("user-claims", {})["nonce"] = nonce + + isolation_type = { + "maa_snp": IsolationType.SEV_SNP, + "maa_tdx": IsolationType.TDX, + "maa_trusted_launch": IsolationType.TRUSTED_LAUNCH, + }.get(config.get("attestation_provider"), IsolationType.UNDEFINED) + + # Resolve the regional MAA endpoint from IMDS (matches the upstream CLI). + imds_client = ImdsClient(logger) + region = imds_client.get_region_from_compute_metadata() + if not region: + raise RuntimeError("Unable to read Azure region from IMDS - is this pod on an Azure VM?") + region = region.replace(" ", "").lower() + table = "attestation_uri_table_usgov.json" if "usgov" in region else "attestation_uri_table.json" + endpoint = EndpointSelector(os.path.join(CVM_TOOLS_DIR, table), logger).get_attestation_endpoint( + isolation_type, "guest", region + ) + + params = AttestationClientParameters( + endpoint=endpoint, + verifier=Verifier.MAA, + isolation_type=isolation_type, + claims=config.get("claims"), + api_key=config.get("api_key", ""), + ) + client = AttestationClient(logger, params) + token = client.attest_guest() + if isinstance(token, bytes): + token = token.decode("utf-8") + + header, payload = decode_jwt(token) + + hw_summary = {} + try: + evidence = client.get_hardware_evidence() + if evidence is not None and getattr(evidence, "hardware_report", None): + hw_summary["hardware_report_bytes"] = len(evidence.hardware_report) + hw_summary["hardware_report_sha256"] = __import__("hashlib").sha256( + evidence.hardware_report + ).hexdigest() + if evidence is not None and getattr(evidence, "runtime_data", None): + try: + hw_summary["runtime_data"] = json.loads(evidence.runtime_data) + except Exception: + hw_summary["runtime_data_raw_bytes"] = len(evidence.runtime_data) + except Exception: + # Hardware evidence is best-effort; the token is the source of truth. + pass + + return { + "endpoint": endpoint, + "region": region, + "isolation_type": isolation_type.name if hasattr(isolation_type, "name") else str(isolation_type), + "nonce": nonce, + "token": token, + "header": header, + "payload": payload, + "claims": annotate_claims(payload), + "hardware_evidence": hw_summary, + } + + +# --------------------------------------------------------------------------- +# Flask app +# --------------------------------------------------------------------------- +app = Flask(__name__) + + +@app.route("/", methods=["GET"]) +def index(): + return render_template( + "index.html", + node_name=os.environ.get("NODE_NAME", ""), + pod_name=os.environ.get("POD_NAME", ""), + ) + + +@app.route("/healthz", methods=["GET"]) +def healthz(): + return "ok", 200 + + +@app.route("/api/attest", methods=["POST"]) +def api_attest(): + user_nonce = (request.json or {}).get("nonce") if request.is_json else request.form.get("nonce") + try: + result = perform_attestation(user_nonce or None) + except Exception as exc: + return ( + jsonify( + { + "ok": False, + "error": str(exc), + "trace": traceback.format_exc(), + } + ), + 500, + ) + return jsonify({"ok": True, **result}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "80"))) diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/config_snp.json b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/config_snp.json new file mode 100644 index 000000000..dfbb73418 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/config_snp.json @@ -0,0 +1,11 @@ +{ + "attestation_url": "", + "attestation_provider": "maa_snp", + "api_key": "", + "enable_metrics": false, + "claims": { + "user-claims": { + "nonce": "placeholder-replaced-per-request" + } + } +} diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/manifest.yaml b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/manifest.yaml new file mode 100644 index 000000000..858a293f2 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/manifest.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cc-attest + labels: { app: cc-attest } +spec: + replicas: 1 + selector: { matchLabels: { app: cc-attest } } + template: + metadata: + labels: { app: cc-attest } + spec: + nodeSelector: + kubernetes.io/os: linux + workload: confidential + containers: + - name: cc-attest + image: docker.io/library/python:3.11-slim + securityContext: + privileged: true + env: + - name: POD_NAME + valueFrom: { fieldRef: { fieldPath: metadata.name } } + - name: NODE_NAME + valueFrom: { fieldRef: { fieldPath: spec.nodeName } } + - name: CVM_TOOLS_DIR + value: /opt/cvm-tools/cvm-attestation + ports: + - containerPort: 80 + command: ["bash","-c"] + args: + - | + set -e + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq --no-install-recommends git ca-certificates tpm2-tools >/dev/null + rm -rf /opt/cvm-tools + git clone --depth 1 https://github.com/Azure/cvm-attestation-tools.git /opt/cvm-tools + cd /opt/cvm-tools/cvm-attestation + pip install --no-cache-dir -r requirements.txt >/dev/null + pip install --no-cache-dir flask >/dev/null + mkdir -p /app/templates + cp /etc/attest-app/app.py /app/app.py + cp /etc/attest-app/config_snp.json /app/config_snp.json + cp /etc/attest-app/index.html /app/templates/index.html + exec python /app/app.py + volumeMounts: + - { name: tpmrm, mountPath: /dev/tpmrm0 } + - { name: tpm0, mountPath: /dev/tpm0 } + - { name: securityfs, mountPath: /sys/kernel/security, readOnly: true } + - { name: app, mountPath: /etc/attest-app } + startupProbe: + httpGet: { path: /healthz, port: 80 } + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 60 + readinessProbe: + httpGet: { path: /healthz, port: 80 } + periodSeconds: 10 + failureThreshold: 6 + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: 1, memory: 1Gi } + volumes: + - name: tpmrm + hostPath: { path: /dev/tpmrm0, type: CharDevice } + - name: tpm0 + hostPath: { path: /dev/tpm0, type: CharDevice } + - name: securityfs + hostPath: { path: /sys/kernel/security, type: Directory } + - name: app + configMap: + name: cc-attest-app +--- +apiVersion: v1 +kind: Service +metadata: + name: cc-attest +spec: + type: LoadBalancer + selector: { app: cc-attest } + ports: + - port: 80 + targetPort: 80 diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/requirements.txt b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/requirements.txt new file mode 100644 index 000000000..6cf394144 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/requirements.txt @@ -0,0 +1,6 @@ +flask>=2.3 +requests +click +pyjwt +cryptography +construct diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/templates/index.html b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/templates/index.html new file mode 100644 index 000000000..b6a8ef039 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/attestation/templates/index.html @@ -0,0 +1,203 @@ + + + + + AKS CC - Runtime Attestation + + + + +
+ +

AKS Confidential Computing - Runtime Attestation

+

This pod is running on an AMD SEV-SNP confidential node. Click Attest to fetch a fresh hardware-rooted attestation token from Microsoft Azure Attestation (MAA) and inspect the claims.

+
+
+
+

Pod: {{ pod_name or 'unknown' }}   Node: {{ node_name or 'unknown' }}

+

The attestation flow uses Azure/cvm-attestation-tools to:

+
    +
  1. Read the AMD SEV-SNP attestation report from the node's vTPM (HCL report).
  2. +
  3. Send the hardware evidence + a fresh nonce to the regional MAA endpoint.
  4. +
  5. Receive a signed JWT whose claims describe the TEE state at this exact moment.
  6. +
+

+ + +

+
+ +
+
+ + + + diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/vote-front-patch.yaml b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/vote-front-patch.yaml new file mode 100644 index 000000000..b9f710b73 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/azure-voting-app/vote-front-patch.yaml @@ -0,0 +1,36 @@ +spec: + replicas: 2 + template: + spec: + containers: + - name: azure-vote-front + image: docker.io/library/python:3.9-slim + command: ["bash","-c"] + args: + - | + set -e + apt-get update -qq && apt-get install -y -qq --no-install-recommends git ca-certificates >/dev/null + rm -rf /src + git clone --depth 1 https://github.com/Azure-Samples/azure-voting-app-redis /src + rm -rf /app && mkdir -p /app + cp -r /src/azure-vote/azure-vote/. /app/ + cd /app + pip install --no-cache-dir flask redis >/dev/null + exec python -c "import sys; sys.path.insert(0,'.'); from main import app; app.run(host='0.0.0.0', port=80)" + env: + - name: REDIS + value: azure-vote-back + ports: + - containerPort: 80 + startupProbe: + httpGet: { path: /, port: 80 } + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 60 + readinessProbe: + httpGet: { path: /, port: 80 } + periodSeconds: 10 + failureThreshold: 6 + resources: + requests: { cpu: 100m, memory: 128Mi } + limits: { cpu: 500m, memory: 512Mi } diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/cvm-attestation-pod.yaml b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/cvm-attestation-pod.yaml deleted file mode 100644 index c749805e3..000000000 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/resources/cvm-attestation-pod.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: cvm-attestation - labels: - app: cvm-attestation -spec: - containers: - - image: mcr.microsoft.com/acc/samples/cvm-attestation:1.1 - imagePullPolicy: IfNotPresent - name: cvm-attestation - volumeMounts: - - mountPath: /sys/kernel/security - name: tcg - - mountPath: /dev/tpmrm0 - name: tpmrm0 - securityContext: - privileged: true - nodeSelector: - kubernetes.azure.com/security-type: ConfidentialVM - volumes: - - name: tcg - hostPath: - path: /sys/kernel/security - - name: tpmrm0 - hostPath: - path: /dev/tpmrm0 - restartPolicy: Never diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/solution-05.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/solution-05.md index 48c2057d5..b6d696aaa 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/solution-05.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-05/solution-05.md @@ -1,420 +1,235 @@ -# Walkthrough Challenge 5 - Encryption in use with Confidential VMs/Node Pools in Azure Kubernetes Service (AKS) +# Walkthrough Challenge 5 - Azure Voting App on confidential AKS nodes -[Previous Challenge Solution](../challenge-04/solution-04.md) - **[Home](../../Readme.md)** - [Next Challenge Solution](../challenge-06/solution-06.md) +[Previous walkthrough](../challenge-04/solution-04.md) - **[Home](../../Readme.md)** - [Challenge](../../challenges/challenge-05.md) -**Estimated Duration:** 90-120 minutes +**Estimated duration:** 30-45 minutes -> 💡 **Objective:** Learn how to deploy and validate guest attestation on Azure Confidential VMs in AKS to ensure business logic only executes in trusted, hardware-backed confidential computing environments. You will create an AKS cluster using Azure CLI, add a Confidential VM node pool, deploy attestation workloads, and verify cryptographic proof of node integrity before processing sensitive operations. +## Objective ---- - -## Prerequisites - -Please ensure that you successfully verified the [General prerequisites](../../Readme.md#general-prerequisites) before continuing with this challenge. - -- Azure subscription with Contributor permissions on your resource group -- Azure CLI >= 2.54 or access to Azure Portal -- **Linux/Bash environment** — Azure Cloud Shell (Bash), WSL2 on Windows, or a native Linux/macOS terminal -- `kubectl` command-line tool (can be installed via `az aks install-cli`) -- Basic understanding of Kubernetes concepts (pods, deployments, node pools) -- Basic understanding of confidential computing concepts - -## Scenario Context - -You are a cloud security engineer at a European healthcare organization that processes sensitive patient data in containerized applications. Your organization has adopted Kubernetes (AKS) for container orchestration but must comply with strict data protection regulations including GDPR and healthcare privacy requirements. - -Your mandate includes: - -- **Encryption in Use**: Protect data while it's being processed in containers, not just at rest or in transit -- **Hardware-Based Trust**: Ensure security guarantees are rooted in hardware (AMD SEV-SNP), not just software isolation -- **Attestation for Workloads**: Verify that containerized applications run only on genuine confidential computing nodes -- **Zero Trust for Kubernetes**: Validate the integrity of worker nodes before deploying sensitive workloads -- **Compliance and Auditability**: Provide cryptographic evidence that workloads execute in compliant infrastructure - -In this challenge, you'll deploy Azure Confidential Computing node pools in AKS using Azure CLI. You'll configure a confidential VM node pool with AMD SEV-SNP technology, deploy attestation-aware workloads, and verify that containers run in hardware-protected environments before processing sensitive operations. - -### Understanding Confidential VMs in AKS - -Azure Kubernetes Service (AKS) supports Confidential VM node pools, which provide: - -- **Hardware-based memory encryption** - AMD SEV-SNP encrypts node memory at the hardware level -- **Node attestation** - Cryptographic proof that worker nodes run on genuine confidential hardware -- **Workload isolation** - Containers benefit from the underlying confidential VM protections -- **Integration with existing Kubernetes workflows** - Use standard Kubernetes constructs (node selectors, taints/tolerations) - -**Security Architecture:** - -This setup implements defense-in-depth security: -- **Confidential VM Node Pool** - Worker nodes with AMD SEV-SNP hardware encryption -- **Attestation Verification** - Pods can verify they're running on confidential nodes -- **Node Selectors** - Target sensitive workloads to confidential node pools only -- **Microsoft Azure Attestation (MAA)** - Validates node integrity and issues signed JWT tokens - -### Learning Resources - -- [Azure Confidential Computing overview](https://azure.microsoft.com/solutions/confidential-compute/) -- [Confidential VMs on Azure](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) -- [AKS with Confidential Computing](https://learn.microsoft.com/azure/aks/use-confidential-computing) -- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/) - -### Original Source Materials - -This challenge is based on the **AKS with Confidential Computing Linux Sample** from the Microsoft Azure Confidential Computing repository: - -- **Main Repository**: [Azure Confidential Computing CVM Guest Attestation](https://github.com/Azure/confidential-computing-cvm-guest-attestation) -- **Source Module**: [AKS Linux Sample](https://github.com/Azure/confidential-computing-cvm-guest-attestation/tree/main/aks-linux-sample) - -The AKS deployment patterns and attestation verification workflows have been adapted for this MicroHack challenge to provide a guided learning experience with Confidential VM node pools in Azure Kubernetes Service. - ---- - -## Task 1: Configure Environment and Register Preview Features - -💡 **Set up your environment variables and register the required Azure preview features for Confidential VM support in AKS.** - -### Step 1: Configure Environment Variables +Use the newer Azure Voting App automation to deploy an AKS cluster with an AMD +SEV-SNP node pool. The script also deploys a runtime attestation UI, allowing +you to verify the node from inside a pod before trusting the workload. > [!IMPORTANT] -> The Azure CLI commands in this walkthrough use **bash** syntax and will not work directly in PowerShell. Use **Azure Cloud Shell (Bash)** for the best experience. If running locally on Windows, use **WSL2** (Windows Subsystem for Linux) to run a bash shell. You can install the Azure CLI inside WSL with: -> -> ```bash -> curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash -> ``` +> **Execution environment.** Like Challenge 4, this challenge runs in a **local +> PowerShell 7+ session**, not in Azure Cloud Shell. It requires `kubectl` access +> and both Azure command-line tools signed in locally. -### Linux/Bash - -```bash -# Set common variables -# Customize ATTENDEE_ID for each participant -RESOURCE_GROUP="labuser-xx" # Change this for each participant (e.g., labuser-01, labuser-02,...) +## Prerequisites -ATTENDEE_ID="${RESOURCE_GROUP}" +- The [general MicroHack prerequisites](../../Readme.md#general-prerequisites). +- PowerShell 7 or later, running locally. +- Azure CLI signed in to the target subscription. +- Azure PowerShell (`Az`) signed in to the same subscription. +- Contributor access to the attendee resource group. +- At least 4 available `standardDCASv5Family` vCPUs in the selected region for the default two-node pool. +- Permission to create AKS clusters and public LoadBalancer services. +- `kubectl` on PATH. The script installs it with `az aks install-cli` if missing. -# Generate a unique random hash suffix (different on each run) -HASH_SUFFIX=$(echo -n "$ATTENDEE_ID-$(date +%s)-$RANDOM" | md5sum | cut -c1-6) - -LOCATION="northeurope" -AKS_CLUSTER_NAME="aks-cvmcluster$HASH_SUFFIX" -DNS_LABEL="cvmcluster$HASH_SUFFIX" -ADMIN_USERNAME="azureuser" -KEYVAULT_NAME="kv-cc-${HASH_SUFFIX}" # Must be globally unique -SSH_KEY_NAME="cc-${ATTENDEE_ID}-key" -ATTESTATION_NAME="attest${HASH_SUFFIX}" -``` +The current confidential AKS capability is GA. The old `aks-preview` extension +and `AzureLinuxCVMPreview` feature registration are not used. > [!WARNING] -> If your Azure Cloud Shell session times out (e.g. during a break), the variables defined above will be lost and must be re-defined before continuing. We recommend saving them in a local text file on your machine so you can quickly copy and paste them back into a new session. +> **This challenge needs two separate sign-ins.** The Azure CLI (`az`) and the +> Az PowerShell module maintain **independent** credential stores. The script +> calls `az account show` to read the subscription **and** `Set-AzContext` to set +> the PowerShell context, so `az login` on its own is not sufficient. +> +> If you completed Challenges 1-4 you are signed in to the Azure CLI only, and +> the script fails at `Set-AzContext` with +> `SharedTokenCacheCredential authentication unavailable`. -### Step 2: Install Required AKS Extensions +### Sign in to both tools and verify -```bash -# Install / Update `aks-preview` extension -az extension add --name aks-preview -az extension update --name aks-preview +```powershell +# 1. Install the Az PowerShell module if it is not already present +Get-Module -ListAvailable Az.Accounts, Az.Compute +Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force -# Register the `AzureLinuxCVMPreview` feature flag -az feature register --namespace "Microsoft.ContainerService" --name "AzureLinuxCVMPreview" +# 2. Sign in to Az PowerShell. This is separate from 'az login'. +# Pass -TenantId when your machine has signed in to more than one tenant. +Connect-AzAccount -TenantId -# Verify the registration status - status should show "Registered" -az feature show --namespace Microsoft.ContainerService --name AzureLinuxCVMPreview +# 3. Point both tools at the same subscription +Set-AzContext -SubscriptionId +az account set --subscription -# Refresh the registration of the `Microsoft.ContainerService` resource provider -az provider register --namespace Microsoft.ContainerService +# 4. Confirm the subscription IDs match +Get-AzContext | Select-Object Name, Subscription, Tenant +az account show --query "{sub:name, id:id, tenant:tenantId}" --output table ``` -> [!WARNING] -> For all Microsoft-hosted events, these features and providers below are already registered. Ignore any error messages due to lack of permissions. - ---- +> [!NOTE] +> The script uses `Get-AzComputeResourceSku` and `Get-AzVMUsage` for its SKU and +> quota preflight, which is why the Az module is required rather than optional. -## Task 2: Create Azure Kubernetes Service cluster +### Check for a conflicting `aks-preview` extension -💡 **Deploy the foundational AKS cluster with a standard system node pool. You'll add the Confidential VM node pool in the next task.** +If the `aks-preview` Azure CLI extension is installed - for example from an +earlier AKS lab - it intercepts every `az aks` command. A version that predates +your current Azure CLI fails to load and stops the deployment. This challenge +does not use `aks-preview`. -### Step 1: Create AKS cluster +```powershell +az extension list --output table -```bash -# Create an AKS cluster -az aks create --resource-group $RESOURCE_GROUP --name $AKS_CLUSTER_NAME --node-count 1 --location $LOCATION --generate-ssh-keys --node-vm-size Standard_D2s_v5 +# Remove it if listed +az extension remove --name aks-preview -# Connect to the cluster -az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_CLUSTER_NAME +# Confirm 'az aks' works again (an empty list is a valid result) +az aks list --output table ``` -At this point, you should see an AKS cluster in your resource group: - -![AKS](./images//aks.png) - ---- - -## Task 3: Add Confidential VM Node Pool - -💡 **Create a dedicated node pool with Confidential VM compute resources for running sensitive workloads.** - -### Step 1: Add the Confidential Node Pool - -```bash -az aks nodepool add --resource-group $RESOURCE_GROUP --cluster-name $AKS_CLUSTER_NAME --name cvmnodepool --node-count 1 --node-vm-size Standard_DC2as_v5 -``` +> [!NOTE] +> Challenge 5 does not use the Confidential VM Orchestrator enterprise +> application. That identity is required by the customer-managed confidential +> OS disk path demonstrated in Challenge 5.5. See +> [when Confidential VM Orchestrator is required](CVM-ORCHESTRATOR.md). -At this point, you should see another AKS node pool in your cluster: - -![AKS](./images//aks-02.png) - ---- - -## Task 4: Verify Confidential Node Pool Configuration +> [!IMPORTANT] +> The automation creates the AKS cluster in the existing attendee resource +> group. Cleanup deletes the cluster, not the resource group. -💡 **Validate that the Confidential VM node pool is properly configured and running with the correct VM size and image.** +## Task 1: Configure the MicroHack environment -### Step 1: Verify VM Size and Image +Challenge 5 uses the same PowerShell environment variables as Challenge 4. If +the session still contains them, reuse the existing values. -```bash -# Verify that the node pool uses a Confidential VM -az aks nodepool show --resource-group $RESOURCE_GROUP --cluster-name $AKS_CLUSTER_NAME --name cvmnodepool --query 'vmSize' +```powershell +$env:RESOURCE_GROUP = "labuser-xx" +$env:ATTENDEE_ID = $env:RESOURCE_GROUP +$env:LOCATION = "northeurope" -# Verify that the node pool uses a Confidential VM image -az aks nodepool list --resource-group $RESOURCE_GROUP --cluster-name $AKS_CLUSTER_NAME --query "[?name=='cvmnodepool'].nodeImageVersion" -o tsv +# Run only when HASH_SUFFIX is not already set. +$seed = "$($env:ATTENDEE_ID)-$(Get-Random)-$(Get-Random)" +$bytes = [Text.Encoding]::UTF8.GetBytes($seed) +$hash = [Security.Cryptography.MD5]::HashData($bytes) +$env:HASH_SUFFIX = [Convert]::ToHexString($hash).Substring(0, 8).ToLowerInvariant() ``` ---- - -## Task 5: Deploy and Verify Attestation Pod +> [!NOTE] +> These are PowerShell environment variables (`$env:` prefix), not the Bash +> variables used in Challenges 1-3. The script exits immediately if any of +> `RESOURCE_GROUP`, `ATTENDEE_ID`, `LOCATION`, or `HASH_SUFFIX` is missing. -💡 **Deploy a sample pod that retrieves attestation data to prove it's running on a confidential VM node.** +## Task 2: Run the automated deployment -### Step 1: Deploy the Attestation Pod +From this walkthrough directory, run: -1. The attestation pod YAML file is located at `walkthrough/challenge-5/resources/cvm-attestation-pod.yaml` in this repository (if using Cloud Shell, upload the file via the **Manage files** menu option). - -2. Apply the YAML file using the file from the repository: - -```bash -kubectl apply -f cvm-attestation-pod.yaml +```powershell +./Deploy-VotingAppCC.ps1 -Deploy ``` -2. Check pod status: +The script starts by confirming the cluster plan, then runs a SKU and quota +preflight before creating the AKS cluster: -```bash -kubectl get pods -``` +![PowerShell output showing the cluster plan (system pool, CC pool, resource group), the Standard_DC2as_v5 preflight passing with quota available in northeurope, and the AKS cluster creation beginning](./images/01-deploy-start-preflight.png) -![AKS](./images//aks-03.png) +The script automatically: -3. Get the attestation report by checking logs: +1. Checks that `Standard_DC2as_v5` is available in `$env:LOCATION`. +2. Creates a managed-identity AKS cluster with stable Kubernetes and node-image upgrades. +3. Adds two `Standard_DC2as_v5` nodes labelled `workload=confidential`. +4. Acquires cluster credentials and deploys the Azure Voting App. +5. Creates the attestation ConfigMap and deploys the runtime attestation UI. +6. Waits for both rollouts and LoadBalancer IP addresses. -```bash -kubectl logs cvm-attestation -``` +> [!NOTE] +> **Expect roughly 10 minutes.** Cluster creation alone takes about five +> minutes, the confidential node pool adds several more, and the LoadBalancer +> can take up to five minutes to receive a public IP. The script retries the +> front-end smoke test up to 30 times, so early "not responding yet" messages +> are expected rather than a failure. -### Expected Output - -The attestation report will contain JWT tokens with confidential computing verification data: - -```json -{ - "alg": "RS256", - "jku": "https://sharedeus2.eus2.attest.azure.net/certs", - "kid": "J0pAPdfXXHqWWimgrH853wMIdh5/fLe1z6uSXYPXCa0=", - "typ": "JWT" -} -{ - "exp": 1663376286, - "iat": 1663347486, - "iss": "https://sharedeus2.eus2.attest.azure.net", - "jti": "89a500344d9ecc081b14ff6c848fbc1d557694946e6f8d83687654a1139e055d", - "nbf": 1663347486, - "secureboot": true, - "x-ms-attestation-type": "azurevm", - "x-ms-azurevm-attestation-protocol-ver": "2.0", - "x-ms-azurevm-attested-pcrs": [0, 1, 2, 3, 4, 5, 6, 7], - "x-ms-azurevm-bootdebug-enabled": false, - "x-ms-azurevm-dbvalidated": true, - "x-ms-azurevm-dbxvalidated": true, - "x-ms-azurevm-debuggersdisabled": true, - "x-ms-azurevm-default-securebootkeysvalidated": true, - "x-ms-azurevm-elam-enabled": false, - "x-ms-azurevm-flightsigning-enabled": false, - "x-ms-azurevm-hvci-policy": 0, - "x-ms-azurevm-hypervisordebug-enabled": false, - "x-ms-azurevm-is-windows": false, - "x-ms-azurevm-kerneldebug-enabled": false, - "x-ms-azurevm-osbuild": "NotApplication", - "x-ms-azurevm-osdistro": "Ubuntu", - "x-ms-azurevm-ostype": "Linux", - "x-ms-azurevm-osversion-major": 18, - "x-ms-azurevm-osversion-minor": 4, - "x-ms-azurevm-signingdisabled": true, - "x-ms-azurevm-testsigning-enabled": false, - "x-ms-azurevm-vmid": "A80B7FE7-5B93-4027-9971-6CCEE468C2B3", - "x-ms-isolation-tee": { - "x-ms-attestation-type": "sevsnpvm", - "x-ms-compliance-status": "azure-compliant-cvm", - "x-ms-runtime": { - "keys": [ - { - "e": "AQAB", - "key_ops": ["encrypt"], - "kid": "HCLAkPub", - "kty": "RSA", - "n": "2I-ayAABWYhQU-D81quVW4i1sH14-Offul2U2LwsgtihxykIzXY_5YzQAY4e56GMZSpm5r6telRr5rnFJa8iklzol7ecYZEX1nc1WK51a68E2kZNyomFVSIlDPJCn14NpRoxuipIfhe16zWVYZ8dpYbpelyzHZZpskdBLnUKldffUYliWSXLBpjPb89VV0FYxKPi_bSGviBXWOiRtcITRcXfpjlfD3DgZqlK4gj11RChqaEYG_GAPlxceu5h1pusgLuPEULWzvkKuGw7j8ZrxdYEUNB-uHU0nxuQvYxtksPs3zX6ELcV2GjwJupzYUUAu95OQUGI-soDWKvIXM4epw" - } - ], - "vm-configuration": { - "console-enabled": true, - "current-time": 1662691445, - "secure-boot": true, - "tpm-enabled": true, - "vmUniqueId": "A80B7FE7-5B93-4027-9971-6CCEE468C2B3" - } - }, - "x-ms-sevsnpvm-authorkeydigest": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "x-ms-sevsnpvm-bootloader-svn": 2, - "x-ms-sevsnpvm-familyId": "01000000000000000000000000000000", - "x-ms-sevsnpvm-guestsvn": 2, - "x-ms-sevsnpvm-hostdata": "0000000000000000000000000000000000000000000000000000000000000000", - "x-ms-sevsnpvm-idkeydigest": "57486a447ec0f1958002a22a06b7673b9fd27d11e1c6527498056054c5fa92d23c50f9de44072760fe2b6fb89740b696", - "x-ms-sevsnpvm-imageId": "02000000000000000000000000000000", - "x-ms-sevsnpvm-is-debuggable": false, - "x-ms-sevsnpvm-launchmeasurement": "ad6de16ac59ee52351c6038df58d1be5aeaf41cd0f7c81b2279ecca0df6ef43a2b69d663ad6973d6dbb9db0ffd7a9023", - "x-ms-sevsnpvm-microcode-svn": 93, - "x-ms-sevsnpvm-migration-allowed": false, - "x-ms-sevsnpvm-reportdata": "d707bdbeeeb6c6e7fa42e94e71ec537e21c8d4c4316422c4011742f55ecc22c00000000000000000000000000000000000000000000000000000000000000000", - "x-ms-sevsnpvm-reportid": "afc01d4d5f22974bd00c2d993bc3354fcd3bf37c789c2611233da72df1712d82", - "x-ms-sevsnpvm-smt-allowed": true, - "x-ms-sevsnpvm-snpfw-svn": 6, - "x-ms-sevsnpvm-tee-svn": 0, - "x-ms-sevsnpvm-vmpl": 0 - }, - "x-ms-policy-hash": "wm9mHlvTU82e8UqoOy1Yj1FBRSNkfe99-69IYDq9eWs", - "x-ms-runtime": { - "client-payload": { - "nonce": "MTIzNA==" - }, - "keys": [ - { - "e": "AQAB", - "key_ops": ["encrypt"], - "kid": "TpmEphemeralEncryptionKey", - "kty": "RSA", - "n": "peWMfgAALfH53tQC-noqUvYLgycL8K9Ejn7mKKDJwu7hdrrfydinD04burg83WANTGOKO4OHiNieJf4SiGmxZQyLym6gJr4m0bGbsMt4NM6dXXVmRZZSkCp4hn_2XL6aMOnnn0YNOXg6zmRmOeRu4rgkOA_WCd8YE23k7wp0twZG0VCgVmUUr2LD_xwqLLsukoDG8_b38QJmkh78Vz6BGLIA9-qgG5fpBGVoERWe1CCC1aH7bkKhKtNPSD0x6EbfxCfe4dU_Adg6xdxuaDEK9mcfxZWz56cevmlc44SapFm00iSYeWmyoyqlZUJ6mr-1P-DYataNHZPZr8mz2wDAgQ" - } - ] - }, - "x-ms-ver": "1.0" -} -``` +A successful run ends with both rollouts complete and both public URLs printed: -### Key Attestation Fields +![PowerShell output listing three Ready nodes (two ccpool, one syspool), the voting app rollout succeeding with external IP, and the CC attestation UI URL, followed by the cleanup reminder and total execution time](./images/02-deployment-complete-urls.png) -The attestation report contains important security validation fields: +Use `-CcNodeCount 1` when the event design calls for a single confidential node +and the reduced resiliency is acceptable. The script prints both public URLs; +it does not open a browser automatically. -- **secureboot**: `true` - Secure Boot is enabled -- **x-ms-azurevm-attestation-protocol-ver**: Version of attestation protocol -- **x-ms-azurevm-attested-pcrs**: Platform Configuration Registers that were measured -- **x-ms-isolation-tee**: Trusted Execution Environment details -- **x-ms-compliance-status**: `"azure-compliant-cvm"` - Confirms Azure Confidential VM compliance -- **x-ms-sevsnpvm-is-debuggable**: `false` - VM is not debuggable (production setting) -- **x-ms-sevsnpvm-launchmeasurement**: Launch measurement for integrity verification +## Task 3: Verify workload placement ---- - -## Task 6: Clean Up Resources - -💡 **Delete all resources to avoid ongoing charges.** - -### Step 1: Delete Resource Group - -```bash -az group delete --name $RESOURCE_GROUP --yes --no-wait +```powershell +kubectl get nodes --show-labels +kubectl get pods --output wide +kubectl get deployment azure-vote-front --output jsonpath='{.spec.template.spec.nodeSelector}' ``` -⚠️ **Warning**: This command will permanently delete all resources in the resource group including the AKS cluster, node pools, and all associated resources. - ---- - -## Key Takeaways - -In this challenge, you successfully deployed and validated Azure Confidential Computing in AKS with guest attestation. Here are the key concepts and best practices: - -### Confidential Computing in Kubernetes - -✅ **Node-Level Encryption** - Confidential VM node pools provide hardware-based memory encryption (AMD SEV-SNP) for all containers running on those nodes +Confirm that `azure-vote-front` and `cc-attest` run on nodes with the +`workload=confidential` label. The Redis back end may run on the system pool. -✅ **Workload Targeting** - Use Kubernetes node selectors to schedule sensitive workloads exclusively on confidential node pools +## Task 4: Exercise the applications -✅ **Transparent Integration** - Applications benefit from confidential computing protections without code changes (though attestation-aware apps provide stronger guarantees) +The script prints both public URLs. -### Attestation in Containerized Environments +1. Open the Azure Voting App and submit several votes. +2. Open the attestation UI and select **Attest**. +3. Confirm the MAA token reports `sevsnpvm` and `azure-compliant-cvm`. +4. Confirm `x-ms-sevsnpvm-is-debuggable` is `false`. +5. Review the launch measurement and policy hash as cryptographic evidence. -✅ **Node Attestation** - Containers can query the Instance Metadata Service to retrieve attestation tokens proving they run on confidential hardware +The attestation UI names the pod and the confidential node it runs on, then +displays the verified token: -✅ **Runtime Verification** - Applications should verify attestation at startup before processing sensitive data +![Runtime attestation UI in a browser showing the pod and node names, the MAA endpoint and nonce, an outer envelope of azurevm with inner TEE sevsnpvm, an azure-compliant-cvm verdict, and the start of the x-ms-isolation-tee claims table](./images/03-aks-attestation-result.png) -✅ **JWT Token Validation** - Attestation tokens are signed by Microsoft Azure Attestation and contain claims about hardware security features +> [!NOTE] +> The token has two layers. The **outer** `azurevm` envelope is issued by the +> Azure HCL and carries VM-level claims such as the vTPM PCR quote and Secure +> Boot state. The **inner** `x-ms-isolation-tee` sub-token carries the SEV-SNP +> hardware evidence that MAA verified. Seeing `azurevm` as the outer type is +> expected on AKS confidential nodes - check the inner block for `sevsnpvm`. -### AKS Architecture and Security +The attestation pod reads the node vTPM through `/dev/tpmrm0`, calls MAA, and +renders the signed claims. In production, a relying application should validate +the token signature, issuer, nonce, and required claims before releasing data. -✅ **Mixed Node Pools** - Combine standard and confidential node pools in the same cluster for cost optimization (use confidential nodes only for sensitive workloads) +## Troubleshooting -✅ **Preview Features** - Confidential VM support in AKS requires feature registration via `az feature register` +| Message | Cause | Fix | +| --- | --- | --- | +| `SharedTokenCacheCredential authentication unavailable` followed by `Failed to Set-AzContext` | Signed in to the Azure CLI only. The Az PowerShell module has no session. | `Connect-AzAccount -TenantId `, then `Set-AzContext -SubscriptionId `. See the prerequisites above. | +| `ModuleNotFoundError: No module named 'msrestazure'` in a Python traceback during `az aks create` | A stale `aks-preview` CLI extension is intercepting `az aks` commands. This challenge does not need it. | `az extension remove --name aks-preview`, verify with `az aks list -o table`, then re-run. | +| `Azure CLI is not signed in. Run az login.` | No Azure CLI session. | `az login`, then `az account set --subscription `. | +| ` is not set. Define the MicroHack environment variables...` | Task 1 variables missing, or set with Bash syntax instead of `$env:`. | Re-run the Task 1 block in the current PowerShell session. | +| `Resource group '' was not found.` | `RESOURCE_GROUP` does not match the attendee resource group. | Confirm with `az group show --name $env:RESOURCE_GROUP`. | +| Preflight reports unavailable capacity or insufficient quota | The region has no `Standard_DC2as_v5` capacity or no AMD CVM quota. | Select a supported region, request quota, or use `-CcNodeCount 1`. | +| `az aks get-credentials failed` / `kubectl get nodes failed` | Cluster not reachable or `kubectl` missing. | `az aks install-cli`, then re-run `az aks get-credentials -g $env:RESOURCE_GROUP -n `. | +| Rollout or LoadBalancer timeout | Image pull, scheduling, or public IP allocation is slow. | Inspect with the commands below, then re-run `-Deploy`; the script is idempotent for existing resources. | -✅ **CLI Management** - Azure CLI provides efficient commands for managing AKS clusters and confidential node pools +Check rollout and bootstrap logs: -### Production Best Practices - -✅ **Node Pool Sizing** - Start with DC-series VMs (e.g., Standard_DC2as_v5) and scale based on workload requirements - -✅ **Workload Isolation** - Use Kubernetes namespaces, network policies, and RBAC in addition to confidential computing - -✅ **Attestation Policies** - For production, consider custom Azure Attestation policies that enforce specific security requirements - -✅ **Monitoring and Logging** - Integrate AKS diagnostics with Azure Monitor to track confidential node pool health - -### Compliance and Governance - -✅ **Data Sovereignty** - Confidential computing ensures data in containers is encrypted even from cloud operators - -✅ **Regulatory Compliance** - Meets requirements for healthcare (HIPAA), financial services, and government workloads - -✅ **Audit Trail** - Attestation tokens provide cryptographic proof for compliance auditing - ---- - -## Next Steps - -### Explore Advanced AKS Confidential Computing Scenarios - -- **[Confidential Containers on AKS](https://learn.microsoft.com/azure/confidential-computing/confidential-containers)** - Deploy containers with Intel SGX enclaves for process-level isolation -- **[Azure Confidential Computing Add-on for AKS](https://learn.microsoft.com/azure/confidential-computing/confidential-nodes-aks-get-started)** - Explore the official Microsoft add-on for confidential computing -- **[Managed Identity with Confidential AKS](https://learn.microsoft.com/azure/aks/use-managed-identity)** - Integrate Azure AD workload identities with confidential workloads - -### Implement Production-Ready Confidential AKS - -- **Custom Attestation Policies** - Define organization-specific attestation requirements using Azure Attestation policies -- **Multi-Region Deployment** - Deploy confidential AKS clusters across multiple Azure regions for high availability -- **GitOps with Confidential Workloads** - Use Flux or ArgoCD to manage confidential workload deployments -- **Service Mesh Integration** - Combine Istio/Linkerd with confidential computing for end-to-end encryption - -### Secure Your AKS Workloads Further - -- **[Azure Key Vault Provider for Secrets Store CSI Driver](https://learn.microsoft.com/azure/aks/csi-secrets-store-driver)** - Securely inject secrets into confidential pods -- **[Azure Policy for AKS](https://learn.microsoft.com/azure/governance/policy/concepts/policy-for-kubernetes)** - Enforce governance policies on confidential node pools -- **[Network Policies](https://learn.microsoft.com/azure/aks/use-network-policies)** - Restrict network traffic to/from confidential workloads +```powershell +kubectl get pods --output wide +kubectl describe deployment azure-vote-front +kubectl logs deployment/cc-attest --tail=100 +``` -### Learn More About Confidential Computing +If the preflight reports unavailable capacity, select a region that supports +`Standard_DC2as_v5` or request AMD confidential VM quota. -- **[Confidential Inferencing](https://learn.microsoft.com/azure/machine-learning/how-to-machine-learning-confidential-containers)** - Deploy ML models in confidential containers on AKS -- **[Azure Confidential Ledger](https://learn.microsoft.com/azure/confidential-ledger/overview)** - Integrate tamper-proof audit logs with confidential AKS workloads -- **[Multi-Party Computation Patterns](https://learn.microsoft.com/azure/confidential-computing/use-cases-scenarios)** - Build collaborative data processing scenarios with confidential AKS +## Task 5: Clean up ---- +```powershell +./Deploy-VotingAppCC.ps1 -Cleanup +``` -## Additional Resources +AKS deletion is asynchronous. The attendee resource group and resources from +other challenges are retained. -- [Azure Confidential Computing overview](https://azure.microsoft.com/solutions/confidential-compute/) -- [Confidential VMs on Azure](https://learn.microsoft.com/azure/confidential-computing/confidential-vm-overview) -- [AKS Documentation](https://learn.microsoft.com/azure/aks/) -- [Microsoft Azure Attestation](https://learn.microsoft.com/azure/attestation/) -- [AMD SEV-SNP Technology](https://www.amd.com/en/developer/sev.html) -- [Kubernetes Node Selectors](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) +> [!IMPORTANT] +> An AKS cluster with two `Standard_DC2as_v5` nodes plus two public +> LoadBalancer services is the most expensive resource in this MicroHack. Run +> cleanup as soon as you finish the challenge. + +## Source alignment + +The complete Azure Voting App confidential AKS source is retained as an +unchanged local snapshot under +[`resources/azure-voting-app`](resources/azure-voting-app/README.md). The +top-level deployment script is derived from that source with only the +MicroHack-specific changes listed in [UPSTREAM-SOURCE.md](UPSTREAM-SOURCE.md). +Use the top-level script for this walkthrough; the script inside `resources` +is retained only as the unchanged upstream reference.