Skip to content

Instantly share code, notes, and snippets.

@pkutzner
Created March 26, 2026 16:51
Show Gist options
  • Select an option

  • Save pkutzner/fb74a139645184dbb8e5eaa7443a6116 to your computer and use it in GitHub Desktop.

Select an option

Save pkutzner/fb74a139645184dbb8e5eaa7443a6116 to your computer and use it in GitHub Desktop.
Remote-enable Bitlocker.
function Enable-Encryption {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string[]]$ComputerName,
[Parameter(Mandatory=$false)]
[ValidateRange(1,100)]
[int]$ThrottleLimit = 20,
[Parameter(Mandatory=$false)]
[int]$TimeoutSeconds = 300
)
$ScriptBlock = {
param($Computer)
$result = [pscustomobject]@{
ComputerName = $Computer
RecoveryPassword = $null
Success = $false
Error = $null
}
try {
$TPM = Get-CimInstance -ComputerName $Computer -Namespace "root\cimv2\security\microsofttpm" -ClassName win32_tpm -ErrorAction Stop
} catch {
$result.Error = "$($Computer): Failed to connect or query TPM. $($_.Exception.Message)"
return $result
}
if (-not ($TPM | Invoke-CimMethod -MethodName IsReady).IsReady) {
$result.Error = "$($Computer): TPM not ready!"
return $result
}
try {
$ComputerDN = (Get-ADComputer -Identity $Computer -ErrorAction Stop).DistinguishedName
} catch {
$result.Error = "$($Computer): Failed to find computer in Active Directory. $($_.Exception.Message)"
return $result
}
[string[]]$KeyList = @()
$EncryptableVolume = Get-CimInstance -ComputerName $Computer `
-Namespace "root\cimv2\Security\MicrosoftVolumeEncryption" `
-ClassName Win32_EncryptableVolume `
-Filter "Driveletter = 'C:'"
$TPP = $null
$NP = $null
$ES = $EncryptableVolume | Invoke-CimMethod -MethodName GetConversionStatus
if ($ES.ConversionStatus -ne 0) {
$result.Error = "Drive is not fully decrypted"
return $result
}
if (-not $EncryptableVolume.IsVolumeInitializedForProtection) {
try {
$TPP = $EncryptableVolume | Invoke-CimMethod -MethodName ProtectKeyWithTPM -ErrorAction Stop
} catch {
$result.Error = "$($Computer): Failed to initialize TPM protector. $($_.Exception.Message)"
return $result
}
try {
$NP = $EncryptableVolume | Invoke-CimMethod -MethodName ProtectKeyWithNumericalPassword -ErrorAction Stop
} catch {
$result.Error = "$($Computer): Failed to initialize drive with numerical password. $($_.Exception.Message)"
return $result
}
if ($TPP.ReturnValue -ne 0) {
$result.Error = "$($Computer): TPM initialization request failed with return code: $($TPP.ReturnValue)"
return $result
}
if ($NP.ReturnValue -ne 0) {
$result.Error = "$($Computer): Numeric recovery password initialization request failed with return code: $($NP.ReturnValue)"
return $result
}
try {
$BackupResults = $EncryptableVolume | Invoke-CimMethod -MethodName BackupRecoveryInformationToActiveDirectory -Arguments @{VolumeKeyProtectorID=$NP.VolumeKeyProtectorID} -ErrorAction Stop
} catch {
$result.Error = "$($Computer): Failed to backup recovery key to AD."
return $result
}
if ($BackupResults.ReturnValue -ne 0) {
$result.Error = "$($Computer): Backup of recovery password to AD failed with return value: $($BackupResults.ReturnValue)"
return $result
}
$ADRecInfo = Get-ADObject -Filter {objectClass -eq 'msFVE-RecoveryInformation'} -SearchBase $ComputerDN
foreach ($item in $ADRecInfo) {
$itemName = $item.Name.Tostring()
$KeyList += $itemName.Substring($itemName.IndexOf('{'), $itemName.IndexOf('}') - $itemName.IndexOf('{') + 1)
}
if ($NP.VolumeKeyProtectorID -notin $KeyList) {
$result.Error = "$($Computer): Newly generated recvoery password not found in AD"
return $result
}
try {
$encryptionResults = $EncryptableVolume | Invoke-CimMethod -MethodName Encrypt -ErrorAction Stop
} catch {
$result.Error = "$($Computer): Failed to initiate encryption."
return $result
}
if ($encryptionResults.ReturnValue -ne 0) {
$result.Error = "$($Computer): Encryption request failed with return code: $($encryptionResults.ReturnValue)"
return $result
}
$passwords = foreach ($id in $NP.VolumeKeyProtectorID) {
$EncryptableVolume | Invoke-CimMethod -MethodName GetKeyProtectorNumericalPassword `
-Arguments @{VolumeKeyProtectorID=$id} -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty NumericalPassword
}
$result.RecoveryPassword = $passwords -join ' | '
$result.Success = $true
return $result
} else {
$result.Error = "$($Computer): Encryption already enabled."
return $result
}
}
#region Runspace Pool Setup
$SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$RunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $ThrottleLimit, $SessionState, $Host)
$RunspacePool.Open()
#endregion
# Kick off one PowerShell instance per computer
$Jobs = foreach ($Computer in $ComputerName) {
$PS = [System.Management.Automation.PowerShell]::Create()
$PS.RunspacePool = $RunspacePool
[void]$PS.AddScript($ScriptBlock).AddArgument($Computer)
[pscustomobject]@{
Computer = $Computer
PS = $PS
Handle = $PS.BeginInvoke()
StartTime = [datetime]::UtcNow
}
}
# Collect results as jobs complete, respecting timeout
$Results = foreach ($Job in $Jobs) {
$elapsed = ([datetime]::UtcNow - $Job.StartTime).TotalSeconds
# Wait for this job, but only up to the remaining timeout window
$remaining = [Math]::Max(0, $TimeoutSeconds - $elapsed)
$completed = $Job.Handle.AsyncWaitHandle.WaitOne([int]($remaining * 1000))
if ($completed) {
try {
$Job.PS.EndInvoke($Job.Handle)
} catch {
[pscustomobject]@{
ComputerName = $Job.Computer
RecoveryPassword = $null
Success = $false
Error = "$($Job.Computer): Job threw an exception. $_"
}
}
} else {
# Timed out — stop the runspace and report failure
$Job.PS.Stop()
[pscustomobject]@{
ComputerName = $Job.Computer
RecoveryPassword = $null
Success = $false
Error = "$($Job.Computer): Timed out after $TimeoutSeconds seconds."
}
}
$Job.PS.Dispose()
}
$RunspacePool.Close()
$RunspacePool.Dispose()
return $Results
}
Enable-Encryption -ComputerName ws2,ws3,ws4
#Here's a breakdown of the key changes for PS 5.1 compatibility:
#Runspace pool instead of ForEach-Object -Parallel — A RunspacePool is created with a minimum of 1 and a maximum of $ThrottleLimit concurrent runspaces. PowerShell automatically queues jobs beyond the limit, giving you the same throttling behavior as PS 7's -ThrottleLimit. Each computer gets its own [System.Management.Automation.PowerShell] instance added to the pool.
#BeginInvoke / EndInvoke async pattern — Jobs are started with BeginInvoke() (non-blocking) so all computers up to the throttle limit begin immediately. Results are then collected with EndInvoke(), which retrieves the output and re-throws any terminating errors, which are caught and converted into a structured failure object.
#Per-job timeout via AsyncWaitHandle.WaitOne() — Each job's wait handle is polled with a millisecond timeout calculated from the remaining time budget. If a machine doesn't finish in time, $Job.PS.Stop() is called and a timeout failure object is returned — no machine can hang the whole batch.
#foreach replaced ForEach-Object in the script block — PS 5.1 doesn't support return inside ForEach-Object to exit the enclosing function, so all pipeline-style loops that had early return paths were converted to standard foreach loops.
#Explicit disposal — Each [PowerShell] instance and the pool itself are disposed after use to avoid runspace leaks, which is especially important when processing large numbers of machines.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment