mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Fixed BitLocker encryption failing after migrating. (#41911)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33529 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] QA'd all new/changed functionality manually ## 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 commit is contained in:
parent
f1d9e93371
commit
6cc2836c20
4 changed files with 104 additions and 5 deletions
1
orbit/changes/33529-bitlocker-mdm-migration
Normal file
1
orbit/changes/33529-bitlocker-mdm-migration
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fixed BitLocker encryption failing with E_INVALIDARG after migrating Windows devices from another MDM. Fleet now reads the OSEncryptionType registry policy to use the correct encryption mode and cleans up stale key protectors from previous failed attempts.
|
||||
|
|
@ -36,6 +36,7 @@ const (
|
|||
|
||||
const (
|
||||
// Error Codes
|
||||
ErrorCodeInvalidArg int32 = -2147024809 // E_INVALIDARG: encryption flags conflict with Group Policy
|
||||
ErrorCodeIODevice int32 = -2147023779
|
||||
ErrorCodeDriveIncompatibleVolume int32 = -2144272206
|
||||
ErrorCodeNoTPMWithPassphrase int32 = -2144272212
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@
|
|||
package bitlocker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// Encryption Methods
|
||||
|
|
@ -30,7 +33,8 @@ const (
|
|||
type EncryptionFlag int32
|
||||
|
||||
const (
|
||||
EncryptDataOnly EncryptionFlag = 0x00000001
|
||||
EncryptDataOnly EncryptionFlag = 0x00000001
|
||||
// EncryptDemandWipe encrypts the entire disk, including (wiping) free space.
|
||||
EncryptDemandWipe EncryptionFlag = 0x00000002
|
||||
EncryptSynchronous EncryptionFlag = 0x00010000
|
||||
)
|
||||
|
|
@ -65,6 +69,8 @@ func encryptErrHandler(val int32) error {
|
|||
var msg string
|
||||
|
||||
switch val {
|
||||
case ErrorCodeInvalidArg:
|
||||
msg = "the encryption flags conflict with the current Group Policy settings (check HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE)"
|
||||
case ErrorCodeIODevice:
|
||||
msg = "an I/O error has occurred during encryption; the device may need to be reset"
|
||||
case ErrorCodeDriveIncompatibleVolume:
|
||||
|
|
@ -209,6 +215,18 @@ func (v *Volume) protectWithTPM(platformValidationProfile *[]uint8) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// deleteKeyProtectors removes all key protectors from the volume.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/secprov/deletekeyprotectors-win32-encryptablevolume
|
||||
func (v *Volume) deleteKeyProtectors() error {
|
||||
resultRaw, err := oleutil.CallMethod(v.handle, "DeleteKeyProtectors")
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleteKeyProtectors(%s): %w", v.letter, err)
|
||||
} else if val, ok := resultRaw.Value().(int32); val != 0 || !ok {
|
||||
return fmt.Errorf("deleteKeyProtectors(%s): %w", v.letter, encryptErrHandler(val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBitlockerStatus returns the current status of the volume
|
||||
// https://learn.microsoft.com/en-us/windows/win32/secprov/getprotectionstatus-win32-encryptablevolume
|
||||
func (v *Volume) getBitlockerStatus() (*EncryptionStatus, error) {
|
||||
|
|
@ -355,6 +373,53 @@ func getBitlockerStatus(targetVolume string) (*EncryptionStatus, error) {
|
|||
// Bitlocker Management interface implementation
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
// encryptionFlagFromRegistry reads the OSEncryptionType Group Policy registry value to determine
|
||||
// the encryption flag. Other MDM solutions may set this key to require full disk
|
||||
// encryption, and the key persists after unenrolling. If the policy requires full disk encryption,
|
||||
// we honor it; otherwise we default to used-space-only.
|
||||
//
|
||||
// Registry values for OSEncryptionType (under HKLM\SOFTWARE\Policies\Microsoft\FVE):
|
||||
// - 0: allow user to choose (default to used-space-only)
|
||||
// - 1: full disk encryption required
|
||||
// - 2: used-space-only encryption
|
||||
//
|
||||
// See https://learn.microsoft.com/en-us/windows/security/operating-system-security/data-protection/bitlocker/configure
|
||||
func encryptionFlagFromRegistry() EncryptionFlag {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Policies\Microsoft\FVE`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return EncryptDataOnly
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
val, _, err := k.GetIntegerValue("OSEncryptionType")
|
||||
if err != nil {
|
||||
return EncryptDataOnly
|
||||
}
|
||||
|
||||
if val == 1 {
|
||||
log.Info().Msg("OSEncryptionType registry policy requires full disk encryption, using full encryption mode")
|
||||
return EncryptDemandWipe
|
||||
}
|
||||
|
||||
return EncryptDataOnly
|
||||
}
|
||||
|
||||
// deleteOSEncryptionTypeRegistry removes the OSEncryptionType value from the FVE policy registry
|
||||
// key. This cleans up orphaned policy keys left by other MDM solutions after unenrolling.
|
||||
func deleteOSEncryptionTypeRegistry() {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Policies\Microsoft\FVE`, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
if err := k.DeleteValue("OSEncryptionType"); err != nil {
|
||||
log.Debug().Err(err).Msg("could not delete OSEncryptionType registry value")
|
||||
} else {
|
||||
log.Info().Msg("deleted orphaned OSEncryptionType registry policy value")
|
||||
}
|
||||
}
|
||||
|
||||
func encryptVolumeOnCOMThread(targetVolume string) (string, error) {
|
||||
// Connect to the volume
|
||||
vol, err := bitlockerConnect(targetVolume)
|
||||
|
|
@ -363,9 +428,41 @@ func encryptVolumeOnCOMThread(targetVolume string) (string, error) {
|
|||
}
|
||||
defer vol.bitlockerClose()
|
||||
|
||||
// Clean up stale key protectors (recovery passwords, TPM, etc.) that may be left over from
|
||||
// a previous failed encryption attempt or from another MDM solution. Without this, leftover
|
||||
// protectors cause prepareVolume to return ErrorCodeNotDecrypted and subsequent encryption
|
||||
// attempts to silently fail. Failures are logged but not fatal since a fresh volume won't
|
||||
// have any protectors to delete.
|
||||
if err := vol.deleteKeyProtectors(); err != nil {
|
||||
log.Debug().Err(err).Msg("could not delete existing key protectors (may not have any), continuing anyway")
|
||||
}
|
||||
|
||||
// Read the OSEncryptionType registry policy to determine the encryption flag. If a GPO or
|
||||
// another MDM set this to require full disk encryption (value 1), passing the wrong flag
|
||||
// to Encrypt() would fail with E_INVALIDARG. We honor the policy to avoid this conflict.
|
||||
// If the key is absent or any other value, we default to used-space-only (EncryptDataOnly).
|
||||
encFlag := encryptionFlagFromRegistry()
|
||||
|
||||
// Delete the registry key now that we've read it. If it was orphaned from a previous MDM,
|
||||
// this cleans it up permanently. In practice, customers should not use GPO for BitLocker
|
||||
// policy alongside Fleet MDM since the two will conflict. However, if an active GPO is present,
|
||||
// it will re-apply the key on its next refresh (~90 minutes). Orbit retries every ~30
|
||||
// seconds on failure, so interim retries before the GPO re-applies the key will default to
|
||||
// EncryptDataOnly and fail with E_INVALIDARG. That's expected and the error is reported to
|
||||
// Fleet. Once the GPO restores the key, the next retry reads it and succeeds.
|
||||
deleteOSEncryptionTypeRegistry()
|
||||
|
||||
// Prepare for encryption
|
||||
if err := vol.prepareVolume(VolumeTypeDefault, EncryptionTypeSoftware); err != nil {
|
||||
return "", fmt.Errorf("preparing volume for encryption: %w", err)
|
||||
// A previous failed encryption attempt may have already called PrepareVolume, which sets
|
||||
// BitLocker metadata on the volume. Calling it again returns ErrorCodeNotDecrypted
|
||||
// (FVE_E_NOT_DECRYPTED). This is safe to ignore because the volume is already prepared
|
||||
// and we can proceed with adding protectors and encrypting.
|
||||
var encErr *EncryptionError
|
||||
if !errors.As(err, &encErr) || encErr.Code() != ErrorCodeNotDecrypted {
|
||||
return "", fmt.Errorf("preparing volume for encryption: %w", err)
|
||||
}
|
||||
log.Debug().Msg("volume already prepared from previous attempt, continuing")
|
||||
}
|
||||
|
||||
// Add a recovery protector
|
||||
|
|
@ -379,8 +476,8 @@ func encryptVolumeOnCOMThread(targetVolume string) (string, error) {
|
|||
return "", fmt.Errorf("protecting with TPM: %w", err)
|
||||
}
|
||||
|
||||
// Start encryption
|
||||
if err := vol.encrypt(XtsAES256, EncryptDataOnly); err != nil {
|
||||
// Start encryption using the flag determined from the registry policy
|
||||
if err := vol.encrypt(XtsAES256, encFlag); err != nil {
|
||||
return "", fmt.Errorf("starting encryption: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -551,7 +551,7 @@ func (w *windowsMDMBitlockerConfigReceiver) attemptBitlockerEncryption(notifs fl
|
|||
}
|
||||
|
||||
if encryptionErr != nil {
|
||||
log.Error().Err(err).Msg("failed to encrypt the volume")
|
||||
log.Error().Err(encryptionErr).Msg("failed to encrypt the volume")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue