Introduction
In today’s digital landscape, safeguarding sensitive data is paramount. BitLocker, a built-in encryption feature in Windows, provides a robust layer of protection by encrypting drives. However, ensuring that recovery keys are securely backed up is crucial, especially for workplace-joined devices where users often lack local admin rights. This blog post delves into a solution we’ve implemented to address this challenge, detailing two PowerShell scripts designed to ensure all BitLocker keys, including those on workplace-joined devices, are backed up to Azure Active Directory (Azure AD).
The Challenge
The primary challenge addressed by this solution is the unique scenario posed by workplace-joined devices. These devices typically do not back up BitLocker keys to Azure AD. This is due to how the authentication process for backing up the keys occurs. Normally, a certificate issued to the device is used to authenticate to the API to which the keys are sent. Workplace-joined devices do not have this certificate, so we must back up the keys as the logged-on user. However, admin rights are required to retrieve the keys, making this impractical. Our solution circumvents this issue by first gathering key protector details using an account with local system rights (via Intune deployment), and then performing the backup using user-level certificates. This method ensures the backup of keys even when the user lacks admin rights, thereby enhancing data recovery capabilities.
The Solution
To address this issue, we developed a two-script solution:
Deploying the Scripts with Intune Proactive Remediations
To automate the execution of these scripts and ensure consistent backup of BitLocker keys, we deploy them using Intune Proactive Remediations. This method leverages Intune’s ability to execute PowerShell scripts under specific contexts, allowing us to overcome the challenges posed by workplace-joined devices.
Deployment Overview:
Detailed Steps for Deployment:
Steps:
Steps:
Future Enhancements
Currently, the code for using the BackupToAAD-BitLockerKeyProtector cmdlet for Azure AD joined or hybrid joined devices is commented out in the Is-Encrypted.ps1 script. We plan to conduct further testing and refine this process to ensure seamless integration and support for all device join scenarios. Feel free to uncomment this line and test yourself.
Conclusion
Data security is non-negotiable, and ensuring that all BitLocker recovery keys are backed up to Azure AD is a critical aspect of this. Our solution, while working around some unsupported scenarios, provides a comprehensive method to secure BitLocker keys for workplace-joined devices. By leveraging both system and user-level access, we ensure no key is left unbacked, safeguarding against data loss.
For those implementing similar solutions, it’s crucial to thoroughly test in your environment and understand the limitations and permissions required. Our approach provides a robust foundation, but customization may be necessary to fit specific organizational needs.
Feel free to reach out for any clarifications or further discussions on BitLocker key management and Azure AD integrations.
Legal Disclaimer
This solution is provided “as is” without any warranty or guarantee of any kind. The author is not liable for any damages or issues that arise from using this solution. It is the user’s responsibility to ensure that the solution is suitable for their environment and does not interfere with other processes or security policies.
This script should be executed in local system context.
<#
.SYNOPSIS
Checks for fully encrypted BitLocker volumes with a RecoveryPassword key protector and verifies if the key protectors are backed up.
.DESCRIPTION
This script scans all BitLocker-protected volumes on the system for fully encrypted volumes with a RecoveryPassword key protector.
It then checks if the key protectors have been backed up to Azure AD by searching for specific Event IDs in the event logs or
entries in a custom log file. If not backed up, the script writes the KeyProtectorIds and RecoveryPasswords to a text file
for further processing.
.AUTHOR
John Marcum (PJM)
@MEM_MVP
.LEGAL DISCLAIMER
This script is provided "as is" without any warranty or guarantee of any kind. The author is not liable for any damages or issues
that arise from using this script. It is the user's responsibility to ensure that the script is suitable for their environment
and that it does not interfere with other processes or security policies.
.PARAMETER None
This script does not require any parameters.
.EXAMPLE
.\Is-Encrypted.ps1
Runs the script to check for unbacked BitLocker key protectors and logs the results.
.NOTES
The script must be run with elevated privileges (local system) to access BitLocker key protectors.
It is designed to work around the limitation of workplace-joined devices not backing up BitLocker keys to Azure AD.
#>
function Is-Encrypted {
# Get all BitLocker volumes
$global:volumes = Get-BitLockerVolume
# Initialize a counter for fully encrypted volumes with unbacked RecoveryPassword key protector
$unbackedEncryptedVolumeCount = 0
# Initialize the file path for storing the KeyProtectorIds and RecoveryPasswords
$filePath = 'C:\Temp\RecoveryPasswords.txt'
$customLogFilePath = "C:\Temp\Backup_Log.txt"
# Check if the file exists, if so, delete it to start fresh
if (Test-Path $filePath) {
Remove-Item $filePath -Force
}
# Iterate through each volume
foreach ($volume in $volumes) {
# Check if the volume is fully encrypted
if ($volume.VolumeStatus -eq 'FullyEncrypted') {
# Get key protectors for the volume
$keyProtectors = $volume.KeyProtector
$MountPoint = $volume.MountPoint
# Check if any of the key protectors is of type "RecoveryPassword"
foreach ($protector in $keyProtectors) {
if ($protector.KeyProtectorType -eq 'RecoveryPassword') {
$keyProtectorId = $protector.KeyProtectorId
$isBackedUp = $false
# Check if the key protector has a backup event (EventID 845 or 846) or is in custom log file
try {
# Check for EventID 845
$BLBackupEvent845 = Get-WinEvent -ProviderName Microsoft-Windows-BitLocker-API -FilterXPath "*[System[(EventID=845)] and EventData[Data[@Name='ProtectorGUID'] and (Data='$keyProtectorId')]]" -MaxEvents 1 -ErrorAction Stop
if ($BLBackupEvent845) {
$isBackedUp = $true
}
# Check for EventID 846 if not already backed up
if (-not $isBackedUp) {
$BLBackupEvent846 = Get-WinEvent -ProviderName Microsoft-Windows-BitLocker-API -FilterXPath "*[System[(EventID=846)] and EventData[Data[@Name='ProtectorGUID'] and (Data='$keyProtectorId')]]" -MaxEvents 1 -ErrorAction Stop
if ($BLBackupEvent846) {
$isBackedUp = $true
}
}
}
catch {
Write-Host "Protector ID $keyProtectorId not found in event logs. Let's check the log file."
}
# Check the custom log file
if (-not $isBackedUp -and (Test-Path $customLogFilePath)) {
$customLogContent = Get-Content -Path $customLogFilePath -ErrorAction SilentlyContinue
$isBackedUp = $customLogContent -like "*$keyProtectorId*"
}
# Output the Protector ID and its backup status
if ($isBackedUp) {
Write-Host "Protector ID: $keyProtectorId backup status is: True"
}
else {
Write-Host "Protector ID: $keyProtectorId backup status is: False"
}
if (-not $isBackedUp) {
$unbackedEncryptedVolumeCount++
# Write KeyProtectorId and RecoveryPassword to the file
$recoveryPassword = $protector.RecoveryPassword
$entry = "KeyProtectorId: $($keyProtectorId), RecoveryPassword: $recoveryPassword"
Add-Content -Path $filePath -Value $entry
}
}
}
}
}
# Output the result based on the count of fully encrypted volumes with unbacked RecoveryPassword key protector
if ($unbackedEncryptedVolumeCount -ge 1) {
Write-Host "Encrypted volumes that have not been backed up were found. Details saved to $filePath"
}
else {
Write-Host "No encrypted volumes that have not been backed up were found. Exiting"
exit 0
}
}
# Check for encrypted drives and only save details if not backed up
Is-Encrypted
This script should be executed in the logged-on user context AFTER the Is-Encrypted.ps1 has been executed as local system. Note: Even running PowerShell as admin in the logged-on user context will break this! For manual testing just run the script without admin rights.
<#
.SYNOPSIS
Backs up BitLocker RecoveryPassword key protectors to Azure AD using user-level certificates.
.DESCRIPTION
This script reads a file containing unbacked BitLocker RecoveryPassword key protectors generated by another script.
It attempts to back up these protectors to Azure AD using the Invoke-WebRequest cmdlet, authenticated with a user certificate.
This approach addresses the challenge of backing up BitLocker keys for workplace-joined devices where users do not have local admin rights.
It uses a custom log file to track the backup status to avoid redundant backups.
.AUTHOR
John Marcum (PJM)
@MEM_MVP
.LEGAL DISCLAIMER
This script is provided "as is" without any warranty or guarantee of any kind. The author is not liable for any damages or issues
that arise from using this script. It is the user's responsibility to ensure that the script is suitable for their environment
and that it does not interfere with other processes or security policies.
.PARAMETER None
This script does not require any parameters.
.EXAMPLE
.\Backup-RecoveryPasswords.ps1
Runs the script to back up BitLocker key protectors to Azure AD.
.NOTES
The script should be run as the logged-in user who has access to the necessary certificates.
It processes the file generated by the preceding script to back up identified key protectors.
#>
# Start Logging
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$LogPath = "C:\Temp\Backup_BitLocker_Keys_$timestamp.log"
Start-Transcript -Path $LogPath
# Set the security protocol to TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Define the file path where the KeyProtectorIds and RecoveryPasswords are stored and the log for history to prevent redundant backups.
$filePath = 'C:\Temp\RecoveryPasswords.txt'
$customLogFilePath = "C:\Temp\Backup_Log.txt"
# Function to log messages to a custom log file
function Log-Message {
param (
[string]$message
)
Add-Content -Path $customLogFilePath -Value $message
}
# Check if the file exists
if (Test-Path $filePath) {
# Initialize an array to hold the KeyProtectorId and RecoveryPassword pairs
$keyProtectorArray = @()
# Read each line from the file
$fileContent = Get-Content -Path $filePath
# Iterate through each line
foreach ($line in $fileContent) {
# Use a regular expression to extract KeyProtectorId and RecoveryPassword
if ($line -match 'KeyProtectorId: (\S+), RecoveryPassword: (\S+)') {
# Create a custom object for each pair
$pair = [PSCustomObject]@{
KeyProtectorId = $matches[1]
RecoveryPassword = $matches[2]
}
# Add the object to the array
$keyProtectorArray += $pair
}
}
# Output the array to verify the contents
$keyProtectorArray
if ($keyProtectorArray) {
Write-Host 'We found keys to backup'
# Get the device details from dsregcmd /status
$DsregCmdStatus = dsregcmd /status
$DeviceId = $null
$WorkplaceDeviceId = $null
$TenantId = $null
$WorkplaceTenantId = $null
# Split the output into lines
$lines = $DsregCmdStatus -split "`r?`n"
# Loop through each line to find DeviceId, WorkplaceDeviceId, TenantId, and WorkplaceTenantId
foreach ($line in $lines) {
if ($line -match '^\s*DeviceId\s*:\s*([a-f0-9-]{36})\s*$') {
$DeviceId = $matches[1]
Write-Host "Found DeviceId: $DeviceId"
}
if ($line -match '^\s*WorkplaceDeviceId\s*:\s*([a-f0-9-]{36})\s*$') {
$WorkplaceDeviceId = $matches[1]
Write-Host "Found WorkplaceDeviceId: $WorkplaceDeviceId"
}
if ($line -match '^\s*TenantId\s*:\s*([a-f0-9-]{36})\s*$') {
$TenantId = $matches[1]
Write-Host "Found TenantId: $TenantId"
}
if ($line -match '^\s*WorkplaceTenantId\s*:\s*([a-f0-9-]{36})\s*$') {
$WorkplaceTenantId = $matches[1]
Write-Host "Found WorkplaceTenantId: $WorkplaceTenantId"
}
}
# Ensure we have at least one tenant ID to proceed
if (-not $TenantId -and -not $WorkplaceTenantId) {
Write-Output 'No valid Tenant IDs found'
Stop-Transcript
Exit 1
}
# Get the certs that match the device ID or workplace device ID
$certs = Get-ChildItem -Path cert:CurrentUser\My -Recurse | Where-Object {
$_.Subject -match "CN=$DeviceId" -or $_.Subject -match "CN=$WorkplaceDeviceId"
}
if ($certs.Count -eq 0) {
Write-Host "No valid certificates found matching the DeviceId or WorkplaceDeviceId."
Stop-Transcript
Exit 1
}
# Initialize success flag and error log
$allSuccessful = $true
$errorLog = @()
# Attempt backup based on matching certificate
foreach ($cert in $certs) {
# Extract the subject name from the certificate
$certSubject = $cert.Subject -replace "^CN=", "" # Remove the 'CN=' prefix if present
Write-Output "Checking certificate with subject: $certSubject"
# Determine the appropriate tenant and device based on the certificate subject
if ($certSubject -eq $DeviceId) {
$currentId = $DeviceId
$currentTenant = $TenantId
}
elseif ($certSubject -eq $WorkplaceDeviceId) {
$currentId = $WorkplaceDeviceId
$currentTenant = $WorkplaceTenantId
}
else {
Write-Host "No matching DeviceId or WorkplaceDeviceId for certificate subject: $certSubject"
continue
}
foreach ($protector in $keyProtectorArray) {
$body = @{
key = $protector.RecoveryPassword
kid = $protector.KeyProtectorId -replace '[{}]', ''
vol = "OSV"
} | ConvertTo-Json
# URL to send the post to
$url = "https://enterpriseregistration.windows.net/manage/$currentTenant/device/$($certSubject)?api-version=1.0"
try {
# Send the recovery information using Invoke-WebRequest
Write-Host "Sending data to: $($url)"
Write-Host "Using Cert: $($cert)"
Write-Host "With body: $($body)"
$req = Invoke-WebRequest -Uri $url -Body $body -UseBasicParsing -Method Post -Certificate $cert
Write-Host "Request return status: $($req.StatusCode)"
if ($req.StatusCode -eq 200) {
$successMessage = "Backup successful for protector ID: $($protector.KeyProtectorId)"
Write-Output $successMessage
Log-Message $successMessage
}
else {
$failureMessage = "Backup failed for protector ID: $($protector.KeyProtectorId). HTTP status code: $($req.StatusCode)"
Write-Output $failureMessage
$allSuccessful = $false
$errorLog += @{
URL = $url
Error = "HTTP status code: $($req.StatusCode)"
}
Log-Message $failureMessage
}
}
catch {
$errorMessage = "Failed to post recovery information for protector ID: $($protector.KeyProtectorId). Error: $_"
Write-Output $errorMessage
$allSuccessful = $false
$errorLog += @{
URL = $url
Error = $_.Exception.Message
}
Log-Message $errorMessage
}
}
}
# Only delete the file if all requests were successful
if ($allSuccessful) {
Write-Host "All backups were successful. Deleting the file."
Remove-Item $filePath -Force
Stop-Transcript
Exit 0
}
else {
Write-Host "Not all backups were successful. Retaining the file for further inspection."
Write-Host "Failed backups details:"
foreach ($errorDetail in $errorLog) {
Write-Host "URL: $($errorDetail.URL)"
Write-Host "Error: $($errorDetail.Error)"
}
Stop-Transcript
Exit 1
}
}
else {
Write-Host "No key protectors found in the file"
Stop-Transcript
Exit 0
}
}
else {
Write-Host "File not found: $filePath"
Stop-Transcript
Exit 0
}
Cookie | Duration | Description |
---|---|---|
cookielawinfo-checkbox-analytics | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics". |
cookielawinfo-checkbox-functional | 11 months | The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-others | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other. |
cookielawinfo-checkbox-performance | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance". |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |