Skip to main content
  1. Blog/

Secure Boot Certificates – Confirm Escrow of BitLocker Recovery Keys in Microsoft Entra

Simon Pauly Kofoed Mose
Author
Simon Pauly Kofoed Mose
Sharing knowledge on Microsoft Intune, endpoint management, device compliance, and cloud-first IT strategies.
Table of Contents

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.0 endpoint also supports the BitLocker recovery key API if you prefer stability.

Required Graph API permissions:

Device.Read.All
BitLockerKey.ReadBasic.All
DeviceManagementManagedDevices.Read.All

Windows 365 Cloud PC devices are automatically excluded from the report.

📖 Get-BitLockerEntraEscrowStatus.ps1 — GitHub

📜 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:

📖 Detect-BitLockerEscrow.ps1 — GitHub

📜 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:

📖 Remediate-BitLockerEscrow.ps1 — GitHub

📜 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!

Related

Confirm Escrow of FileVault Recovery Keys in Microsoft Entra

A couple of weeks ago I wrote about confirming the escrow of BitLocker recovery keys in Microsoft Entra — driven by the urgency of the Secure Boot certificate changes. On the macOS side, there is no equivalent certificate crisis forcing our hand right now, but that does not make FileVault key escrow any less important. macOS continues to grow as a platform in the enterprise. More and more organizations are offering Macs as a choice — or even a default — for their workforce, and with Apple Silicon delivering strong performance across developer, creative, and general productivity workloads, that trend is only accelerating. As your Mac fleet grows, so does the importance of managing it with the same rigour you apply to Windows.

IntuneTip: Reset Windows Hello for Business Using On-Demand Remediation

Sometimes users need to have their Windows Hello for Business container reset. This can happen for a myriad of reasons: Biometrics stopped working “Something went wrong” errors during sign-in that won’t resolve Trust relationship between the credential and Microsoft Entra ID broke User suspects their PIN was observed or compromised Device was lost briefly and recovered — user wants to re-key For this support request, you can easily push a small script using Intune’s on-demand remediation feature (preview). All it does is use certutil to delete the Windows Hello container and return the exit code.