Intune and Windows 10 supports automatic key rollover when a key has been used to unlock or recover a drive. This means the key in Azure AD will be automatically replaced with a new key after a successful recovery key usage. This is driven by the client and some policies in Intune. You can read more about that in Oliver’s post from last year. This is all great, but this only works when a key has actually been used for a recovery or unlock on a device.
Back on-premises, MBAM (Microsoft BitLocker Administration and Monitoring) had a great rollover function to make sure the recovery keys where one-time use only. That was often one of the main reason a lot of organizations implemented MBAM. Today, with Intune cloud management of Windows 10, some concerns are raised by customers about the fact that any user could simply go and get their own key from places like https://myaccount.microsoft.com. Take a screenshot or even worse tape it to their laptop “just in case”.
I decided to have a look to see how to mitigate this. What this post is looking to achieve is that each time a key has been exposed (read by user/admin) , Intune will perform a Bitlocker Key Rotation command on the device the key belongs to.
Requirements
- Azure AD Audit logs forwarded to Log Analytics
- Intune Audit Logs forwarded to Log Analytics
- Azure Subscription with an Azure Automation Account
- Azure Monitor
I will not go into how to forward logs into Log Analytics or how to setup that Azure Automation account and , Azure Monitor in this post, that have been covered in many earlier posts. So I presume you have these requirements in place. This picture explains what this solution is about.
Log Queries
A few log analytic queries are needed for this. First query Azure AD logs to find all the key exposures in your organization. If you don’t find any the last 24 hours choose a longer time period or expose a key for a device to get the entry.
AuditLogs | where OperationName contains "Read BitLocker key"
Here are some output examples from the last 7 days.
Then query the logs to find if any of these keys has been automatically rolled over after usage. To find that, simple ask Azure AD Logs for “Delete BitLocker key”
AuditLogs | where OperationName contains "Delete BitLocker key"
Then check if there has been already performed a Bitlocker Key rotation from Intune on these devices. The reason for that is that a key rotation action on a device is not actually deleting the key from Azure AD before AFTER the device has been rebooted. The key is replaced locally when the command runs and the reboot trigger the delete action on the old key in Azure AD.
IntuneAuditLogs | where OperationName == "rotateBitLockerKeys ManagedDevice"
Now that all the raw data needed, lets have a look on how to set this up and the Azure Automation script needed to perform the Bitlocker key rollover.
Azure Monitor
Run the first query (“Read BitLocker key”) in Log Analytics and click on +New Alert Rule. This opens up the Create alert rule blade where configuration is needed. First go to Condition and click by the red exclamation point.
In Configure signal logic set the threshold value to zero. Then set evaluation time and frequency to 60 minutes. It is important that evaluation time and frequency matches, but the recurrence can be changed.
Save the alert rule.
Azure Automation
Go to your Azure Automation account. The script requires that the automation account has a Azure Run As Account. If that not exists, create one.
There is a warning coming up when you click create that this will create a new service principal user in Azure Active Directory that will be assigned the Contributor role at the subscription level. So make sure you have the permissions to do that. This action will also create a Certificate assigned to this automation account that can be used for authentication to various services where the service principal user has permissions. The user/certtificate has a 1 year expiry date.
Resource Permissions
Now that the user has the role of Contributor in the subscription of the Automation account you must verify or add permission so that this user is allowed to read the logs from log analytics. The account needs the following permissions:
- Reader access to the subscription of the log analytics workspace
- Log Analytics Reader on the resource group of the log analytics workspace
These permissions are inherited down to the actual log analytics workspace. If all is in one Azure Subscription your user already should have the required permissions with the contributor role. Search for the name of your automation account, the service principal should start with the same name.
Microsoft Graph API Permissions
The Azure Automation runbook also requires permissions in Microsoft Graph API (Intune). Normally that means creating a new Azure AD App registration and create a client secret, but this time lets do something else.
Go to Azure AD – App Registrations and – All Applications. Search for the name of your automation account. As long as you have a Azure Run As Account created the app registration should be there.
Click on the name. Now you see this is just another Azure AD App Registration that can be given API permissions like any other self-created app. Click on API Permissions and Add Permission. Choose Microsoft Graph and Application Permission. Search for DeviceManagementManagedDevices.ReadWrite.All. That is the one API permission needed. (Remember to give Admin Consent)
Azure Automation Runbook
Go to github and download the runbook from here: BitlockerRemedy.ps1. Go to your Automation Account – Runbooks and click on Import a runbook. Now that the runbook itself is ready, we need to add the following to the automation account
- Add modules:
- Az.Accounts
- Az.Automation
- Az.OperationalInsights
- Az.Resources
- MSAL.PS
- Add Automation variable BitlockerRemedyWorkspaceID as a encrypted string variable and use the log analytics Workspace ID as the value.
Verify that you have AzureRunAsConnection under Connections and AzureRunAsCertificate under Certificate. If you did not create them now, make sure they have not expired.
The script
The script we are running will use the Azure Run AS Connection to connect to Azure Log Analytics to perform the 3 queries (a bit modified) from above to find keys that have been exposed the last hour, then compare these 3 results to decide whether or not a Bitlocker Key Rollover is required. If the rollover is required, a Intune action via Graph will perform the rollover command on the managed device from which the key belonged too. If you changed the recurrence in Azure Monitor, the script must be change to the same timespan.
Disable-AzContextAutosave –Scope Process $connection = Get-AutomationConnection -Name AzureRunAsConnection $certificate = Get-AutomationCertificate -Name AzureRunAsCertificate $connectionResult = Connect-AzAccount -ServicePrincipal -Tenant $connection.TenantID -ApplicationId $connection.ApplicationID -CertificateThumbprint $connection.CertificateThumbprint $GraphConnection = Get-MsalToken -ClientCertificate $certificate -ClientId $connection.ApplicationID -TenantId $connection.TenantID $Header = @{Authorization = "Bearer $($GraphConnection.AccessToken)"} [string]$WorkspaceID = Get-AutomationVariable -Name 'BitlockerRemedyWorkspaceID' #Define the query objects $ExposedKeysQuery = @' AuditLogs | where OperationName == "Read BitLocker key" and TimeGenerated > ago(65m) | extend MyDetails = tostring(AdditionalDetails[0].value) | extend userPrincipalName_ = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName) | parse MyDetails with * "key ID: '" MyRecoveryKeyID "'. Backed up from device: '" MyDevice "'" * | project MyDevice, MyRecoveryKeyID, userPrincipalName_, TimeGenerated '@ $DeletedKeysQuery = @' AuditLogs | where OperationName == "Delete BitLocker key" and TimeGenerated > ago(65m) | extend MyRecoveryKeyID = tostring(TargetResources[0].displayName) | project MyRecoveryKeyID, ActivityDateTime '@ $IntuneKeyRolloverQuery = @' IntuneAuditLogs | where OperationName == "rotateBitLockerKeys ManagedDevice" and TimeGenerated > ago(65m) | extend DeviceID = tostring(parse_json(tostring(parse_json(Properties).TargetObjectIds))[0]) | project DeviceID, ResultType '@ #Query Log Analytics Audit Logs $AllKeyExposures = Invoke-AZOperationalInsightsQuery -WorkspaceId $WorkspaceID -Query $ExposedKeysQuery $MyAutoKeyDeletion = Invoke-AZOperationalInsightsQuery -WorkspaceId $WorkspaceID -Query $DeletedKeysQuery $MyIntuneRolloverActions = Invoke-AZOperationalInsightsQuery -WorkspaceId $WorkspaceID -Query $IntuneKeyRolloverQuery $DeviceToRolloverIDs = @() foreach($KeyExposure in $AllKeyExposures.Results){ if ($KeyExposure.MyRecoveryKeyID -in $MyAutoKeyDeletion.Results.MyRecoveryKeyID){ #Write-Output "Device $($KeyExposure.MyDevice) with key $($KeyExposure.MyRecoveryKeyID) has been replaced OK" }elseif ($KeyExposure -notin $MyAutoKeyDeletion.Results.MyRecoveryKeyID) { #Write-Output "Device $($KeyExposure.MyDevice) with key $($KeyExposure.MyRecoveryKeyID) needs a rollover" $DeviceToRolloverIDs += $KeyExposure.MyDevice } } if ([string]::IsNullOrEmpty($DeviceToRolloverIDs)){ Write-Output "Query returned empty. Possibly issues with delay in query" } else { #Write-Output "Device to rollover IDs $DeviceToRolloverIDs" foreach($DeviceToRolloverID in $DeviceToRolloverIDs){ #write-output $DeviceToRolloverID $GetManagedDeviceIDUri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?filter=azureADDeviceID eq '$DeviceToRolloverID'" #Write-Output $GetManagedDeviceIDUri $ManagedDeviceResult = Invoke-RestMethod -Method GET -Uri $GetManagedDeviceIDUri -ContentType "application/json" -Headers $Header -ErrorAction Stop write-output "Evaluating $($ManagedDeviceResult.value.deviceName)" $ManagedDeviceID = $ManagedDeviceResult.value.id if ($ManagedDeviceID -notin $MyIntuneRolloverActions.Results.DeviceID){ $RolloverKeyUri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$ManagedDeviceID/rotateBitLockerKeys" $RolloverKeyResult = Invoke-RestMethod -Method POST -Uri $RolloverKeyUri -ContentType "application/json" -Headers $Header -ErrorAction Stop write-output "Recovery Key Rollover invoked on $($ManagedDeviceResult.value.deviceName)" } else { Write-Output "Intune Rollover has already been performed on $($ManagedDeviceResult.value.deviceName), no action needed" } } }
Create Azure Monitor Action Group
Go back to Azure Monitor and open the Alert rule you created earlier. (Manage alert rules). Then under Action Group click on Select action group, and on the slideout blade click on Create action group
Select the correct subscription and the resource group where your Azure Automation runbooks is and give your action group a fitting name.
Go to Actions and select Action Type Automation Runbook. This will open a selector to pick the Bitlocker Remedy runbook.
Now click on Review + Create and click Create on the last page. This connects the Azure Monitor alert to your runbook. Now we do not need to schedule the runbook as it will only run whenever someone has exposed a Bitlocker recovery key.
Test and Verify
Verify the solution!
https://myaccount.microsoft.com/device-list
Expose a key from the following url:Wait at least 5 minutes – Then run the ExposedKeysQuery from the script in Log Analytics
(Line 13-18 from script) – Note the RecoveryKeyID is the same as in step 1
Go to Azure Monitor – Depending on your schedule, the alert may take 60 minutes to trigger.
Go to Azure Automation and open your Runbook – verify the last job is recent and open it.
Click on Output
Check the Device in Endpoint Manager Portal
Verify in initiation in Event Viewer on your managed device
Reboot the device – Verify old key deleted in Eventviewer
Some times the key is deleted without a reboot, but to check quickly reboot the device.
In Endpoint Manager the Recovery Key should now be changed to a new Key ID
And the Bitlocker key rotation should be marked as completed
Check Audit logs in AAD Log Analytics to see the full rollover automation
The long time in the logs is because my test device was turned off when I exposed the key. Device needed to be turned on and then receive the Action command from Intune.
Conclusion
With this automation on top of the normal client driven bitlocker recovery key rollover, I think we have a more complete solution. Any time a user exposes a key, the key will be replaced within 1 hour as long as the device is connected to the network. The only caveat now is if a local admin user grabs the key locally on a running device. Another good reason to avoid having users with local admin privileges.
Hope this solution give you value. If you have feedback, feel free to comment on this post.
Hi Jan,
We are co-managing our Win10 workstations with SCCM and Intune. Currently we are using MBAM to store our bit locker keys. We are interested in migrating our keys from MBAM over to Intune. To test, I moved the endpoint protection workflow in SCCM to Pilot and created a pilot collections for staging this workflow. In Intune, I created an endpoint protection policy for bitlocker. Created and Azure AD group to assign the endpoint policy to. And made the test workstations a member of both SCCM collections and Azure Ad group. Force the workstation to check SCCM policy and waited. After 2 minutes, I can see in Intune that the workstation was applying the endpoint protection workflow as instructed by SCCM. I waited to see if the bit locker key would be moved to Intune. After waiting for 20 minutes, I decided to manually rollover the key using Intune. Within 2 minutes, the key was generated and stored in Intune.
My question is as follows, is there a way to dynamically force the rollover of the bitlocker keys for all workstations? Our plan is to move the workflow to Intune but need a method to also force the workstations to rollover their bitlocker key.
Either you have to do a rollover like you propose your self, or you need to push out a PowerShell script to handle the key escrow to Azure AD in this scenario
Perfect!
great work! Question, does this process have a check/balance to verify the new key is truly uploaded AAD?
As long as your bitlocker policies require storage of keys in AAD, it will require that the new key is in fact stored in AAD before it does the actual switch locally.
super cool!