Skip to content

Bitlocker: do not decrypt already encrypted drive.#43130

Merged
getvictor merged 30 commits intomainfrom
victor/40809-bitlocker
Apr 9, 2026
Merged

Bitlocker: do not decrypt already encrypted drive.#43130
getvictor merged 30 commits intomainfrom
victor/40809-bitlocker

Conversation

@getvictor
Copy link
Copy Markdown
Member

@getvictor getvictor commented Apr 7, 2026

Related issue: Resolves #40809

Orbit agent: key rotation replaces decrypt-then-re-encrypt:

  • When the disk is already encrypted, orbit now adds a new Fleet-managed recovery key protector, removes old ones, and escrows the new key. The disk is never decrypted.
  • If key escrow fails, the rotated key is cached in memory and retried on subsequent ticks without rotating again.
  • Removes DecryptVolume and decrypt() (no longer called from production code).

Server: osquery query returns both protection_status and conversion_status:

  • The disk_encryption_windows query now returns both columns instead of just checking protection_status = 1. This lets the server correctly identify a disk as encrypted via conversion_status = 1 even when protection_status = 0.
  • New directIngestDiskEncryptionWindows function parses both values, handles parse errors, and normalizes protection_status = 2 (unknown) to NULL.

Server: new bitlocker_protection_status column and status logic:

  • Adds bitlocker_protection_status column to host_disks (DB migration).
  • When a disk is encrypted and key is escrowed but protection is off, the host shows "Action required" with a detail message explaining the issue, instead of misleadingly showing "Verified."
  • protection_status = 2 (unknown) and NULL (older orbit hosts) are treated as protection on for backward compatibility.
  • The profiles_verified and profiles_verifying branches in the combined profiles+BitLocker summary now handle bitlocker_action_required, counting those hosts as "pending".

Contributor docs updates: #43241
Public docs updates: https://github.com/fleetdm/fleet/pull/43243/changes

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

Database migrations

  • Checked schema for all modified table for columns that will auto-update timestamps during migration.

fleetd/orbit/Fleet Desktop

  • Verified compatibility with the latest released version of Fleet (see Must rule)
  • If the change applies to only one platform, confirmed that runtime.GOOS is used as needed to isolate changes
  • Verified that fleetd runs on macOS, Linux and Windows
  • Verified auto-update works from the released version of component to the new version (see tools/tuf/test)

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed Windows BitLocker encryption/decryption request loop on systems with secondary drives and auto-unlock.
  • New Features

    • Added BitLocker recovery key rotation capability, allowing safe key updates without full disk re-encryption.
    • Enhanced BitLocker protection status tracking to correctly display "Action required" when protection is disabled.

@getvictor
Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the Windows BitLocker encrypt/decrypt loop described in #40809 by improving how Fleet detects/represents BitLocker state and by changing orbit’s behavior for pre-encrypted disks.

Changes:

  • Server: Windows disk encryption detail query now ingests both conversion_status and protection_status, and persists bitlocker_protection_status in host_disks to better classify Verified vs Action required.
  • Server: Datastore API updated (SetOrUpdateHostDisksEncryption) to optionally persist BitLocker protection status; MySQL schema + migration added.
  • Orbit: When the OS volume is already encrypted, rotate/escrow a new recovery key instead of decrypting and re-encrypting.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
server/service/osquery_utils/queries.go Updates Windows disk encryption query + adds Windows-specific ingestion (conversion/protection status).
server/service/osquery_utils/queries_test.go Updates mock datastore signature for disk encryption ingestion tests.
server/fleet/datastore.go Extends datastore interface for BitLocker protection status.
server/mock/datastore_mock.go Updates mock datastore method signature and forwarding.
server/datastore/mysql/schema.sql Adds bitlocker_protection_status column and migration tracking updates.
server/datastore/mysql/migrations/tables/20260407120000_AddBitlockerProtectionStatusToHostDisks.go Adds MySQL migration for new host_disks column.
server/datastore/mysql/hosts.go Persists optional BitLocker protection status alongside encrypted flag.
server/datastore/mysql/microsoft_mdm.go Uses protection status in BitLocker status classification and adds better “action required” detail.
server/datastore/mysql/test.go + server/service/integration_test.go Updates call sites for new datastore method signature.
orbit/pkg/update/notifications.go Switches behavior for pre-encrypted disks to rotate recovery key instead of decrypting.
orbit/pkg/update/notifications_test.go Updates tests to cover key rotation flow.
orbit/pkg/bitlocker/bitlocker_worker_*.go Adds RotateRecoveryKey wiring on Windows and no-op stubs elsewhere.
orbit/pkg/bitlocker/bitlocker_management_windows.go Implements COM-thread recovery key rotation + protector cleanup.
changes/issue-40809-bitlocker-loop User-visible change note for server-side fix and UI status improvements.
orbit/changes/issue-40809-bitlocker-key-rotation User-visible change note for orbit behavior change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/service/osquery_utils/queries.go Outdated
Comment thread server/service/osquery_utils/queries.go Outdated
Comment thread orbit/pkg/update/notifications.go
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/service/osquery_utils/queries.go
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

Walkthrough

This PR implements a fix for a Windows BitLocker encryption/decryption loop on systems with secondary drives using auto-unlock. The solution introduces recovery key rotation for already-encrypted volumes, preventing repeated decryption attempts that fail with FVE_E_AUTOUNLOCK_ENABLED errors. Changes include new BitLocker key-protector management helpers on Windows, a rotation flow that adds new protectors and removes old ones without decrypting, updates to disk encryption detection logic to use conversion_status for determining encryption state, addition of a bitlocker_protection_status column to track protection status in the datastore, modifications to Windows MDM BitLocker status filtering logic, and updates to the disk encryption ingestion query to return both protection_status and conversion_status values.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly summarizes the main objective: preventing Fleet from attempting to decrypt drives that are already encrypted, directly addressing the reported BitLocker loop issue.
Linked Issues check ✅ Passed The PR successfully addresses issue #40809 by implementing BitLocker recovery key rotation for already-encrypted drives, preventing the encrypt/decrypt loop caused by FVE_E_AUTOUNLOCK_ENABLED errors on systems with secondary drives using auto-unlock.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the issue: BitLocker management (recovery key rotation), Windows disk encryption detection logic, database migration for BitLocker protection status tracking, and supporting test updates. No unrelated changes are present.
Description check ✅ Passed PR description includes detailed technical implementation notes, testing confirmation, and checklist items, addressing the issue and PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch victor/40809-bitlocker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
orbit/pkg/update/notifications.go (1)

613-615: ⚠️ Potential issue | 🟡 Minor

Remove unused decryptVolume method and execDecryptVolumeFn field.

The decryptVolume method at lines 613-615 is never called. The new rotation-based approach (lines 524-528) explicitly avoids decryption entirely. All tests verify that decryption is never invoked. Remove the unused method and the execDecryptVolumeFn field from the windowsMDMBitlockerConfigReceiver struct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@orbit/pkg/update/notifications.go` around lines 613 - 615, Remove the unused
decryptVolume method and the execDecryptVolumeFn field from the
windowsMDMBitlockerConfigReceiver: delete the decryptVolume(targetVolume string)
method and remove the execDecryptVolumeFn field declaration from the
windowsMDMBitlockerConfigReceiver struct (and any assignments in constructors or
initialization like where execDecryptVolumeFn might be set); ensure there are no
remaining references to execDecryptVolumeFn elsewhere (search for
execDecryptVolumeFn and decryptVolume) and run tests to confirm the
rotation-based code paths still pass.
🧹 Nitpick comments (5)
server/datastore/mysql/hosts_test.go (1)

10049-10063: Consider adding one non-nil coverage case for the new argument.

These updates compile and keep legacy behavior, but this test still exercises only the nil branch of bitlockerProtectionStatus. A single assertion with a concrete value would better guard regressions in the new persistence path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/datastore/mysql/hosts_test.go` around lines 10049 - 10063, The test
only covers the nil branch for the new bitlockerProtectionStatus argument;
update the test around SetOrUpdateHostDisksEncryption to add one call that
passes a non-nil concrete value (e.g., a pointer to a specific protection
status) for bitlockerProtectionStatus and assert the persisted
Host.DiskEncryptionEnabled and the stored bitlocker status when reloading via
ds.Host; modify the call that currently uses nil (for host2) to include a
non-nil value and add a require.Equal/require.True/require.False assertion
against the retrieved host's bitlocker protection status to exercise the non-nil
persistence path.
server/service/integration_core_test.go (1)

13600-13600: Consider adding one Windows case with non-nil BitLocker protection status.

This file now compiles against the new signature, but it only exercises the nil path. A single non-nil Windows assertion here would better guard the BitLocker-specific behavior this PR targets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/service/integration_core_test.go` at line 13600, Add a test case that
exercises the non-nil BitLocker path: call
s.ds.SetOrUpdateHostDisksEncryption(ctx, host.ID, true, &bitlockerStatus) using
a Windows host (set host.Platform = "windows") and a non-nil bitlockerStatus
value, then fetch the host/encryption record (e.g., via s.ds.Host or the
datastore read helper used elsewhere in this test file) and assert the stored
BitLockerProtectionStatus equals the value you passed; ensure you reference the
same host.ID and SetOrUpdateHostDisksEncryption function used in the existing
test so the new assertion covers the Windows non-nil branch.
server/service/osquery_utils/queries_test.go (1)

1956-1958: Assert bitlockerProtectionStatus is nil in non-Windows ingestion tests

These tests were correctly updated for the new method signature, but they currently don’t validate the new argument. Adding assert.Nil(t, bitlockerProtectionStatus) would harden the contract for non-Windows paths.

Suggested test hardening
 ds.SetOrUpdateHostDisksEncryptionFunc = func(ctx context.Context, id uint, encrypted bool, bitlockerProtectionStatus *int32) error {
   assert.Equal(t, expectEncrypted, encrypted)
+  assert.Nil(t, bitlockerProtectionStatus)
   return nil
 }

Also applies to: 1985-1987

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/service/osquery_utils/queries_test.go` around lines 1956 - 1958, The
mock SetOrUpdateHostDisksEncryptionFunc used in tests should also assert that
the new bitlockerProtectionStatus argument is nil for non-Windows ingestion
paths; update the anonymous function assigned to
ds.SetOrUpdateHostDisksEncryptionFunc (the mock that currently asserts
expectEncrypted) to include assert.Nil(t, bitlockerProtectionStatus) so the test
validates the new parameter is nil (apply the same change to the other
occurrence around lines referenced).
server/datastore/mysql/labels_test.go (1)

2110-2110: Consider adding explicit BitLocker protection-status coverage in this Windows test

Line 2110 now passes nil for bitlockerProtectionStatus, which updates the signature but doesn’t exercise the new Windows-specific status path. A follow-up subtest with a concrete protection status would better lock in the intended behavior for the new column/filtering logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/datastore/mysql/labels_test.go` at line 2110, The test currently calls
SetOrUpdateHostDisksEncryption(ctx, h1.ID, true, nil) which updates the
signature but doesn't exercise the Windows-specific bitlockerProtectionStatus
branch; add a follow-up subtest in labels_test.go that calls
SetOrUpdateHostDisksEncryption with a concrete non-nil bitlockerProtectionStatus
value (e.g. "protected" or the enum/const your code expects) for the same host
(h1.ID), assert NoError on the call, and then assert the host's disk encryption
label/state and any filtering behavior that depends on bitlockerProtectionStatus
to validate the new column/path is exercised (use the same helper assertions
used elsewhere in the file).
orbit/pkg/bitlocker/bitlocker_management_windows.go (1)

563-570: Consider logging TPM protector failures at a higher level.

When adding a TPM protector fails with an error other than ErrorCodeProtectorExists, it's logged at Debug level and the operation continues. This is likely intentional to handle pre-encrypted disks that may use different unlock mechanisms, but a non-TPM protected disk could leave the system in a less secure state (recovery key only).

Consider logging at Info or Warn level so administrators are aware when TPM protection cannot be added.

Suggested change to improve observability
 	// Ensure a TPM protector exists (some pre-encrypted disks may not have one).
 	if err := vol.protectWithTPM(nil); err != nil {
 		// ErrorCodeProtectorExists is expected if a TPM protector is already present.
 		var encErr *EncryptionError
 		if !errors.As(err, &encErr) || encErr.Code() != ErrorCodeProtectorExists {
-			log.Debug().Err(err).Msg("could not add TPM protector, continuing")
+			log.Info().Err(err).Msg("could not add TPM protector during key rotation, continuing without TPM protection")
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@orbit/pkg/bitlocker/bitlocker_management_windows.go` around lines 563 - 570,
The current handling in the vol.protectWithTPM(nil) block swallows
non-ErrorCodeProtectorExists errors at Debug level; update the logging so
failures to add a TPM protector (when errors.As(err, &encErr) && encErr.Code()
!= ErrorCodeProtectorExists) are logged at a higher level (Info or Warn) instead
of log.Debug() to improve observability while still continuing execution; locate
the vol.protectWithTPM(nil) call and the error handling that builds an
EncryptionError and replace the log.Debug().Err(err).Msg(...) invocation with
log.Info() or log.Warn() as appropriate, preserving the existing error context
and message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/datastore/mysql/microsoft_mdm.go`:
- Around line 884-885: The bitlocker_protection_status value 2 (unknown) is not
handled by the predicates, so encrypted hosts with status=2 fall through to a
failed state; update the predicates used by GetMDMWindowsBitLockerStatus by
modifying whereProtectionOn to include "= 2" (i.e., treat NULL, 1, or 2 as
accepted/unknown) and keep whereProtectionOff matching only explicit 0 values
(hd.bitlocker_protection_status IS NOT NULL AND hd.bitlocker_protection_status =
0) so status=2 is treated as unknown/accepted in the existing logic.

In `@server/service/osquery_utils/queries.go`:
- Around line 2651-2659: The code currently ignores strconv.ParseInt errors
causing empty/malformed osquery fields to be coerced to 0; change the logic in
the block that parses conversion_status and protection_status so you check the
returned errors from strconv.ParseInt: only set `encrypted` when parsing
conversion_status succeeds (if parsing fails, skip/abort the disk-encryption
update rather than treating it as decrypted), and only build and pass a non-nil
protectionStatus pointer to SetOrUpdateHostDisksEncryption when parsing
protection_status succeeds (if parsing protection_status fails, pass nil for the
protection status). Locate the parsing of
`conversion_status`/`protection_status` and the call to
SetOrUpdateHostDisksEncryption and add explicit error checks around
strconv.ParseInt for those symbols.

---

Outside diff comments:
In `@orbit/pkg/update/notifications.go`:
- Around line 613-615: Remove the unused decryptVolume method and the
execDecryptVolumeFn field from the windowsMDMBitlockerConfigReceiver: delete the
decryptVolume(targetVolume string) method and remove the execDecryptVolumeFn
field declaration from the windowsMDMBitlockerConfigReceiver struct (and any
assignments in constructors or initialization like where execDecryptVolumeFn
might be set); ensure there are no remaining references to execDecryptVolumeFn
elsewhere (search for execDecryptVolumeFn and decryptVolume) and run tests to
confirm the rotation-based code paths still pass.

---

Nitpick comments:
In `@orbit/pkg/bitlocker/bitlocker_management_windows.go`:
- Around line 563-570: The current handling in the vol.protectWithTPM(nil) block
swallows non-ErrorCodeProtectorExists errors at Debug level; update the logging
so failures to add a TPM protector (when errors.As(err, &encErr) &&
encErr.Code() != ErrorCodeProtectorExists) are logged at a higher level (Info or
Warn) instead of log.Debug() to improve observability while still continuing
execution; locate the vol.protectWithTPM(nil) call and the error handling that
builds an EncryptionError and replace the log.Debug().Err(err).Msg(...)
invocation with log.Info() or log.Warn() as appropriate, preserving the existing
error context and message.

In `@server/datastore/mysql/hosts_test.go`:
- Around line 10049-10063: The test only covers the nil branch for the new
bitlockerProtectionStatus argument; update the test around
SetOrUpdateHostDisksEncryption to add one call that passes a non-nil concrete
value (e.g., a pointer to a specific protection status) for
bitlockerProtectionStatus and assert the persisted Host.DiskEncryptionEnabled
and the stored bitlocker status when reloading via ds.Host; modify the call that
currently uses nil (for host2) to include a non-nil value and add a
require.Equal/require.True/require.False assertion against the retrieved host's
bitlocker protection status to exercise the non-nil persistence path.

In `@server/datastore/mysql/labels_test.go`:
- Line 2110: The test currently calls SetOrUpdateHostDisksEncryption(ctx, h1.ID,
true, nil) which updates the signature but doesn't exercise the Windows-specific
bitlockerProtectionStatus branch; add a follow-up subtest in labels_test.go that
calls SetOrUpdateHostDisksEncryption with a concrete non-nil
bitlockerProtectionStatus value (e.g. "protected" or the enum/const your code
expects) for the same host (h1.ID), assert NoError on the call, and then assert
the host's disk encryption label/state and any filtering behavior that depends
on bitlockerProtectionStatus to validate the new column/path is exercised (use
the same helper assertions used elsewhere in the file).

In `@server/service/integration_core_test.go`:
- Line 13600: Add a test case that exercises the non-nil BitLocker path: call
s.ds.SetOrUpdateHostDisksEncryption(ctx, host.ID, true, &bitlockerStatus) using
a Windows host (set host.Platform = "windows") and a non-nil bitlockerStatus
value, then fetch the host/encryption record (e.g., via s.ds.Host or the
datastore read helper used elsewhere in this test file) and assert the stored
BitLockerProtectionStatus equals the value you passed; ensure you reference the
same host.ID and SetOrUpdateHostDisksEncryption function used in the existing
test so the new assertion covers the Windows non-nil branch.

In `@server/service/osquery_utils/queries_test.go`:
- Around line 1956-1958: The mock SetOrUpdateHostDisksEncryptionFunc used in
tests should also assert that the new bitlockerProtectionStatus argument is nil
for non-Windows ingestion paths; update the anonymous function assigned to
ds.SetOrUpdateHostDisksEncryptionFunc (the mock that currently asserts
expectEncrypted) to include assert.Nil(t, bitlockerProtectionStatus) so the test
validates the new parameter is nil (apply the same change to the other
occurrence around lines referenced).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0482736d-9a22-49a3-a4ad-4fc888e372dc

📥 Commits

Reviewing files that changed from the base of the PR and between a8dae18 and 99b0d56.

📒 Files selected for processing (21)
  • changes/issue-40809-bitlocker-loop
  • orbit/changes/issue-40809-bitlocker-key-rotation
  • orbit/pkg/bitlocker/bitlocker_management_windows.go
  • orbit/pkg/bitlocker/bitlocker_worker_notwindows.go
  • orbit/pkg/bitlocker/bitlocker_worker_windows.go
  • orbit/pkg/update/notifications.go
  • orbit/pkg/update/notifications_test.go
  • server/datastore/mysql/hosts.go
  • server/datastore/mysql/hosts_test.go
  • server/datastore/mysql/labels_test.go
  • server/datastore/mysql/microsoft_mdm.go
  • server/datastore/mysql/microsoft_mdm_test.go
  • server/datastore/mysql/migrations/tables/20260407120000_AddBitlockerProtectionStatusToHostDisks.go
  • server/datastore/mysql/schema.sql
  • server/fleet/datastore.go
  • server/mock/datastore_mock.go
  • server/service/integration_core_test.go
  • server/service/integration_enterprise_test.go
  • server/service/integration_mdm_test.go
  • server/service/osquery_utils/queries.go
  • server/service/osquery_utils/queries_test.go

Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/service/osquery_utils/queries.go Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 85.54217% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.87%. Comparing base (c09800e) to head (dfae539).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
orbit/pkg/update/notifications.go 80.00% 4 Missing and 1 partial ⚠️
...9183610_AddBitlockerProtectionStatusToHostDisks.go 55.55% 3 Missing and 1 partial ⚠️
server/datastore/mysql/microsoft_mdm.go 92.59% 2 Missing ⚠️
orbit/pkg/bitlocker/bitlocker_worker_notwindows.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #43130   +/-   ##
=======================================
  Coverage   66.87%   66.87%           
=======================================
  Files        2588     2590    +2     
  Lines      207563   207603   +40     
  Branches     9321     9311   -10     
=======================================
+ Hits       138803   138840   +37     
- Misses      56123    56124    +1     
- Partials    12637    12639    +2     
Flag Coverage Δ
backend 68.65% <85.54%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@getvictor getvictor marked this pull request as ready for review April 7, 2026 19:25
@getvictor getvictor requested a review from a team as a code owner April 7, 2026 19:25
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@getvictor
Copy link
Copy Markdown
Member Author

@claude review

Comment thread server/datastore/mysql/microsoft_mdm.go
Comment thread orbit/pkg/update/notifications.go Outdated
Comment thread server/service/osquery_utils/queries.go
Comment thread server/datastore/mysql/microsoft_mdm.go
Comment thread orbit/pkg/update/notifications.go
Comment thread orbit/pkg/bitlocker/bitlocker_management_windows.go
Comment thread server/datastore/mysql/microsoft_mdm.go Outdated
Comment thread server/datastore/mysql/microsoft_mdm.go
@getvictor
Copy link
Copy Markdown
Member Author

getvictor commented Apr 9, 2026

@ksykulev Claude review keeps finding new issues and it takes ~45 minutes to review. I'll ping you when this is ready for re-review. Part of our issue is that we don't have unit tests that run on Windows. I'll plan on adding Windows to our CI.

Comment thread orbit/pkg/bitlocker/bitlocker_management_windows.go
Comment thread orbit/pkg/update/notifications_test.go
claude[bot]

This comment was marked as resolved.

Comment thread orbit/pkg/update/notifications.go
Comment thread orbit/pkg/update/notifications.go
Comment thread server/service/osquery_utils/queries_test.go
Copy link
Copy Markdown
Contributor

@sgress454 sgress454 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

host vitals doc matches new query 👍

Comment thread server/datastore/mysql/microsoft_mdm.go
@getvictor getvictor merged commit 5856385 into main Apr 9, 2026
65 of 66 checks passed
@getvictor getvictor deleted the victor/40809-bitlocker branch April 9, 2026 22:33
ksykulev pushed a commit that referenced this pull request Apr 13, 2026
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40809

**Orbit agent: key rotation replaces decrypt-then-re-encrypt:**
- When the disk is already encrypted, orbit now adds a new Fleet-managed
recovery key protector, removes old ones, and escrows the new key. The
disk is never decrypted.
- If key escrow fails, the rotated key is cached in memory and retried
on subsequent ticks without rotating again.
- Removes `DecryptVolume` and `decrypt()` (no longer called from
production code).

**Server: osquery query returns both protection_status and
conversion_status:**
- The `disk_encryption_windows` query now returns both columns instead
of just checking `protection_status = 1`. This lets the server correctly
identify a disk as encrypted via `conversion_status = 1` even when
`protection_status = 0`.
- New `directIngestDiskEncryptionWindows` function parses both values,
handles parse errors, and normalizes `protection_status = 2` (unknown)
to NULL.

**Server: new `bitlocker_protection_status` column and status logic:**
- Adds `bitlocker_protection_status` column to `host_disks` (DB
migration).
- When a disk is encrypted and key is escrowed but protection is off,
the host shows "Action required" with a detail message explaining the
issue, instead of misleadingly showing "Verified."
- `protection_status = 2` (unknown) and `NULL` (older orbit hosts) are
treated as protection on for backward compatibility.
- The `profiles_verified` and `profiles_verifying` branches in the
combined profiles+BitLocker summary now handle
`bitlocker_action_required`, counting those hosts as "pending".

Contributor docs updates: #43241
Public docs updates: https://github.com/fleetdm/fleet/pull/43243/changes

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

## Database migrations

- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.

## fleetd/orbit/Fleet Desktop

- [x] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [x] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [x] Verified that fleetd runs on macOS, Linux and Windows
- [x] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Fixed Windows BitLocker encryption/decryption request loop on systems
with secondary drives and auto-unlock.

* **New Features**
* Added BitLocker recovery key rotation capability, allowing safe key
updates without full disk re-encryption.
* Enhanced BitLocker protection status tracking to correctly display
"Action required" when protection is disabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows 11 BitLocker loop

4 participants