Updated Modern Driver/BIOS Management with CMG Support

CharlesEndpoint Management, MECM/MEMCM/SCCM, Powershell, Task Sequence16 Comments


This is a long and overdue update on a solution I started working on last year to allow my organization to use the modern driver management solution without the need of the custom webservice. I also wanted the solution to use the built-in task sequence steps as much as possible to allow other administrators to customize the solution to their need without the need to go modify a big PowerShell script.

You can read the original blog posts here:

Modern Driver Management with the Administration Service

Modern BIOS Management with the Administration Service

Notable improvements

  1. The solution now works for clients on the internet by querying the AdminService over a cloud management gateway (CMG)
    Bonus: It works in WinPE! You can do a complete reinstallation of a device over CMG using this solution to dynamically apply bios updates and drivers at the same time during the task sequence.

  2. The solution now supports 7-Zip and Wim driver packages
  3. Improvements to handle driver packages using the DisplayVersion property instead of ReleaseId (ex: 20H2 instead of 2009)

Prerequisites to use the solution over CMG

A working CMG

There are multiple blog articles and guides online to help you set up your CMG. Here are some links that helped me:

Personally, for my lab, I used the Let’s Encrypt Cloud Management Gateway blog post by Nathan Ziehnert to set up my CMG.

Additional configuration to use the AdminService over CMG

There is some additional configuration that is needed to be able to query the AdminService over the cloud management gateway.

Once again, another one of Nathan’s blog post helped me: Securing Access to the ConfigMgr AdminService Over Cloud Management Gateway

There are 2 things I had to do differently in the Authentication section:

How to query the AdminService over CMG?

I started my research by reading the blog post from Sandy Zeng here: https://msendpointmgr.com/2019/07/16/use-configmgr-administration-service-adminservice-over-internet/

Also, at the same time, when I was looking at my application registration in the Azure Portal I saw this notification:

After inspecting Sandy’s script, I confirmed that it was using Azure Active Directory Authentication Library (ADAL), so I wanted to make my solution work with MSAL instead of ADAL.

After doing some research, I found the perfect PowerShell module to help me: MSAL.PS

With the help of that module, I was easily able to get an access token that is needed to query the AdminService over CMG.

Using the MSAL.PS module

Here is a sample script I used to test retrieving an access token with the MSAL.PS module:

And here is what the result looks like:

Now that I have an access token, I can easily query the AdminService over CMG. I reused the same part of the code used by Sandy to do this:

In the example above, I was successfully able to retrieve information for 36 packages from the AdminService over CMG.

Improvements to Invoke-GetPackageIDFromAdminService

Now that I knew what information I needed to use to query the AdminService over CMG, I was ready to make the necessary modifications to my script.

1) New parameters and ParameterSets

Depending on if the client is on the internet or not, my script would require a different set of parameters. This was the perfect opportunity to use parameter sets.

[parameter(Mandatory = $true, ParameterSetName = "Intranet")]

[parameter(Mandatory = $true, ParameterSetName = "Internet")]

[parameter(Mandatory = $true, ParameterSetName = "Internet")]

[parameter(Mandatory = $true, ParameterSetName = "Internet")]

[parameter(Mandatory = $false, ParameterSetName = "Internet")]
[string]$ApplicationIdUri = 'https://ConfigMgrService',

[parameter(Mandatory = $false, ParameterSetName = "Intranet")]
[parameter(Mandatory = $true, ParameterSetName = "Internet")]

[parameter(Mandatory = $false, ParameterSetName = "Intranet")]
[parameter(Mandatory = $true, ParameterSetName = "Internet")]

[parameter(Mandatory = $false, ParameterSetName = "Intranet")]
[parameter(Mandatory = $false, ParameterSetName = "Internet")]
[bool]$BypassCertCheck = $false,

2) Verify, Install and Import the MSAL.PS module

I wrote a function to handle installing & importing the MSAL.PS.

The tricky part here is that the MSAL.PS module requires us to accept the license agreement before we can install the module. There is a parameter -AcceptLicense for the Install-Module function but it is only available in the PowerShellGet module version 2 or higher.

This function checks for the availability of the MSAL.PS module and if it’s not there it will check for the prerequisites to install before it can import the module.

Function Import-MSALPSModule{
    Add-TextToCMLog $LogFile "Checking if MSAL.PS module is available on the device." $component 1
    $MSALModule = Get-Module -ListAvailable MSAL.PS
        Add-TextToCMLog $LogFile "Module is already available." $component 1
        #Setting PowerShell to use TLS 1.2 for PowerShell Gallery
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        Add-TextToCMLog $LogFile "MSAL.PS is not installed, checking for prerequisites before installing module." $component 1
        Add-TextToCMLog $LogFile "Checking for NuGet package provider... " $component 1
        If(-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)){
            Add-TextToCMLog $LogFile "NuGet package provider is not installed, installing NuGet..." $component 1
            $NuGetVersion = Install-PackageProvider -Name NuGet -Force -ErrorAction Stop | Select-Object -ExpandProperty Version
            Add-TextToCMLog $LogFile "NuGet package provider version $($NuGetVersion) installed." $component 1
        Add-TextToCMLog $LogFile "Checking for PowerShellGet module version 2 or higher " $component 1
        $PowerShellGetLatestVersion = Get-Module -ListAvailable -Name PowerShellGet | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version       
        If((-not $PowerShellGetLatestVersion)){
            Add-TextToCMLog $LogFile "Could not find any version of PowerShellGet installed." $component 1
        If(($PowerShellGetLatestVersion.Major -lt 2)){
            Add-TextToCMLog $LogFile "Current PowerShellGet version is $($PowerShellGetLatestVersion) and needs to be updated." $component 1
        If((-not $PowerShellGetLatestVersion) -or ($PowerShellGetLatestVersion.Major -lt 2)){
            Add-TextToCMLog $LogFile "Installing latest version of PowerShellGet..." $component 1
            Install-Module -Name PowerShellGet -AllowClobber -Force
            $InstalledVersion = Get-Module -ListAvailable -Name PowerShellGet | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version
            Add-TextToCMLog $LogFile "PowerShellGet module version $($InstalledVersion) installed." $component 1
        Add-TextToCMLog $LogFile "Installing MSAL.PS module..." $component 1
        If((-not $PowerShellGetLatestVersion) -or ($PowerShellGetLatestVersion.Major -lt 2)){
            Add-TextToCMLog $LogFile "Starting another powershell process to install the module..." $component 1
            $result = Start-Process -FilePath powershell.exe -ArgumentList "Install-Module MSAL.PS -AcceptLicense -Force" -PassThru -Wait -NoNewWindow
            If($result.ExitCode -ne 0){
                Add-TextToCMLog $LogFile "Failed to install MSAL.PS module" $component 3
                Throw "Failed to install MSAL.PS module"
            Install-Module MSAL.PS -AcceptLicense -Force
    Add-TextToCMLog $LogFile "Importing MSAL.PS module..." $component 1
    Import-module MSAL.PS -Force
    Add-TextToCMLog $LogFile "MSAL.PS module successfully imported." $component 1

What about WinPE? PowerShell Gallery does not work in WinPE!

You are right, by default PowerShell Gallery does not work in WinPE. To solve this issue, you have 2 options:

  1. Add PowerShell Gallery support to your boot image:
    • See this post by David Segura on how it can be achieved: https://www.osdeploy.com/blog/2021/winpe-powershell-gallery
    • The easiest way to add PowerShellGallery support to your boot image is to use a Universal WinPE that can be created with the OSD PowerShell module. Use this as a base for your boot image in Configuration Manager and you will be able to install modules from PSGallery directly in WinPE.
  2. Manually install the MSAL.PS module during the task sequence before you run the script to query the AdminService:

a) Create a package with the saved module and distribute it to your CMG

b) Edit the package in the “Manually install MSAL.PS module” step:

Both options will allow you to install and import the MSAL.PS module in WinPE.

If you decide to go with enabling support for PowerShell Gallery in WinPE, you can disable the step to manually install the module in the task sequence.

3) Querying the AdminService

Now, depending on what parameters were used, we will query the AdminService locally or via the CMG:

        $Packages = Invoke-RestMethod -Method Get -Uri $WMIPackageURL -Body $Body @InvokeRestMethodCredential | Select-Object -ExpandProperty value
        $authHeader = @{
            'Content-Type'  = 'application/json'
            'Authorization' = "Bearer " + $token.AccessToken
            'ExpiresOn'	    = $token.ExpiresOn
        $Packages = Invoke-RestMethod -Method Get -Uri $WMIPackageURL -Headers $authHeader -Body $Body | Select-Object -ExpandProperty value

The rest of the script to filter and select the most suitable package did not change.

Improvements to the task sequence

Now that the script itself supports querying over CMG, I needed to make some adjustments to the “Query AdminService for PackageID” task sequence to properly call the script with the right parameters.

1) Is the client on Internet?

We need to determine if the device running the task sequence is on Internet or on the corporate network (intranet).

First, we try using the _SMSTSIsClientOnInternet task sequence variable:

Note: In my limited testing, I found that if a task sequence does not reference any package, this variable may not be set.

If the “ClientIsOnInternet” is still not set after this step and device is not in WinPE, we use the following PowerShell command to determine if the device is on Internet:

Get-CimInstance -ClassName "ClientInfo" -Namespace "Root\CCM" | Select-Object -ExpandProperty InInternet

2) Additional Parameters for Internet clients

Here you fill out your environment-specific information. The additional parameters needed for CMG support are:

  • ExternalUrl: The ExternalUrl to access the AdminService from your CMG. The followinfg query can be used to find the ExternalUrl:
SELECT ProxyServerName,ExternalUrl FROM [dbo].[vProxy_Routings] WHERE [dbo].[vProxy_Routings].ExternalEndpointName = 'AdminService'
  • TenantId: Your Azure AD Tenant ID
  • ClientId: The Client ID of the application registration that you created to interact with the AdminService. See additional configuration to use the AdminService over CMG for details.
  • ApplicationIdUri: The application ID Uri for your application registration. The default value of “https://ConfigMgrService” will probably be OK for most people.

3) Running the script with the right parameters

Depending on the value of the “ClientIsOnInternet” variable, we run the script with different parameters.

If the client is determined to be intranet, use the intranet parameters:

If the client is on Internet, use the internet parameters:

The end result

You can now use the Modern Driver Management and Modern BIOS Management solution for Internet clients just like you are doing with your intranet clients.

If you are on Current Branch 2010 or later, you can use a boot media to run bare metal deployment on the Internet and still use the Modern Driver/BIOS Management solution.

How do I set this up in my environment?

  1. Download the task sequences here.
  2. Import the task sequences in your environment
  3. Configure the parameters correctly in the “Query AdminService for PackageID” task sequence:
  1. Add the “Apply Driver Package” and/or “Apply BIOS Package” task sequences in your existing task sequences.

For example on what you can do with these task sequences, see my previous blog posts:

What if I have multiple CMGs?

There is nothing stopping you from writing a small script to detect/check which CMG to use and then set the “AS_ExternalUrl” variable to whatever you want.

Now you have support for multiple CMGs 🙂

A note on the task sequence size

Currently, the scripts used in this solution are added directly in the task sequence instead of referencing a package containing the scripts. As you can see below, the task sequence size can be somewhat big:

If you are concerned about the total size of the task sequence, you could store the scripts in a package instead and this would greatly reduce the size of the task sequence.

Reference regarding task sequence size: https://docs.microsoft.com/en-us/mem/configmgr/osd/deploy-use/manage-task-sequences-to-automate-tasks#reduce-the-size-of-task-sequence-policy


Task Sequence Exports

MDM-AdminService GitHub repository

16 Comments on “Updated Modern Driver/BIOS Management with CMG Support”

    1. Hello, I like your modifications to make it more generalized, it’s a great idea. I’m glad my solution can help you!

  1. I have some security concerns about providing the username and password in the task sequence. What is your thoughts on the security?

    1. Hi Joakim,
      There are a few things that you can do to mitigate the risks.
      First, limiting the access of the service account being used. You want to create a service account that has the minimal rights needed to do the job. In our case, we only want the account to be able to “Read” SMS_Packages, that’s it. You could even restrict further by assigning a specific scope to BIOS and Driver packages and then limiting the role assigned to that service account to only this scope. This is on top of the regular stuff you do with service accounts (do not allow interactive logon, etc.)
      Second, in the task sequence itself, you want to make sure that the password variable is hidden and, in our case, we also want the “OSDLogPowerShellParameters” TS variable to be false or you will see all the parameters used when running the powershell script, including the user’s password in smsts.log
      At the end of the day, there’s always a risk but you try to mitigate it as much as you can.
      Let me know if that answers your concerns.

      1. Hi, i will follow your least privilege recommendations thanks. Another question: when i run the Apply Bios TS i get an error saying Unable to locate the flash64w.exe utility and Unable to determine bios package path. Looking at the ccmcache i cannot find the package either can i find it under _SMSTaskSequence. Do you know what this can be caused by?

        1. Forgot to add that i am testing over internet with CMG on version ConfigMgr 2010. Running internally works.

          1. I would look at what package was returned when querying the AdminService (you should be able to see that in the GetPackageID_BiosPackage.log file) and make sure it’s distributed to your CMG.
            Else than that, it’s hard to tell without looking at the smsts.log file to find out what went wrong.
            You could also try using the task sequence debugger and go step by step and see what happens at the download package step.

  2. Hi

    Apologies if this is in the article and I have missed something already… 🙂

    Would this support a reset scenario from SCCM to support Autopilot Provisioning? The clients, are remote have rebooted to WinPE, and need to connect to the CMG in WinPE over the internet?

    We can connect to the CMG to download the content if set to download as needed, but would like to be able to precache the Boot disk, OS, and if possible the drivers. Happy to download the drivers as needed during the reset, if the precaching option with the invoke command is not an option.

    As expected, the first run of the modern management solution, ‘out of the box’, has failed as it cannot reach the on-prem AdminService.

    We are currently precaching content before the TS starts, and need to avoid the requirement to cache ALL drivers, re: amount of content, and disk space issues required to support it re: traditional methods and capabilities with SCCM.


    1. Hi Lee, sorry this might not answer your question but if you are looking at doing a wipe & load of machines remotely I would look at maybe using OSDCloud. This is essentially doing OSD over the internet and fetching the OS image & drivers directly from the vendors instead of using your CMG and allows you to do autopilot too.

  3. I get EXIT 1 by running the Invoke-GetPackageIDFromAdminService – Intranet

    6/15/2022 1:17 PM,92,Invoke-GetPackageIDFromAdminService – Intranet,Get PackageID,The task sequence execution engine failed executing an action,11135,1,… etting URL = http://exampleserver, Ports = 80,443, CRL = false
    Setting Server Certificates.
    Setting Authenticator.
    Setting Media Certificate.
    Sending StatusMessage
    Setting the authenticator.
    CLibSMSMessageWinHttpTransport::Send: WinHttpOpenRequest – URL: exampleserver:80 CCM_POST /ccm_system/request
    Not in SSL.
    Request was successful.
    PowerShell script file created successfully
    Running PowerShell script generated in temporary folder ‘X:\Windows\TEMP\SMSTSPowerShellScripts’ with execution policy: ‘Bypass’
    Will run PowerShell script under SYSTEM account
    PowerShell path: X:\Windows\system32\windowspowershell\v1.0\powershell.exe
    Working dir ‘not set’
    Command line for extension .exe is “%1” %*
    Set command line: Run PowerShell Script
    PowerShell command line is NOT shown (‘OSDLogPowerShellParameters’ is NOT set to ‘True’)
    Executing command line: Run PowerShell Script with options (0, 4)
    Process completed with exit code 1
    PowerShell command line returned code 1

    Error -2146233079 The remote server returned an error: (400) Bad Request

    1. Hi James, the script creates its own log file in the same directory as the TS log files. Could you take a look and see if anything in that log file can help you?
      By default, the log file name is GetPackageID_DriverPackage.log or GetPackageID_BIOSPackage.log

  4. I got the same Error -2146233079 The remote server returned an error: (400) Bad Request after the SCCM Upgrade to 2203
    Exit Code 1
    Invoke-RestMethod : {“error”:{“code”:”400″,”message”:”Invalid OData Uri:

    It looks like the Invoke-RestMethod GET method does not recognize the body anymore. I solved the issue by removing the Body and changing the paremeters in the Invoke-RestMethod line 466:

    $Body = @{

    “`$filter” = $Filter

    “`$select” = “Name,Description,Manufacturer,Version,SourceDate,PackageID”



    $Packages = Invoke-RestMethod -Method Get -Uri $WMIPackageURL -Body $Body @InvokeRestMethodCredential | Select-Object -ExpandProperty value

    $Packages = Invoke-RestMethod -Method Get -Uri $WMIPackageURL @InvokeRestMethodCredential | Select-Object -ExpandProperty value
    $authHeader = @{
    'Content-Type' = 'application/json'
    'Authorization' = "Bearer " + $token.AccessToken
    'ExpiresOn' = $token.ExpiresOn

    $Packages = Invoke-RestMethod -Method Get -Uri $WMIPackageURL -Headers $authHeader -Body $Body | Select-Object -ExpandProperty value

    $Packages = Invoke-RestMethod -Method Get -Uri $WMIPackageURL -Headers $authHeader | Select-Object -ExpandProperty value

    1. Hi Ditor,

      If I understand correctly, you simply removed the “body” part of the Invoke-Restmethod? The thing is the body contains the filter part to make sure you are only querying for drivers or BIOS packages. If you remove that part, your task sequence might fail because it might be trying to apply drivers as a BIOS package or vice-versa.
      Unfortunately, I don’t have a lab at the moment to test this out.
      Can you confirm that this was working before you upgraded to version 2203?
      Thank you.

  5. Hi Charles,

    OK, now I understand that it makes sense since I only used the BIOS method. Yes, before the upgrade it worked, after the upgrade to 2203 it didn’t. I modified the PS code a bit, removed the lines 449 – 483 so that the GET query is executed without the body, and then filtered the Drivers/BIOS – I still need to test it though, since I have only executed the query manually.
    If you have an opinion on this, please share it.

    $webData = Invoke-RestMethod -Method Get -Uri $WMIPackageURL @InvokeRestMethodCredential | Select-Object -ExpandProperty value
    $authHeader = @{
    'Content-Type' = 'application/json'
    'Authorization' = "Bearer " + $token.AccessToken
    'ExpiresOn' = $token.ExpiresOn
    $webData = Invoke-RestMethod -Method Get -Uri $WMIPackageURL -Headers $authHeader | Select-Object -ExpandProperty value

    If(-not $PilotPackages){
    $Packages = $webData | where-object { $_.name -like "*Drivers -*" }
    $Packages = $webData | where-object { $_.name -like "*Drivers Pilot -*" }
    If(-not $PilotPackages){
    $Packages = $webData | where-object { $_.name -like "*BIOS Update -*" }
    $Packages = $webData | where-object { $_.name -like "*BIOS Update Pilot -*" }

    Best regards

    1. Hi there,
      I guess that works for now as a workaround but it’s not very efficient because you are requesting a list of all the packages to Configuration Manager. The body part of the request was to prevent ConfigMgr from returning all of the packages and just some needed properties instead of returning all properties.
      Whenever I get the time to rebuild a lab at home, I will try to see what changed in 2203 to cause the query to fail after this upgrade…

      1. Hey Charles,
        Update: So far the script works very well. I mean it is executed within 7-10 seconds – Do you really want to make the effort with the body? I don’t know anyone who has 1000 packages, most companies use applications instead. If you change your mind, you can add my part in your script. Your amazing work has also helped me a lot.

        Best regards

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.