Series · BitLocker Key Monitor: proving recovery keys exist · Part 1 of 3

Intune · BitLocker · Entra ID · Active Directory

Checking BitLocker recovery keys in AD and Entra

Part 1 of 3. Check where recovery keys are available, resolve device identities carefully, and compare passwords when explicitly enabled rather than relying on presence alone.

Drafted
Published
Updated
Reading time
5 min
Author
Roberto Gramellini

Source: BitLocker Key Monitor · revision 6390107e

  • BitLocker recovery key
  • Intune
  • Entra ID
  • Active Directory
  • key escrow
  • consistency check
  • Quartz.NET
  • device management
  • hybrid join
  • audit
Two overlapping circles labelled AD and Entra with a red mismatch badge on a padlock.

Consider a Friday-evening recovery call. A laptop is at the BitLocker recovery screen and somebody says, quite reasonably, "The key should be in Entra." Should be. The record might be missing, it might contain a different key, or it might have disappeared during a cleanup of stale devices. None of these possibilities becomes easier to investigate with the user waiting.

BitLocker Key Monitor exists to check that assumption before the call. It repeatedly scans the expected escrow locations, correlates the records and, when key-value collection is enabled, compares the passwords themselves. I'd rather have the evidence and its collection limits than another reassuring green tile.

We start with the scanner because a secure transport cannot rescue a bad observation. Part 2 follows the collected data into Azure, and Part 3 explains how stored keys are encrypted and where that protection stops.

The records can disagree

In a hybrid estate, recovery information may be in on-premises Active Directory, Microsoft Entra ID, both, or neither. Each native view shows its own records. Reconciling those views is left to the operator, including some awkward questions:

  • Is a key present in AD but missing from Entra, or the other way round?
  • Do both sides hold the same protector identifier while the recovery password differs?
  • Did a device really retire, or did it merely vanish from a scan?
  • When audit asks for evidence of availability, can you produce observations rather than a manually maintained spreadsheet?

Give the disagreement a name

The scanner reads AD and Entra through Graph, links device identities and retains observation history. It then classifies each device. The explicit, append-only enum matters more than it first appears: changing the meaning of an old numeric status would also change how historical reports are read. Here are the states:

// src\BitLockerKeyMonitor.Core\Models\ConsistencyResult.cs (lines 7-27)
public enum ConsistencyStatus
{
    Consistent = 0,
    MissingInAd = 1,
    MissingInEntra = 2,
    StaleInAd = 3,
    StaleInEntra = 4,
    DeviceNotCorrelated = 5,
    KeyMismatch = 6,
    ScanError = 7,
    /// <summary>
    /// Key with same ID exists in both AD and Entra, but the actual recovery password values differ.
    /// Only detectable when IncludeRecoveryKeyValue is enabled. Indicates corruption or tampering.
    /// </summary>
    KeyValueMismatch = 8,
    /// <summary>
    /// No recovery keys found in either AD or Entra for this device.
    /// The device may not have BitLocker enabled, or keys were not escrowed.
    /// </summary>
    NoKeysFound = 9,

The four core recovery-key states: healthy, missing in AD, missing in Entra and value mismatch.

Figure 1. Four common results. Checking for a value mismatch requires explicitly enabled key-value collection.

I'd pay particular attention to KeyValueMismatch. Presence checks can look healthy even when the stored password is unusable for the expected protector. Comparing actual values catches that disagreement, but it means collecting secrets. That is why the mode is optional. Do not enable it as if it were just another harmless reporting column.

Who is who? Correlating devices

One laptop can have an AD computer account, an Entra device object and an Intune record. The names may look familiar; the objects are still different. The scan job preloads lookup maps for its data source and tries the Entra device ID first. Display-name matching is a fallback only when the name is unique:

// src\BitLockerKeyMonitor.Worker\Jobs\BitLockerScanJob.cs (lines 459-468)
var devicesByEntraId = await db.Devices
    .Where(d => d.EntraDeviceId != null && d.SourceId == scanRun.SourceId)
    .ToDictionaryAsync(d => d.EntraDeviceId!.Value, ct);

var devicesByName = await db.Devices
    .Where(d => d.EntraDeviceId == null && d.SourceId == scanRun.SourceId)
    .GroupBy(d => d.DisplayName)
    .Where(g => g.Count() == 1) // Only use name matching if unique
    .Select(g => g.First())
    .ToDictionaryAsync(d => d.DisplayName, ct);

Two devices called LAPTOP-01 should not inherit each other's history just because a report would look tidier with fewer unmatched rows. Scoping the lookup by source and rejecting ambiguous names makes the uncertainty visible as DeviceNotCorrelated. Leave it visible until identity is resolved.

Running the scanner inside the domain

On-premises architecture with a Quartz worker, a web portal, a core library, SQL Server, Active Directory and Entra ID.

Figure 2. The worker collects observations; operators review them in a Windows-authenticated portal backed by SQL Server.

A Windows worker service runs the scans using Quartz.NET. The cron expression is configuration, so changing the collection schedule does not require rewriting the scan job:

// src\BitLockerKeyMonitor.Worker\Program.cs (lines 371-384)
var cronSchedule = builder.Configuration.GetValue<string>("ScanSettings:CronSchedule") ?? "0 0 2 * * ?";

builder.Services.AddQuartz(q =>
{
    var jobKey = new JobKey("BitLockerScanJob");
    q.AddJob<BitLockerScanJob>(opts => opts.WithIdentity(jobKey));
    q.AddTrigger(opts => opts
        .ForJob(jobKey)
        .WithIdentity("BitLockerScanTrigger")
        .WithCronSchedule(cronSchedule));

    // Listener that logs next-fire-time after each job run
    q.AddTriggerListener<ScanScheduleListener>();
});

By default, the scan runs at 02:00. Compliance re-evaluation runs every 5 minutes, and both worker types send heartbeats every 30 seconds. Those are separate clocks. A recent heartbeat tells you the worker is alive; it is not a newly completed directory scan.

If preserving Entra keys independently of AD is the requirement, there is also RecoveryVault mode. It retains historical keys after device retirement or deletion. That can be useful, but it is an archive. It does not perform the AD-to-Entra consistency check described above.

Decide what you need to prove

For an endpoint team, the useful question is more specific than "Are our keys escrowed?" Which device, which protector, which location, and how recent was the observation? Password equality adds stronger evidence, with the extra responsibility of handling those passwords. An unresolved identity should remain unresolved rather than being guessed into a healthy state.

The AD collection still has to happen inside the domain. Getting those sensitive observations to Azure without giving the exposed intake access to their contents is the next job. Continue with Part 2: the sealed intake.