Mastering Autopilot Automation in OSDCloud Deployments

I often receive requests about integrating Autopilot into the OSDCloud process and how it can be automated.

Note: In this blog post, I will focus solely on the traditional Autopilot process, not the newer Autopilot device preparation.

Let me share my experience and examples of how Autopilot registration and profile assignment can be automated.

Create Certificate script

For all my OSDCloud customers, I use an enterprise application in Entra ID. Before creating this application, I generate a self-signed certificate and split it into two parts: a CER file (public key) and a PFX file (private key). The public key is assigned to the enterprise application, while the private key is imported into the OSDCloud boot image.

$subjectName = "OSDCloudRegistration"

$newCert = @{
    Subject           = "CN=$($subjectName)"
    CertStoreLocation = "Cert:\LocalMachine\My"
    KeyExportPolicy   = "Exportable"
    KeySpec           = "Signature"
    KeyLength         = "2048"
    KeyAlgorithm      = "RSA"
    HashAlgorithm     = "SHA512"
    NotAfter          = (Get-Date).AddMonths(60) #5 years validity period
}
$Cert = New-SelfSignedCertificate @newCert

# Export public key only
New-Item -Path "C:\Temp\OSD\Certs" -ItemType Directory -Force | Out-Null
$certFolder = "C:\Temp\OSD\Certs"
$certExport = @{
    Cert     = $Cert
    FilePath = "$($certFolder)\$($subjectName).cer"
}
Export-Certificate @certExport

# Export with private key
$certThumbprint = $Cert.Thumbprint
$certPassword = ConvertTo-SecureString -String "@H1ghS3curePassw0rd!" -Force -AsPlainText

$pfxExport = @{
    Cert         = "Cert:\LocalMachine\My\$($certThumbprint)"
    FilePath     = "$($certFolder)\$($subjectName).pfx"
    ChainOption  = "EndEntityCertOnly"
    NoProperties = $null
    Password     = $certPassword
}
Export-PfxCertificate @pfxExport

Import Certificate script

Secondly, the following script is used for importing the PFX certificate before the machine connects to GraphAPI:

$Global:Transcript = "$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Import-Certificate.log"
Start-Transcript -Path (Join-Path "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs\OSD\" $Global:Transcript) -ErrorAction Ignore

Write-Host -ForegroundColor Cyan "Importing PFX certificate"
$subjectName = "OSDCloudRegistration"
$certPassword = ConvertTo-SecureString -String "@H1ghS3curePassw0rd!" -Force -AsPlainText

$Params = @()
$Params = @{
    FilePath          = "$env:SystemDrive\OSDCloud\Scripts\$($subjectName).pfx"
    CertStoreLocation = "Cert:\LocalMachine\My"
    Password          = $certPassword
}
Import-PfxCertificate @Params | Out-Null

Write-Host -ForegroundColor Cyan "Remove cert"
Remove-Item $env:SystemDrive\OSDCloud\Scripts\$($subjectName).pfx -Force

Stop-Transcript

Once these files are created, the private key and the import script must be copied into the OSDCloud media. To accomplish this, copy the files into the ..\Config\Scripts folder and create a new bootable media.

$WorkingDir = "C:\Temp\OSDCloud"
New-OSDCloudISO -WorkspacePath $WorkingDir -Verbose

Create Entra ID App Registration

My OSDCloud fellow David Segura has already written a post about creating this enterprise app, which you can find here. David is using a secret for testing purpose. In my process, I am using certificates for the Graph API authentication.

On the other hand, I am using the following API permissions:

Using a certificate for the Graph API connection raised a key question: at which stage during the OSD process should this step occur? Initially, when experimenting with Autopilot, I relied on either using a secret or using the AutopilotOOBE PowerShell module (more on this in the next blog post). These methods worked during the OOBE phase very well.

However, when switching to certificate-based authentication, I encountered challenges connecting to the Graph API in the OOBE phase. Like, this error message:

I had to move this step to an earlier phase—the specialize phase. For this, I utilize the familiar unattended.xml file to execute the Autopilot process. During the OSDCloud process, I create the following XML file and save it in the C:\Windows\Panther folder from which it will be retrieved during the first boot.

$UnattendXml = @'
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="specialize">
        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <RunSynchronous>
                <RunSynchronousCommand wcm:action="add">
                    <Order>1</Order>
                    <Description>Start Autopilot Import & Assignment Process</Description>
                    <Path>PowerShell -ExecutionPolicy Bypass C:\Windows\Setup\scripts\autopilot.ps1</Path>
                </RunSynchronousCommand>
            </RunSynchronous>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <InputLocale>de-CH</InputLocale>
            <SystemLocale>de-DE</SystemLocale>
            <UILanguage>de-DE</UILanguage>
            <UserLocale>de-CH</UserLocale>
        </component>
    </settings>
</unattend>
'@ 

if (-NOT (Test-Path 'C:\Windows\Panther')) {
    New-Item -Path 'C:\Windows\Panther'-ItemType Directory -Force -ErrorAction Stop | Out-Null
}

$Panther = 'C:\Windows\Panther'
$UnattendPath = "$Panther\Unattend.xml"
$UnattendXml | Out-File -FilePath $UnattendPath -Encoding utf8 -Width 2000 -Force

In this OSDCloud script, we need to handle the PFX certificate and the import script, both of which are part of the bootable media. Let’s copy these to the target OS and utilize them in the autopilot.ps1 file:

Write-DarkGrayHost "Copying PFX file & the import script"
Copy-Item X:\OSDCloud\Config\Scripts C:\OSDCloud\ -Recurse -Force

Bonus for Swiss customers: you can easily define your OS and keyboard language in your unattend.xml file section too.

The magic happens in the autopilot.ps1 file. In this process, I use the “original” WindowsAutopilotInfo tool from Michael Niehaus. If you need additional features, consider trying the community version by Andrew Taylor. More details to this module, you can find in this blog post.

Note: in this script, I still have some parameters, and here’s where they come from:

Write-DarkGrayHost "Create C:\ProgramData\OSDeploy\OSDeploy.AutopilotOOBE.json file"
$AutopilotOOBEJson = @"
{
        "AssignedComputerName" : "$AssignedComputerName",
        "AddToGroup":  "$AddToGroup",
        "Assign":  {
                    "IsPresent":  true
                },
        "GroupTag":  "$GroupTag",
        "Hidden":  [
                    "AddToGroup",
                    "AssignedUser",
                    "PostAction",
                    "GroupTag",
                    "Assign"
                ],
        "PostAction":  "Quit",
        "Run":  "NetworkingWireless",
        "Docs":  "https://google.com/",
        "Title":  "Autopilot Register"
    }
"@

If (!(Test-Path "C:\ProgramData\OSDeploy")) {
    New-Item "C:\ProgramData\OSDeploy" -ItemType Directory -Force | Out-Null
}
$AutopilotOOBEJson | Out-File -FilePath "C:\ProgramData\OSDeploy\OSDeploy.AutopilotOOBE.json" -Encoding ascii -Force

More information to this JSON file, let’s read the next blog post, which is coming soon. 😉

Key Steps in the Automation Process:

1. Import the PFX certificate into the computer store.

2. Connect to the Graph API using the private key.

3. Register the device based on its hardware hash.

4. Assign the device to an Autopilot deployment profile.

5. Disconnect from the Graph API.

6. Remove and clean up the certificate scripts and the certificate itself.

If the full automation isn’t feasible for your setup, you can still use the AutopilotOOBE PowerShell module. I will cover this use case in more detail in the next blog post.

Automation Summary

Happy OSDClouding & cheers, Ákos

1 Comment


  1. Hi Ákos, thanks a lot for all the blog posts. I keep getting confused on where to put all these different scripts and snippets, and which process executes them at what point.

    Let’s say we’re using a lot of automation and github etc. We build the boot media with the command “Edit-OSDCloudWinPE -WebPSScript [gist-URL]”

    Do you then use this WebPSScript to create the “C:\Windows\System32\OOBE.cmd” or the “C:\Windows\Setup\Scripts\SetupComplete.cmd”

    I think I went with the latter, where you basically use the “SetupComplete.cmd” to call another gist-script, to do All The Things, like calling your “Set-KeyboardLanguage.ps1”, “Install-EmbeddedProductKey.ps1” and other scripts from github.

    And in this blog post you have even more scripts that create the “unattended.xml”, or the “OSDeploy.AutopilotOOBE.json”, and somewhere the “autopilot.ps1” comes into play. I’m kinda lost. I think I’ll need to book you for an hour at some point 😉

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *