In this blog post I will walk you through Guest Attestation for Confidential Azure Virtual Desktop session hosts. This is the third post in the Confidential AVD series. In part one we built CVM-compatible images with Azure Image Builder and deployed session hosts with AMD SEV-SNP memory encryption. In part two we layered customer-managed keys on top: Key Vault, Disk Encryption Set, Event Grid alerting, and a 9-step offline rotation script.
Both posts ended with the same question from readers: you said Guest Attestation provides cryptographic proof that a session host is running in a genuine Trusted Execution Environment, but how do you actually verify that? What does the proof look like? Where does it come from? And what happens if it breaks?
That is exactly what this post covers. One important thing I discovered while building this out: you do not need to deploy your own Azure Attestation Provider to get that proof. The Microsoft.Attestation/attestationProviders resource type is not available in Belgium Central, which is where this deployment runs. Microsoft provides shared regional endpoints that do the same job with zero infrastructure, and you can verify the result all the way down to the hardware signature using nothing but PowerShell.
All source code is available on GitHub.
⭐ yannickdils/confidentialavd
Bicep modules, PowerShell scripts, Azure DevOps pipelines, KQL queries, and Azure Policy for Confidential AVD
- What Guest Attestation actually proves and how it works at the hardware level
- Why you do not need a custom Attestation Provider – and why you cannot deploy one in Belgium Central
- The three Microsoft shared MAA endpoints and which one to use for West Europe
- How to decode the JWT attestation token and validate the claims that actually matter
- How to cryptographically verify the RS256 signature against Microsoft’s signing certificates
- A CVM-specific Data Collection Rule that captures attestation, vTPM, and boot integrity events
- Eight KQL queries for monitoring attestation health across your host pool
- An Azure Policy definition that enforces the extension on every Confidential VM
- A six-step
Get-AttestationStatus.ps1validation script and a five-stageAVD-DeployAttestation.ymlpipeline
What Guest Attestation proves and how it works
Let me start with what the proof actually is, because the solution makes more sense once you understand what problem it is solving.
When you deploy a Confidential VM with securityType: ConfidentialVM in Bicep, Azure records the security profile and places the VM on hardware that supports AMD SEV-SNP. But that is a declaration in the Azure control plane. The session host has no built-in way to verify from inside the guest that it is actually running in a hardware TEE and not on a standard host with the security profile flags set. Neither does your security team or your auditors.
Guest Attestation fills that gap. Here is the full sequence of what happens when the GuestAttestation extension runs on a session host.
- The extension calls the AMD SEV-SNP firmware inside the physical CPU and requests a hardware attestation quote. The quote contains cryptographic measurements of the processor firmware version, the boot chain as recorded by the vTPM, the Secure Boot state, and the SEV-SNP features active at the hardware level.
- The quote is signed by a key that is physically embedded in the AMD CPU. It cannot be forged in software.
- The extension sends the quote to a Microsoft Azure Attestation (MAA) endpoint over HTTPS.
- MAA verifies the AMD signature, checks the firmware measurements against Microsoft’s known-good baseline for Azure CVMs, and if everything passes, issues a signed JWT attestation token.
- The extension writes the result to the
Microsoft-Windows-Attestation/OperationalWindows event log.
The JWT token is the proof. The three claims in the payload that matter for Confidential AVD are:
- x-ms-attestation-type: sevsnpvm – confirms the platform is AMD SEV-SNP hardware, not emulated and not a standard VM with the security profile set incorrectly
- x-ms-compliance-status: azure-compliant-cvm – confirms Secure Boot is active, vTPM is present, and the platform passed Microsoft’s full CVM compliance baseline
- secureboot: true – explicit Secure Boot confirmation
And critically: the JWT is signed with RS256 using a certificate from the MAA endpoint. Anyone who holds the public key can verify that the token was issued by Microsoft and was not tampered with. That verification step is what makes this a technical guarantee rather than a policy promise.
avd-sessionhosts-cc.bicep in part one is already installed on your CVM session hosts and is already talking to Microsoft’s shared MAA endpoint. The maaEndpoint field was left empty in the Bicep, which is correct. An empty string tells the extension to use the shared endpoint automatically. This post adds the validation layer on top of what is already working.Why you do not need a custom Attestation Provider
You might expect this post to start with deploying a custom Microsoft.Attestation/attestationProviders resource. It does not – and for a Belgium Central deployment it cannot. The resource type has a fixed list of supported regions that predates the Belgium Central region, which only went GA in November 2025.
If you try to deploy the Attestation Provider resource to belgiumcentral you will get:
LocationNotAvailableForResourceType: The provided location 'belgiumcentral' is not available for resource type 'Microsoft.Attestation/attestationProviders'.
This turns out not to matter, because Microsoft already runs shared Attestation Provider instances in each supported region. These shared endpoints are available to every Azure customer, require no deployment, and apply Microsoft’s default attestation policy which validates exactly what you need for Confidential VMs: AMD SEV-SNP hardware, Secure Boot, and vTPM state.
The three shared endpoints closest to a Belgium-based deployment are:
| Region | Endpoint |
|---|---|
| Belgium Central | https://sharedbec.bec.attest.azure.net |
| West Europe (Amsterdam) | https://sharedweu.weu.attest.azure.net |
| North Europe (Dublin) | https://sharedneu.neu.attest.azure.net |
| East US 2 | https://sharedeus2.eus2.attest.azure.net |
For a Belgium Central deployment, the region-local authority is https://sharedbec.bec.attest.azure.net, and that is what Belgium Central session hosts should call – and do call, once you set it explicitly. This is the same authority that CreateHSM_CMK.ps1 hardcodes in the Managed HSM Secure Key Release policy, so lining up the runtime GuestAttestation extension with it keeps the attestation authority consistent across both flows. It is not the default yet: if you leave maaEndpoint empty the extension falls back to sharedweu.weu.attest.azure.net. To pin Belgium Central session hosts to the region-local endpoint, set maaEndpoint to https://sharedbec.bec.attest.azure.net in the GuestAttestation extension settings of your hostpool config, mirroring what the SKR script does for the HSM key.
The only practical difference between the shared endpoint and a dedicated one is that with the shared endpoint you cannot write a custom attestation policy. For Confidential AVD, the default Microsoft policy validates everything you need. Custom policies are relevant for more advanced scenarios like Secure Key Release, where you want to restrict key access to CVMs running in a specific region or with a specific firmware version.
If you followed part two of this series you will have noticed that the Secure Key Release (SKR) policy attached to the Managed HSM key uses a different attestation authority than the one discussed here. Both are correct – they serve two different purposes.
Runtime GuestAttestation (this post): The GuestAttestation extension running inside the session host calls sharedweu.weu.attest.azure.net to produce the JWT token you can decode, log, and validate from KQL. This is the customer-visible attestation flow and runs on a schedule from inside the guest OS.
CVM platform SKR (part two): When a Belgium Central CVM boots against a CMK-encrypted disk, the Azure Confidential VM Orchestrator (a Microsoft-internal platform service, not the in-guest extension) presents an attestation token signed by the region-local authority sharedbec.bec.attest.azure.net to the Managed HSM. The SKR release policy on the key therefore hardcodes this authority – the full Microsoft default policy lists 55 regional authorities, which exceeds the Managed HSM request size limit, so CreateHSM_CMK.ps1 ships a Belgium-only subset.
This is why the customer-facing Microsoft.Attestation/attestationProviders resource type being unavailable in Belgium Central does not block CMK boot: the platform SKR authority is operated by Microsoft and exists independently of the customer-deployable resource.
Architecture overview
The attestation validation stack built in this post sits on top of the infrastructure from parts one and two. The session hosts were already calling the shared MAA endpoint. What we are adding is the visibility layer: a Data Collection Rule that captures what the extension reports, KQL queries to surface it in Azure Monitor, a PowerShell script that performs six steps of validation, and a pipeline that runs everything in sequence.

Figure 1 – Guest Attestation flow: CVM session host calls the Microsoft shared MAA endpoint, the GuestAttestation extension writes results to the Windows event log, Azure Monitor Agent ships them to Log Analytics via the CVM-specific DCR
The validation flow has six steps, each building on the previous one.
Step 1 – Extension health (control plane): Checks the GuestAttestation extension provisioning state on every CVM via az vm extension show.
Step 2 – Instance view (control plane): Queries the detailed attestation status from each VM’s instance view, retrieving component status and health messages reported by the extension.
Step 3 – MAA certificate chain analysis: Fetches all signing certificates from the shared MAA endpoint and analyses the full X.509 certificate chain: subject, issuer, validity period, and signature algorithm.
Step 4 – In-VM evidence collection (data plane): Uses Run Command to collect Secure Boot state, TPM status, VBS configuration, THIM endpoint data, and IMDS attestation metadata directly from each session host.
Step 5 – JWT decode and signature verification: Retrieves the attestation token from the Windows event log on each session host, decodes the payload to validate the critical claims (x-ms-attestation-type, x-ms-compliance-status, secureboot), then verifies the RS256 signature against the MAA signing certificates. A valid signature proves the token was issued by Microsoft Azure Attestation and was not tampered with.
Step 6 – Log Analytics query: Queries the Log Analytics workspace for attestation error events from the past N days, providing a historical view of attestation health across the host pool.
The new components added in this post:
- CVM-specific Data Collection Rule (
dcr-avd-confidential-prd-weu-001) – collects attestation events, vTPM key operations, and boot integrity events that the standard AVD Insights DCR misses - Azure Policy definition – audits all Confidential VMs for the GuestAttestation extension
- Get-AttestationStatus.ps1 – six-step validation script covering extension health, instance view, MAA certificates, in-VM evidence, JWT signature verification, and Log Analytics queries
- AVD-DeployAttestation.yml – five-stage pipeline that loads JSON config, deploys the DCR and Log Analytics solutions, deploys Policy, and runs the validation script
Understanding the shared MAA endpoint
Because there is no custom provider to deploy, the first thing to verify is that your session hosts can reach the shared endpoint. The GuestAttestation extension makes an outbound HTTPS call to sharedweu.weu.attest.azure.net on TCP 443. If an NSG blocks this call, the extension will provision successfully but attestation will silently fail.
You can test connectivity from a session host using Run Command in the portal, or from your pipeline:
You can also query the signing certificates directly from the pipeline agent to verify the endpoint is reachable from your network:
Decoding and validating the JWT attestation token
The GuestAttestation extension writes the attestation token to the Microsoft-Windows-Attestation/Operational Windows event log on each session host after each attestation cycle. The token is a standard base64url-encoded JWT with three dot-separated sections: header, payload, and signature.
Here is how to decode and validate it manually from inside a session host:
x-ms-compliance-status value azure-compliant-cvm is the single most important claim. It means Microsoft’s attestation service has verified that the platform is genuine AMD SEV-SNP hardware AND that all the CVM requirements are met: Secure Boot enabled, vTPM present, firmware version within the approved baseline, and no debug mode active. If this claim is missing or has any other value, the host is not attestation-compliant regardless of what the extension provisioning state shows.Cryptographic signature verification
Decoding the claims tells you what the token says. Verifying the signature tells you that the token was issued by Microsoft Azure Attestation and has not been tampered with since. This is the step that Thomas Van Laere covers in depth in his post on verifying MAA JWT tokens.
The JWT header contains a jku field pointing to the MAA signing certificate set URL, and a kid identifying which specific certificate was used to sign this token. The process is:
- Fetch the signing certificates from
<MAA_ENDPOINT>/certs - Find the certificate whose
kidmatches the token header - Build an RSA public key from the certificate’s
x5cvalue - Reconstruct the signed input as
base64url(header).base64url(payload) - Verify the RS256 signature (RSASSA-PKCS1-v1_5 with SHA-256)
kid in the token header must match a certificate at the jku URL in that same header. If you pass a token from a North Europe session host to the West Europe certs endpoint, the kid will not match and verification will fail. Always verify against the endpoint whose URL appears in the token’s jku field. For West Europe session hosts this will always be https://sharedweu.weu.attest.azure.net/certs.The CVM-specific Data Collection Rule
The standard AVD Insights Data Collection Rule collects performance counters and Windows event logs useful for general desktop virtualisation monitoring. It does not collect anything from the attestation or TPM event channels. The module at ComponentLibrary/GuestAttestation/main.bicep adds three event sources that are completely absent from the standard DCR.
You can review the key parts here, or view the full module on GitHub:
📄 ComponentLibrary/GuestAttestation/main.bicep
CVM-specific DCR collecting attestation events, vTPM key operations, boot integrity events, and AVD performance counters
Microsoft-Windows-Attestation/Operational is where the GuestAttestation extension writes the result of each attestation cycle. Errors here mean the extension failed to verify with the MAA endpoint. This is the primary monitoring signal.
Microsoft-Windows-TPM-WMI/Operational contains events from the vTPM driver. Errors here indicate the virtual TPM is in a degraded state, which means attestation measurements cannot be trusted even if the extension reports success.
Microsoft-Windows-Kernel-Boot/Operational records boot chain events. If Secure Boot blocks a driver or the boot sequence deviates from the measured baseline, errors appear here and help you distinguish hardware-level from software-level problems.
Security EventIDs 5059 and 5061 are cryptographic key operations on the vTPM. These should only appear during VM deployment or CMK key rotation from part two. Unexpected occurrences warrant investigation.
Once the DCR is deployed, update the dataCollectionRuleId field in your hostpool.template.json to the DCR’s resource ID. New session hosts deployed via AVD-DeployAdditionalHosts will associate with it automatically.
📄 ComponentLibrary/GuestAttestation/main.parameters.json
Parameters file – update dataCollectionRuleName and logAnalyticsWorkspaceId
The DCR requires the SecurityEvent and WindowsEvent tables to exist in the Log Analytics workspace before data can flow. These are solution-managed tables that cannot be created via the REST API. The module at ComponentLibrary/GuestAttestation/solutions.bicep deploys the Security and SecurityInsights Log Analytics solutions, which auto-provision both tables. The pipeline deploys this module first and waits for the tables to appear before deploying the DCR.
📄 ComponentLibrary/GuestAttestation/solutions.bicep
Deploys Security and SecurityInsights LA solutions to auto-provision SecurityEvent and WindowsEvent tables
Monitoring attestation health with KQL
With data flowing into Log Analytics via the CVM DCR, these queries give you visibility into attestation health across your host pool. All eight are in Queries/attestation-kql-queries.kql.
📄 Queries/attestation-kql-queries.kql
Eight KQL queries: attestation health per host, failure events, vTPM operations, hosts with no telemetry, 30-day trend, session correlation, performance, and Policy compliance
Attestation health per session host
This is the query to run first after any session host deployment or image rollout. A host missing from the results entirely has never sent attestation telemetry, which is just as concerning as one with explicit errors.
Session hosts with no attestation telemetry
This catches the silent failure case. Hosts that appear in the Windows event log stream but not in the attestation channel are either running an older image from before part one, have a failed extension installation, or have an NSG blocking the shared MAA endpoint.
ErrorCount > 0 for any session host, routed to the same Action Group used for CMK key expiry alerts in part two.Enforcing the extension with Azure Policy
The policy definition at ComponentLibrary/Policy/policy-require-guest-attestation.bicep uses AuditIfNotExists to flag any Confidential VM that does not have a successfully provisioned GuestAttestation extension. The securityProfile.securityType == ConfidentialVM filter means it only evaluates CVMs, so standard session hosts, the AIB build VM, and any other non-CVM infrastructure are invisible to it.
📄 ComponentLibrary/Policy/policy-require-guest-attestation.bicep
AuditIfNotExists policy definition – flags Confidential VMs missing a healthy GuestAttestation extension
The policy definition uses targetScope = 'subscription' and is deployed at subscription scope via az deployment sub create. The pipeline handles both the definition deployment and the policy assignment in AuditIfNotExists mode automatically.
If you prefer to assign it manually in the Azure Portal:
- Go to Azure Portal > Policy > Assignments > Assign Policy
- Set the scope to the subscription containing your CVM session hosts
- Search for
require-guest-attestation-confidential-avd - Leave the effect as
AuditIfNotExistsinitially - Review compliance after 30 minutes, fix any flagged hosts, then consider switching to
Denyif your security posture requires it

Figure 2 – Azure Policy compliance dashboard showing all Confidential VMs with a healthy GuestAttestation extension
Validating attestation health with Get-AttestationStatus.ps1
The script at Scripts/Get-AttestationStatus.ps1 performs the six validation steps in sequence: extension health, instance view, MAA certificates, in-VM evidence, JWT decode with signature verification, and Log Analytics queries. It uses the Microsoft shared MAA endpoint for signature verification – no custom provider required.
| Step | What it does |
|---|---|
| 0 – Subscription context | Sets the Azure subscription and confirms the logged-in identity |
| 1 – Extension health | Checks the GuestAttestation extension provisioning state on every CVM via az vm extension show |
| 2 – Instance view | Queries the detailed attestation status from each VM’s instance view, retrieving component status and health messages |
| 3 – MAA certificate chain | Fetches all signing certificates from the shared MAA endpoint and analyses the full X.509 certificate chain |
| 4 – In-VM evidence | Uses Run Command to collect Secure Boot state, TPM status, VBS configuration, THIM endpoint data, and IMDS attestation metadata |
| 5 – JWT decode + RS256 verify | Retrieves the attestation token from the event log, decodes the payload to validate critical claims, and verifies the RS256 signature against the MAA signing certificates |
| 6 – Log Analytics query | Queries for attestation error events from the past N days |
📄 Scripts/Get-AttestationStatus.ps1
Six-step attestation validation: extension health, instance view, MAA certificates, in-VM evidence, JWT decode + RS256 signature verification, Log Analytics query
To validate with -DryRun first to confirm all resources are reachable:
To run the full six-step validation:




Figures 3-6 – Get-AttestationStatus.ps1 output showing all six validation steps passing
To skip JWT validation when session hosts are deallocated (for example, during a maintenance window):
The script exits with code 0 if all validations pass, code 2 if some were inconclusive (typically because VMs are deallocated), and code 1 if any validation fails. These exit codes make it safe to use directly in the pipeline validate stage.
Common failure modes and their causes:
| Exit state | Common cause | Fix |
|---|---|---|
| MISSING | VM deployed without the extension in the Bicep | Redeploy the extension via az vm extension set |
| FAILED | NSG blocking outbound HTTPS to sharedweu.weu.attest.azure.net |
Add outbound allow rule for TCP 443 to the MAA endpoint |
| CLAIMS_FAIL | VM is not a Confidential VM (wrong size or missing security profile) | Verify securityType: ConfidentialVM and DC-series VM size in the Bicep |
| NO_TOKEN | Extension newly installed and has not run its first attestation cycle yet | Wait a few minutes and re-run |
| PROVISIONING | Extension still installing | Wait 5-10 minutes after deployment |
Deploying everything with AVD-DeployAttestation.yml
The updated pipeline at Pipelines/AVD-DeployAttestation.yml has been restructured to reflect the correct approach: no Attestation Provider deployment, just parameter loading, DCR and Log Analytics solutions deployment, optional Policy deployment and assignment, and six-step validation.
The five stages are:
- Initialize Parameters – loads the JSON hostpool config, extracts subscription, host pool, resource group, Log Analytics workspace, and MAA endpoint variables. Validates that
confidentialComputeis enabled. - Approval gate – same pattern as the other pipelines in the repository
- Deploy DCR – deploys
solutions.bicepto provision the Log Analytics tables, waits for table provisioning, deploys the CVM-specific Data Collection Rule (main.bicep), and associates the DCR to all CVMs in the resource group. - Deploy Policy (optional) – deploys the Azure Policy definition at subscription scope and assigns it in
AuditIfNotExistsmode. Controlled by thedeployPolicyparameter. - Validate – runs
Get-AttestationStatus.ps1with the shared MAA endpoint against all CVM session hosts. Passes-SkipJwtValidationif the pipeline parameter is set.

Figure 6 – Successful AVD-DeployAttestation pipeline run showing all five stages
📄 Pipelines/AVD-DeployAttestation.yml
Five-stage pipeline: initialize parameters, approval gate, deploy DCR + solutions, policy deployment + assignment, validate
maaEndpoint pipeline parameter to the AVD-DeployAdditionalHosts pipeline as well, and pass it through to Get-AttestationStatus.ps1 in the post-deployment validation step. That way every new session host deployment automatically gets six-step attestation verification before users connect.What a failed attestation means operationally
A failed attestation does not block user logons by default. The GuestAttestation extension generates the token and writes it to the event log. Nothing acts on the result automatically unless you configure it to do so.
If you want attestation failures to trigger a response, the most practical options for Confidential AVD are:
Microsoft Defender for Cloud integration. If you have Defender for Servers Plan 2 enabled, a failed attestation generates a security alert in Defender for Cloud. You can connect these alerts to a Logic App or Azure Automation runbook that puts the affected host into drain mode and raises an incident ticket.
Log Analytics scheduled query alert. Use Query 1 from the KQL file and create a scheduled query alert rule in Azure Monitor that fires when ErrorCount > 0 for any session host. Route it to the same Action Group used for CMK key expiry alerts in part two.
For most organisations starting with Confidential AVD, alerting and monitoring first is the right approach. Understand what healthy attestation looks like in your environment, establish a baseline, and then layer in automated remediation once you are confident the alert thresholds are correct.
What’s new in the repository
Here is a summary of what is in the repository after this post. Note that attestation-provider.bicep and its parameters file have been removed because they cannot be deployed to Belgium Central.
📄 ComponentLibrary/GuestAttestation/main.bicep
CVM-specific DCR – attestation events, vTPM key operations, boot integrity, AVD perf counters
📄 ComponentLibrary/GuestAttestation/main.parameters.json
Parameters – update dataCollectionRuleName and logAnalyticsWorkspaceId
📄 ComponentLibrary/GuestAttestation/solutions.bicep
Deploys Security and SecurityInsights LA solutions to auto-provision SecurityEvent and WindowsEvent tables
📄 ComponentLibrary/Policy/policy-require-guest-attestation.bicep
AuditIfNotExists policy – flags Confidential VMs missing the GuestAttestation extension
📄 Scripts/Get-AttestationStatus.ps1
Six-step validation: extension health, instance view, MAA certificates, in-VM evidence, JWT decode + RS256 signature verification, Log Analytics query
📄 Pipelines/AVD-DeployAttestation.yml
Five-stage pipeline: initialize parameters, approval gate, deploy DCR + solutions, policy deployment + assignment, validate
📄 Queries/attestation-kql-queries.kql
Eight KQL queries for attestation health, vTPM events, trend charts, and Policy compliance
Operational checklist
Before calling your Confidential AVD deployment production-ready from an attestation standpoint:
- Confirm the shared MAA endpoint is reachable from your session host subnet. Outbound TCP 443 to
sharedweu.weu.attest.azure.netmust be allowed by the NSG. This is the single most common cause of silent attestation failure. - Set
attestationUrlinhostpool.template.jsonto match your deployment region. For Belgium Central, set it tohttps://sharedbec.bec.attest.azure.netto use the region-local authority. An empty string defaults to the West Europe endpoint (sharedweu.weu.attest.azure.net), which works but is not the region-local authority for Belgium Central deployments. - Update
dataCollectionRuleIdinhostpool.template.jsonwith the ID from the DCR deployment. New session hosts deployed viaAVD-DeployAdditionalHostswill associate with the CVM-specific DCR automatically. - Run
Get-AttestationStatus.ps1after every image rollout. Add it to the validate stage of yourAVD-DeployAdditionalHostspipeline run. A new host that fails the claims validation or signature verification should be investigated before users connect to it. - Create an Azure Monitor alert on Query 1. Set it to fire when
ErrorCount > 0for any session host and route it to your existing Action Group. - The pipeline assigns the Azure Policy in AuditIfNotExists mode automatically. Run for two weeks, review compliance, resolve flagged hosts, and only switch to Deny if required.
- For Thomas Van Laere’s deeper coverage of attestation theory and JWT verification, see his posts on verifying MAA JWT tokens and Microsoft Azure Attestation architecture.
References
- Azure Attestation overview – Microsoft Learn
- Guest Attestation for Azure Confidential VMs – Microsoft Learn
- About Azure Confidential VMs – Microsoft Learn
- Verifying Microsoft Azure Attestation JWT tokens – Thomas Van Laere
- Microsoft Azure Attestation architecture – Thomas Van Laere
- Part 1: Building Confidential AVD Images with Azure Image Builder – tunecom.be
- Part 2: Customer-Managed Keys for Confidential AVD – tunecom.be
- Thomas Van Laere – Confidential Computing series