BitLocker recovery keys missing from Entra ID after removing a third-party agent.
The encryption is still active. The key is not in Entra ID. Clicking Sync does nothing. Here is the two-path fix and the one IME timing fact that explains why nobody's script runs when they expect it to.
The problem state
When you remove a third-party disk encryption agent such as Sophos from a Windows device, the BitLocker encryption it provisioned stays active. The volume is fully encrypted. But the recovery key was never escrowed to Microsoft Entra ID, and on agent removal it is now nowhere central.
Run this on an affected device:
Get-BitLockerVolume -MountPoint C: | Select-Object MountPoint, VolumeStatus, EncryptionPercentage, KeyProtector MountPoint : C:
VolumeStatus : FullyEncrypted
EncryptionPercentage : 100
KeyProtector : {} Fully encrypted. Zero key protectors. BackupToAAD-BitLockerKeyProtector has nothing to work with because no RecoveryPassword protector exists. The agent provisioned BitLocker, held the key internally, and on removal left the volume encrypted with no recoverable protector from an Entra perspective. If a user gets locked out by a TPM change, a firmware update, or a boot integrity failure, the device is unrecoverable.
The manual fix is the Entra portal: find each device, click "Backup recovery key." That works on one device. It does not work on a fleet.
The two-path approach
Not every device has the same problem. Some are fully encrypted with no Entra-escrowed protector. Some were never encrypted at all because the agent was installed before BitLocker ran. You need to classify the fleet before changing anything.
Path 1, already encrypted with no Entra escrow: an Intune platform script adds a RecoveryPassword protector if missing, then escrows every RecoveryPassword protector to Entra ID. Idempotent and safe to re-run.
Path 2, not yet encrypted: a native Endpoint Security BitLocker configuration policy handles encryption and escrow natively via the BitLocker CSP. No script needed for these devices.
Start with a read-only survey.
Step 1: deploy the survey script
This script makes no changes. It reports the BitLocker state per device so you know which path each device needs before anything is modified.
$vol = Get-BitLockerVolume -MountPoint C:
$protectors = $vol.KeyProtector
Write-Host "Device: $env:COMPUTERNAME"
Write-Host "VolumeStatus: $($vol.VolumeStatus)"
Write-Host "EncryptionPercentage: $($vol.EncryptionPercentage)"
Write-Host "KeyProtectorCount: $($protectors.Count)"
foreach ($p in $protectors) {
Write-Host "ProtectorType: $($p.KeyProtectorType) | ID: $($p.KeyProtectorId)"
}
if ($protectors.Count -eq 0) {
Write-Host "STATUS: No key protectors found. Recovery key escrow not possible yet."
} else {
Write-Host "STATUS: Key protectors present. Ready for escrow."
} Deploy as an Intune platform script with these settings. Run this script using the logged on credentials: No. Enforce script signature check: No. Run script in 64 bit PowerShell Host: Yes. Assign to All devices.
The 64-bit host setting is not optional. In the 32-bit PowerShell host on 64-bit Windows, Get-BitLockerVolume returns missing or unreliable data. That was the first thing that went wrong in the lab.
Step 2: read results via Graph
The Intune Device status tab shows only Succeeded or Failed. To see what the script actually reported per device, read resultMessage via Graph:
$scriptId = "your-script-id-here"
$results = Invoke-MgGraphRequest -Method GET `
-Uri "https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts/$scriptId/deviceRunStates"
$results["value"] | ForEach-Object {
Write-Host "================================"
Write-Host "RunState: $($_.runState)"
Write-Host "LastUpdated: $($_.lastStateUpdateDateTime)"
Write-Host "Output:"
Write-Host $_.resultMessage
Write-Host ""
} Requires Connect-MgGraph with DeviceManagementScripts.Read.All. Note that Invoke-MgGraphRequest returns hashtables: index with $results["value"], not Select-Object -ExpandProperty value.
Devices reporting KeyProtectorCount: 0 go to Path 1. Deploy the escrow script to those.
Step 3: deploy the escrow script
Same deployment settings as the survey. The script checks for an existing RecoveryPassword protector, adds one if missing, then escrows every RecoveryPassword protector to Entra ID.
try {
$drive = $env:SystemDrive
$vol = Get-BitLockerVolume -MountPoint $drive
$existing = $vol.KeyProtector |
Where-Object { $_.KeyProtectorType -eq 'RecoveryPassword' }
if ($existing) {
Write-Host 'Recovery password protector already exists. Proceeding to escrow.'
} else {
Write-Host 'No recovery password protector found. Adding one now.'
Add-BitLockerKeyProtector -MountPoint $drive -RecoveryPasswordProtector
Write-Host 'Recovery password protector added successfully.'
}
$vol = Get-BitLockerVolume -MountPoint $drive
$protectors = $vol.KeyProtector |
Where-Object { $_.KeyProtectorType -eq 'RecoveryPassword' }
foreach ($protector in $protectors) {
BackupToAAD-BitLockerKeyProtector -MountPoint $drive `
-KeyProtectorId $protector.KeyProtectorId
Write-Host "Recovery key escrowed to Entra ID. KeyProtectorId: $($protector.KeyProtectorId)"
}
Write-Host "STATUS: Completed successfully on $env:COMPUTERNAME"
} catch {
Write-Host "ERROR: $($_.Exception.Message)"
exit 1
} The recovery password itself is never written to output. Only the KeyProtectorId is logged. The script exits 1 on exception so Intune records a failure rather than a false Succeeded.
The part that took the longest: why Sync does nothing
After assigning the escrow script to All devices in the lab, nothing happened for hours. Every device showed the script as Pending. Clicking Sync in the Intune portal did nothing. Running syncDevice via Graph did nothing.
The Intune portal Sync button and the Graph syncDevice action wake the Windows MDM OMA-DM client. They do not trigger Intune Management Extension PowerShell platform scripts. The IME evaluates PowerShell platform scripts on its own internal timer of roughly eight hours. The widely quoted sixty-minute figure is the Win32 app workload, which is a different mechanism entirely.
On an idle device with no logon or unlock events, a newly assigned platform script will not run for hours. That is not a failure. It is expected timer behaviour. Watch the IME log and you will see the timer set with no immediate execution:
set timer, start the timer for workload PowerShell Then nothing for up to eight hours, unless something triggers early evaluation.
What actually triggers a platform script
Session-change events are first-class triggers. A user logon, logoff, or unlock causes the IME to evaluate platform scripts immediately. Actively used machines run the script within minutes of assignment. Idle locked machines wait the full eight hours.
To force immediate evaluation during testing, restart the IME service as a local administrator:
Restart-Service IntuneManagementExtension -Force After restart, the IME log shows Delaying PS and Win32 app workload checkins by seconds = 237. Execution begins roughly four minutes later. Watch the AgentExecutor log to confirm the script ran:
Get-Content "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\AgentExecutor.log" -Tail 30 | Where-Object {
$_ -like "*BitLocker*" -or
$_ -like "*recovery*" -or
$_ -like "*Completed*" -or
$_ -like "*ERROR*"
} For production rollout: plan for up to eight hours on idle devices. The "is this working?" question will come up. "Wait for a logon event or restart the IME service" is the answer.
Step 4: validate
Read the escrow script's resultMessage via the same Graph call as the survey. A successful run shows the KeyProtectorId confirmation line:
No recovery password protector found. Adding one now.
Recovery password protector added successfully.
Recovery key escrowed to Entra ID. KeyProtectorId: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
STATUS: Completed successfully on DEVICE-NAME Then open the Entra admin centre. Devices > All devices > select the device > BitLocker keys. A key entry for the OS drive confirms the escrow landed. That entry is what your IT team retrieves if a user gets locked out.
Graph scopes and the MSAL token cache
If you add scopes to an existing Graph session without disconnecting first, you get 403 errors on every call. A bare Connect-MgGraph with new scope parameters reuses the cached MSAL token and does not acquire the new permissions. Always Disconnect-MgGraph before reconnecting with additional scopes.
The syncDevice action requires DeviceManagementManagedDevices.PrivilegedOperations.All. The device-list read scope is not sufficient. Missing that scope returns 403 Forbidden on the sync call even after a fresh connect if you did not disconnect first.
Platform scripts run once
Platform scripts execute once per device. To re-run a script that has already completed, add or update a version comment at the top of the script file and save. Changing the content changes the hash, which the IME treats as a new version and re-evaluates. The script ID does not change. For anything requiring scheduled or repeated verification, a Remediation is the better architecture: detect/remediate split, runs on a defined schedule, supports on-demand execution from the portal.
Microsoft references
- Intune Management Extension and PowerShell platform scripts
- Back up BitLocker recovery keys to Azure AD and Entra ID
- BackupToAAD-BitLockerKeyProtector cmdlet reference
- BitLocker CSP and group policy settings
Both platform scripts, the admin-side Graph helpers, and a sanitized sample survey output are in the GitHub repository.