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
Permalink
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 😉