With the change of the Secure Boot certificates coming in fast and furious as summer approaches, it is paramount to ensure that your estate is ready to deploy the changes swiftly and securely.
The change and deployment has been documented thoroughly by several great community articles and contributions in recent months, along with the expansion of Microsoft’s own documentation on the subject.
I will not delve further into that here other than to provide links for further reading, but if you’re looking at a deployment guide, I would highly suggest taking a look at Mindcore’s blog linked below:
📖 Windows Secure Boot certificate expiration and CA updates — Microsoft Support
📖 Windows Secure Boot Certificate Expiration 2026 — Mindcore Blog
I will however touch upon a very specific part of your preparations towards this deployment to make sure it is smooth — one that has only become more important as of recently with the April update KB5083769 (OS Builds 26200.8246 and 26100.8246).
In the known issues of this update, Microsoft notes that devices deployed with an unrecommended BitLocker Group Policy configuration might be required to enter their BitLocker recovery key. The same update also addresses an issue where devices might enter BitLocker Recovery after the Secure Boot updates themselves.
This makes the confirmation of escrowed BitLocker recovery keys in Microsoft Entra absolutely paramount — if a device is prompted for its recovery key and that key was never backed up, you are looking at data loss or a very unpleasant recovery process.
The other week I sat down with a customer and built a script to identify which devices — those with both an Intune object and an Entra object — had escrowed BitLocker keys, and this is the result.
The Reporting Script#
The script queries Microsoft Graph for BitLocker recovery keys stored in Microsoft Entra ID, cross-references them with Intune-managed and Entra-registered Windows devices, and produces a CSV report showing which devices have escrowed keys and which have not.
⚠️ This script uses the Microsoft Graph beta endpoint. While functional, beta APIs may change without notice. The
v1.0endpoint also supports the BitLocker recovery key API if you prefer stability.
Required Graph API permissions:
Device.Read.All
BitLockerKey.ReadBasic.All
DeviceManagementManagedDevices.Read.AllWindows 365 Cloud PC devices are automatically excluded from the report.
📜 View full reporting script
<#
.SYNOPSIS
Reports whether BitLocker recovery keys are escrowed to Entra ID for Windows devices.
.DESCRIPTION
Queries Microsoft Graph for BitLocker recovery keys stored in Entra ID (Azure AD).
This report does NOT include keys escrowed to on-premises Active Directory.
Uses Microsoft.Graph.Authentication and native Invoke-MgGraphRequest calls.
Windows 365 Cloud PC devices are excluded.
Required Graph API permissions:
- Device.Read.All
- BitLockerKey.ReadBasic.All
- DeviceManagementManagedDevices.Read.All
.NOTES
Run once to install the module:
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force
Author: Simon Pauly Kofoed Mose
Blog: https://paulycloud.com
Version: 1.1 - Clarified Entra-only scope; renamed script
1.0 - Initial release
#>
# ── Variables ─────────────────────────────────────────────────────────────────
$baseUri = "https://graph.microsoft.com/beta"
$scopes = @(
"Device.Read.All",
"BitLockerKey.ReadBasic.All",
"DeviceManagementManagedDevices.Read.All"
)
# ── Functions ─────────────────────────────────────────────────────────────────
function Initialize-MgGraphModule {
if (-not (Get-Module -ListAvailable -Name "Microsoft.Graph.Authentication")) {
Write-Host "Installing Microsoft.Graph.Authentication..." -ForegroundColor Yellow
Install-Module "Microsoft.Graph.Authentication" -Scope CurrentUser -Force
}
Import-Module "Microsoft.Graph.Authentication" -ErrorAction Stop
Write-Host "Microsoft.Graph.Authentication module loaded." -ForegroundColor Green
}
function Invoke-MgGraphRequestAll {
param([string]$Uri)
$results = [System.Collections.Generic.List[object]]::new()
$nextUri = $Uri
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $nextUri -OutputType PSObject
if ($response.value) { $results.AddRange($response.value) }
$nextUri = $response.'@odata.nextLink'
} while ($nextUri)
return $results
}
function Connect-Graph {
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
Connect-MgGraph -Scopes $scopes -ContextScope Process -NoWelcome
Write-Host "Connected to Microsoft Graph." -ForegroundColor Green
}
function Get-WindowsDevices {
Write-Host "Fetching Windows devices from Entra..." -ForegroundColor Cyan
$script:devices = Invoke-MgGraphRequestAll -Uri "$baseUri/devices?`$select=id,displayName,deviceId,operatingSystem,approximateLastSignInDateTime" |
Where-Object { $_.operatingSystem -eq "Windows" }
Write-Host " Found $($script:devices.Count) Windows device(s)." -ForegroundColor Cyan
}
function Get-BitLockerKeys {
Write-Host "Fetching BitLocker recovery keys..." -ForegroundColor Cyan
$script:bitlockerMap = @{}
Invoke-MgGraphRequestAll -Uri "$baseUri/informationProtection/bitlocker/recoveryKeys" |
ForEach-Object {
$id = $_.deviceId
$created = $_.createdDateTime
if (-not $script:bitlockerMap.ContainsKey($id) -or $created -gt $script:bitlockerMap[$id]) {
$script:bitlockerMap[$id] = $created
}
}
Write-Host " Found keys for $($script:bitlockerMap.Count) device(s)." -ForegroundColor Cyan
}
function Get-IntuneDeviceInfo {
Write-Host "Fetching Intune managed device info..." -ForegroundColor Cyan
$script:intuneMap = @{}
$script:cloudPcIds = @{}
Invoke-MgGraphRequestAll -Uri "$baseUri/deviceManagement/managedDevices?`$select=id,azureADDeviceId,operatingSystem,lastSyncDateTime,model&`$filter=operatingSystem eq 'Windows'" |
ForEach-Object {
$script:intuneMap[$_.azureADDeviceId] = @{ Id = $_.id; LastSync = $_.lastSyncDateTime; Model = $_.model }
if ($_.model -and $_.model -match "Cloud PC") {
$script:cloudPcIds[$_.azureADDeviceId] = $true
}
}
Write-Host " Found $($script:intuneMap.Count) Intune device(s), $($script:cloudPcIds.Count) Cloud PC(s) excluded." -ForegroundColor Cyan
}
function Build-Report {
Write-Host "Analyzing..." -ForegroundColor Yellow
$script:report = foreach ($device in $script:devices) {
$aadId = $device.deviceId
# Skip Windows 365 Cloud PCs
if ($script:cloudPcIds.ContainsKey($aadId)) { continue }
# Match against both hardware deviceId and directory object id
$escrowed = $script:bitlockerMap.ContainsKey($aadId) -or $script:bitlockerMap.ContainsKey($device.id)
$keyCreated = if ($script:bitlockerMap.ContainsKey($aadId)) { $script:bitlockerMap[$aadId] } else { $script:bitlockerMap[$device.id] }
[PSCustomObject]@{
DeviceName = $device.displayName
DeviceId = $aadId
IntuneDeviceId = $script:intuneMap[$aadId]?.Id
Model = $script:intuneMap[$aadId]?.Model
EscrowStatus = if ($escrowed) { "YES" } else { "NO" }
KeyCreated = $keyCreated
LastIntuneSync = $script:intuneMap[$aadId]?.LastSync
LastEntraActivity = $device.approximateLastSignInDateTime
}
}
}
function Show-Summary {
$withKey = ($script:report | Where-Object { $_.EscrowStatus -eq "YES" }).Count
$withoutKey = ($script:report | Where-Object { $_.EscrowStatus -eq "NO" }).Count
Write-Host "---------------------------------" -ForegroundColor Gray
Write-Host "Devices with escrowed key : $withKey" -ForegroundColor Green
Write-Host "Devices without escrowed key: $withoutKey" -ForegroundColor Red
Write-Host "---------------------------------" -ForegroundColor Gray
}
function Export-Report {
$reportsDir = Join-Path $PSScriptRoot "reports"
if (-not (Test-Path $reportsDir)) { New-Item -ItemType Directory -Path $reportsDir | Out-Null }
$tenantId = (Invoke-MgGraphRequest -Method GET -Uri "$baseUri/organization?`$select=id" -OutputType PSObject).value[0].id
$date = Get-Date -Format "yyyy-MM-dd_HHmm"
$baseName = "BitLockerEntraEscrow_${tenantId}_${date}"
$csvPath = Join-Path $reportsDir "$baseName.csv"
$script:report | Export-Csv $csvPath -NoTypeInformation -Encoding UTF8
Write-Host "CSV saved: $csvPath" -ForegroundColor Cyan
return $reportsDir
}
# ── Execution ─────────────────────────────────────────────────────────────────
$reportsDir = Join-Path $PSScriptRoot "reports"
if (-not (Test-Path $reportsDir)) { New-Item -ItemType Directory -Path $reportsDir | Out-Null }
$transcriptPath = Join-Path $reportsDir "BitLockerEntraEscrow_$(Get-Date -Format 'yyyy-MM-dd_HHmm').log"
Start-Transcript -Path $transcriptPath -Force
try {
Initialize-MgGraphModule
Connect-Graph
Get-WindowsDevices
Get-BitLockerKeys
Get-IntuneDeviceInfo
Build-Report
Show-Summary
Export-Report
} finally {
Disconnect-MgGraph -ErrorAction SilentlyContinue
Stop-Transcript
}Remediating Devices Without Escrowed Keys#
If the report identifies devices that show NO for their escrow status, you can trigger a BitLocker key backup to Microsoft Entra ID remotely.
Detection Script#
Deploy this as an Intune Remediation detection script. It checks whether the device has at least one BitLocker recovery key protector backed up to Microsoft Entra ID:
📜 View detection script
# Detection: Check if BitLocker recovery key is escrowed to Entra ID
try {
$bitlockerVolume = Get-BitLockerVolume -MountPoint "C:" -ErrorAction Stop
$recoveryProtector = $bitlockerVolume.KeyProtector | Where-Object { $_.KeyProtectorType -eq "RecoveryPassword" }
if ($recoveryProtector) {
# Check if the key has been backed up by looking for the recovery password
$keyProtectorId = $recoveryProtector[0].KeyProtectorId
if ($keyProtectorId) {
Write-Output "BitLocker recovery key protector found: $keyProtectorId"
exit 0 # Compliant — key exists
}
}
Write-Output "No BitLocker recovery key protector found."
exit 1 # Non-compliant — needs remediation
} catch {
Write-Output "BitLocker is not enabled on C: drive. Error: $_"
exit 1 # Non-compliant
}Remediation Script#
This script forces a backup of the BitLocker recovery key to Microsoft Entra ID:
📜 View remediation script
# Remediation: Backup BitLocker recovery key to Entra ID
try {
$bitlockerVolume = Get-BitLockerVolume -MountPoint "C:" -ErrorAction Stop
$recoveryProtector = $bitlockerVolume.KeyProtector | Where-Object { $_.KeyProtectorType -eq "RecoveryPassword" }
if (-not $recoveryProtector) {
# Add a recovery password protector if none exists
Add-BitLockerKeyProtector -MountPoint "C:" -RecoveryPasswordProtector
$bitlockerVolume = Get-BitLockerVolume -MountPoint "C:"
$recoveryProtector = $bitlockerVolume.KeyProtector | Where-Object { $_.KeyProtectorType -eq "RecoveryPassword" }
}
# Backup the recovery key to Entra ID
$keyProtectorId = $recoveryProtector[0].KeyProtectorId
BackupToAAD-BitLockerKeyProtector -MountPoint "C:" -KeyProtectorId $keyProtectorId
Write-Output "BitLocker recovery key backed up to Entra ID successfully."
exit 0
} catch {
Write-Output "Failed to backup BitLocker key: $_"
exit 1
}Conclusion#
Before rolling out the Secure Boot certificate updates, take the time to confirm that your BitLocker recovery keys are safely escrowed in Microsoft Entra ID. The reporting script gives you full visibility across your estate, and the detection/remediation pair lets you close the gap automatically through Intune.
If you have questions or improvements, feel free to reach out!
