Enrolling Terraform Deployed AVD Session Hosts into Intune

Background / Requirements:

This post will describe the recent problem my team faced with enrolling Terraform deployed AVD session hosts into Intune.

Below is a summary of the high-level requirements for the wider AVD deployment.

  • Deploying AVD programmatically using Terraform through Azure DevOps Pipelines
  • Personal host pool only
  • All session hosts deployed directly from an Azure Marketplace Windows 10 Multisesson image (no custom images)
  • All session hosts are to be Azure AD joined only
  • All session hosts are to be enrolled in Intune for MDM (including app deployment)

Problem

The deployed session hosts would join Azure AD without issues, however, would not enrol in Intune.

Solution

The solution was simple in hindsight, however, admittedly took some head-scratching to get there.

To get to the solution we deployed a session host manually from the Azure portal and compared the resultant JSON from the Overview pane of the virtual machine, see below, to that of a session host deployed using Terraform.

In comparing the JSON output we found that the VM Extension used for the AAD Login for Windows had an additional setting block defined for MDM.

We updated the Terraform code block for the same VM Extension to include the missing settings block and deployed the session hosts, thankfully each session host auto-enrolled in Intune!

resource "azurerm_virtual_machine_extension" "AADLoginForWindows" {
    depends_on  = [
    azurerm_virtual_machine_extension.registersessionhost,
    ]
  name                 = "AADLoginForWindows"
  virtual_machine_id   = azurerm_windows_virtual_machine.vm.id
  publisher            = "Microsoft.Azure.ActiveDirectory"
  type                 = "AADLoginForWindows"
  type_handler_version = "1.0"
  auto_upgrade_minor_version = true

  settings = <<SETTINGS
    {
        "mdmId" : "0000000a-0000-0000-c000-000000000000"
    }
SETTINGS
}

Notable thanks to Chris Aitken, my AVD and DevOps SME for his efforts, and the hours sitting on Teams calls to get this fixed!

If you have any queries or questions, please reach out on Twitter or LinkedIn.

Thanks!

Using Intune to remotely install Powershell modules on enrolled devices

A few weeks ago I shared a post detailing how you could write the resultant output of an Intune pushed Powershell script to Azure Tables, you can read that post here, the use case that drove that post was a customer asking for explicit evidence that a particular Microsoft hotfix had been installed on all devices in their estate.

The main function of that script used the Az module to connect to the Azure table and write the data, however, in that script I made what is in hindsight a pretty significant oversight in that I assumed and therefore didn’t check that the Az module had been installed and imported, this meant it failed when running on the majority of users’ devices as they didn’t have the module installed.

Thank you to Nathan Cook who commented on that post asking that very question and making me realise the mistake, I’ve now added an addendum to that post advising as such.

Intune-InstallPSModules-Comment

To that end see the below script from Nickolaj Andersen’s Github repo that I’ve adapted to suit being deployed from Intune.

This script can either be used at the start of an individual script to check for the presence of any required modules or deployed separately, say as part of an Autopilot post-deployment sequence to push out commonly used modules.

#Start logging
Start-Transcript -Path "C:\Logs\InstallAzNew - $(((get-date).ToUniversalTime()).ToString("yyyyMMddThhmmssZ")).log" -Force

# Determine if the Az module needs to be installed
try {
Write-Host "Attempting to locate Az module"
$AzModule = Get-InstalledModule -Name Az -ErrorAction Stop -Verbose:$false
if ($AzModule -ne $null) {
Write-Host "Authentication module detected, checking for latest version"
$LatestModuleVersion = (Find-Module -Name Az -ErrorAction Stop -Verbose:$false).Version
if ($LatestModuleVersion -gt $AzModule.Version) {
Write-Host "Latest version of Az module is not installed, attempting to install: $($LatestModuleVersion.ToString())"
$UpdateModuleInvocation = Update-Module -Name Az -Scope CurrentUser -Force -ErrorAction Stop -Confirm:$false -Verbose:$false
}
}
}
catch [System.Exception] {
Write-Host "Unable to detect Az module, attempting to install from PSGallery"
try {
# Install NuGet package provider
$PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false

# Install Az module
Install-Module -Name Az -Scope AllUsers -Force -ErrorAction Stop -Confirm:$false -Verbose:$false
Write-Host "Successfully installed Az"
}
catch [System.Exception] {
Write-Host "An error occurred while attempting to install Az module. Error message: $($_.Exception.Message)" ; break
}
}

# Stop Logging
Stop-Transcript

This script should be run as the logged-on user, ensure this is set when creating the task in Intune, as below.

Intune-InstallPSModules-RunAsUser

Note, if you do use this script to deploy the entire Az module and not a subset such as Az.Network, be aware that it is pretty big and may take a while to download depending on environmental factors, such as available bandwidth etc.

The below screenshot shows the output of the transcript log written to the local device, note it shows that neither the Az module or NuGet package provider was installed so they were both pulled from the PSGallery and installed.

Intune-InstallPSModules-Log

For confidence during testing, you can see the script installing the many Az modules into C:\Program Files\WindowsPowershell\Modules

Intune-InstallPSModules-ModulesDir

A few quick tips for troubleshooting, not just for this script but for any you deploy via Intune.

1 > Don’t Wait

The default refresh and pull cycle of Intune (think GP refresh time for AD GPO’s) is 60 minutes but during development you’re going to want to push that script out fast. There are several ways to force a sync between Windows 10 and Intune, the quickest is definitely to restart the Microsoft Intune Management Extension service, this will force an immediate sync.

Intune-InstallPSModules-Service

2 > Run Script Locally

This may sound like an obvious one, but say you’re running a script like the one above to install a certain module and you’ll want to keep the testing environment that same, that is, you don’t want to run it on a different build of Windows 10, where you have full admin rights etc as this will increase the likelihood of false-positives.

To that end, Intune caches and executes a local copy of the script in C:\Program Files (x86)\Microsoft Intune Management Extension\Policies\Scripts – run that as the locally logged on user, maybe add the -WhatIf switch to simulate the results.

Note, the scripts won’t have the same friendly and informative name you saved them as, instead, they’re given the GUID name of the task in Azure.

Intune-InstallPSModules-SavedDir

3 > In The Registry We Trust

If you don’t subscribe to the practice of using Start and Stop-Transcript for logging you can use the registry to get the results of the script.

The key is HKLM\Software\Microsoft\MicrosoftIntuneManagementExtension\Policies

Again, as with the locally cached script, the key adopts the name of the task GUID, from there you can view the Result and ResultDetails values.

This is handy for development, however, I strongly suggest writing the out to verbose logs for Production.

Intune-InstallPSModules-Registry

Writing the results from an Intune executed Powershell script to Azure Table Storage

Edit 23/05/20: The script in this post omits a check for the presence of the required Powershell modules required to run certain cmdlets, please see new accompanying post for details on how to check for the presence, and install required modules remotely using Intune.

Anyone who has worked as a SysAdmin over the years managing either server or client estates, or both, and likely remotely, will have undoubtedly had to hack up a script of varying complexity to push out some sort of workaround or fix, be it adding a registry key or deleting old profiles to recoup some disk space.

That was all straight forward in ‘traditional’ Windows-based server-client environments were connectivity to the source was over a reliable LAN or WAN and the hardest part was remembering the correct syntax for PSEXEC, but that is no longer the case in the more modern of workplaces were devices are Azure AD joined and use the internet as their default communication medium.

The is where Intune (Endpoint Manager) comes in, the ability to remotely execute Powershell scripts to enrolled devices isn’t new and I’ve used it increasingly over the months to bridge a few small gaps, usually where a particular policy setting I needed wasn’t natively available, such as setting the properties of a local account that was created using an Intune policy, such as ‘password does not expire’ but I’ll cover that end-to-end in another post as it’s handy to know.

So, as mentioned, the actual pushing out of a script in Intune is pretty straight forward however centrally capturing the output isn’t. The Intune portal provides a very basic reporting function that displays whether the script was successfully deployed or not, that is to say, did it exit with defined exit code, but you do not get anything remotely verbose in the portal, for example, it does not capture any directed output.

So, in a decentralised environment with no common LAN or server infrastructure to write results too what are the options?

I did once unsuccessfully experiment trying to write to Azure Blob Storage and have contemplated similar using Azure Files and mapping drives within the script but for now, after consulting the MDM community on Twitter I came across a post by Travis Roberts that provided a great article on writing the output of a Powershell script to Azure Table Storage.

Note, the script that follows is directly adapted from Travis Roberts’ original blog post, so please check that out. Thanks, Travis.

Right, so let’s quickly cover the actual use-case here, why do I need to write a Powershell script and push it out via Intune and why am I so concerned with capturing the results?

The answer is I was contacted by a customer who as part of a wider Microsoft 365 adoption we deployed Intune for and was asked whether we could use Intune to quickly report on whether a particular Microsoft hotfix had been installed on all of their Windows 10 devices in response to a recent threat – the short answer was no we couldn’t.  Yes, we were using Intune to define Windows Updates but again the reporting was not detailed enough.

The result was the below, this script when executed searches for the presence of a defined hotfix by the KB it was wrapped in and writes the results to a very simple Table in Azure which I could use Storage Explorer to monitor and report from.

In readiness to utilise this script you must have already created a Table within an Azure Storage Account and generated a SAS key for a defined window.

I’ve used a generic Partition Key of KBCheck to satisfy the data integrity constraints.

# Step 1, use Start-Transcript to capture the execution to a text file and set variables for connecting to Azure Table

Start-Transcript -Path "C:\TempLogs\KB4541338Check-$(((get-date).ToUniversalTime()).ToString("yyyyMMddThhmmssZ")).log"

$storageAccountName = 'StorageAccountName'
$tableName = 'TableName'
$sasToken = 'SASKeyHere' 
$dateTime = get-date
$partitionKey = 'KBCheck'

# Step 2, Connect to Azure Table Storage
$storageCtx = New-AzureStorageContext -StorageAccountName $storageAccountName -SasToken $sasToken
$table = (Get-AzureStorageTable -Name $tableName -Context $storageCtx).CloudTable

# Step 3, Check for presence of hotfix
Write-Host "Checking for KB4541338"

if (get-hotfix -Id KB4541338) {
$PatchCheck = "Installed"
Write-Host "KB4541338 Installed"
} 
else {
$PatchCheck = "Not Installed"
Write-Host "KB4541338 Missing"
}
# Step 4, Write data to Table Storage and end transcript.

Write-Host "Writing to Azure Table $table"

Add-StorageTableRow -table $table -partitionKey $partitionKey -rowKey ([guid]::NewGuid().tostring()) -property @{
'LocalHostname' = $env:computername
'PatchStatus' = $PatchCheck
} | Out-Null

Stop-Transcript

The output of the script when viewed in Azure Storage Explorer is very simple, it shows the device hostname and either ‘Installed’ or ‘Not Installed’ depending on whether the KB was found.

This script can easily be used as a framework for capturing other simple text-based outputs using Intune and Powershell, simply update the body of the script in step 3.

Thanks again to Travis Roberts for the original script.

Understanding Intune Policies

This blog post will address, and hopefully, demystify a topic I struggled with when first starting out with Intune or Endpoint Manager to use its new moniker, and that is the difference between Configuration, Compliance and Security Policies and in which scenarios to use them.  So, let’s dig into it, I’ll cover each policy type in turn and in an order that should hopefully help tie the relationship between the policies together.

Intune-Policies

Configuration Policies

The best way to think of a Configuration Policy is as Intune’s implementation of Group Policy, in fact, Microsoft has engineered Configuration Policies in such a way as to allow you to import and utilise ADMX files in the same way you would with a traditional Group Policy Object.

Intune-Config

Configuration Policies are therefore what you would use to apply predefined settings to a user or device, such as defining a set homepage or other browser settings in IE and Edge (and even Chrome and other browsers, but that’s for another blog!) or enforce a custom desktop wallpaper or lock screen behaviour in Windows 10 and like Group Policy Objects, Configuration Policies can be applied to a targeted set of users or devices using groups within Azure AD.

Intune-Config2

Security Policies

Security Policies or Security Baselines as they are interchangeably referred to are pre-configured Windows settings that help you apply a known group of settings and default values that are recommended by Microsoft, that is to say, when you create a security baseline, you’re creating a template that consists of hundreds of individual Configuration Policies.

Intune-SecurityBaseline

Microsoft routinely releases a new Security Baseline which is a thorough pre-defined set of policies covering all facets of the target technology, such as Windows 10, that can be quickly and easily deployed to secure your environment.

Note, Security Baseline are extremely exhaustive and I would advise caution over adding them without careful testing, they are, however, extremely useful at locking down an environment to a given standard quickly.

Compliance Policies

Compliance Policies are used to evaluate a device’s compliance against a pre-defined baseline, such as the requirement for a device to be encrypted or to be within a defined minimum OS version.

Intune-Compliance2

Compliance Policies are a good tool for alerting on configuration drift, and when deployed alongside Conditional Access Policies can control what a device can and cannot access should it be deemed non-compliant, for example, non-compliant devices can be blocked from accessing corporately owned data.

Intune-Compliance

Summary

Each policy type when individually deployed correctly can add great value in securing a plethora of OS and device types, however, when configured and deployed together they can not only enforce an entire collection of settings championed by Microsoft but also provide the assurance that should a device fall foul of the required compliance baseline that device and the user using it would not be able to access and potentially but inadvertently open the company up to malicious exploit.

Finally, I’d highly recommend following Intune Training on YouTube where Steve and Adam (and others) share some great content on all things Intune.

Intune-Training

I also maintain a List on Twitter for the key folk I follow in the MDM space, feel free to follow that here.