refactor(msi): the MSI carries the shared stage-1 loader instead of its own downloader

Invoke-BootstrapDownload.ps1 and Start-AutopilotMonitor.ps1 had become the same script:
download the current installer, run it, log, always exit 0. The MSI copy additionally
downloaded and executed WITHOUT verifying what it got, which is exactly the gap the loader
was written to close. The MSI is now packaging only -- it installs and runs the same file
that is assigned directly in Intune, and both delivery channels get publisher verification
from one implementation.

Dropped from the MSI path: its own Deployed-marker check. That guard lives in stage 2 and
runs there anyway, so the only cost is one download on a device that is already deployed.

Kept: the log file name. bootstrap-msi.log is what the diagnostics tooling keys on to tell
the MDM-LOB channel apart from the Intune script channel, so the loader takes -LogFileName
(default bootstrap-loader.log) and the custom action passes bootstrap-msi.log. A Pester
test now pins that contract across the two files -- renaming the parameter on either side
would leave the MSI channel logging under the wrong name.

Package version 1.0.2 -> 1.1.0 so Intune LOB update detection sees the new payload, and a
new component GUID because the key path file changed.

Loader 1.0 -> 1.1. Bootstrap suite on Windows PowerShell 5.1: 44/44.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-08 20:41:52 +02:00
parent db9b1995bc
commit 647581b4ff
6 changed files with 65 additions and 84 deletions
+5 -4
View File
@@ -12,8 +12,9 @@
lands a consistent set (ZIP + manifest + scripts).
Both callers run the same code. The scripts on the download alias are the live
copy the WDP bootstrap MSI (Invoke-BootstrapDownload.ps1) downloads on every
enrolling device, so "current" is a correctness property, not cosmetics.
copy the stage-1 loader downloads on every enrolling device, whether it came from
Intune or from the bootstrap MSI, so "current" is a correctness property, not
cosmetics.
Steps, in order:
1. Parse $ScriptVersion from the loader and the bootstrap source.
@@ -100,8 +101,8 @@ $TableUrl = "https://autopilotmonitoreu.table.core.windows.net/AdminCo
$BootstrapSource = Join-Path $RepoRoot 'scripts/Bootstrap/Install-AutopilotMonitor.ps1'
$BootstrapBlob = 'Install-AutopilotMonitor.ps1'
$DevBlob = 'Install-AutopilotMonitor-Dev.ps1'
# Stage 1. What a customer assigns in Intune once; it downloads the bootstrap above on
# every device and verifies its publisher before running it.
# Stage 1. Assigned in Intune once, and embedded in the bootstrap MSI; it downloads the
# bootstrap above on every device and verifies its publisher before running it.
$LoaderSource = Join-Path $RepoRoot 'scripts/Bootstrap/Start-AutopilotMonitor.ps1'
$LoaderBlob = 'Start-AutopilotMonitor.ps1'
$LoaderDevBlob = 'Start-AutopilotMonitor-Dev.ps1'
+13 -11
View File
@@ -1,11 +1,11 @@
name: Build & Publish Bootstrap MSI
# Thin MDM LOB delivery vehicle for WDP (Autopilot v2) — see
# src/Agent/AutopilotMonitor.BootstrapMsi/Package.wxs. The MSI carries no bootstrap
# logic (only the runner script that downloads the CURRENT server-hosted
# Install-AutopilotMonitor.ps1), so it is rebuilt rarely: only when the runner script
# or the WiX package changes. Version lives in Package.wxs — bump it there before
# dispatching so Intune LOB update detection sees a new ProductVersion.
# src/Agent/AutopilotMonitor.BootstrapMsi/Package.wxs. The MSI is packaging only: it
# carries the SAME stage-1 loader that is assigned directly in Intune, so it is rebuilt
# rarely — only when that loader or the WiX package changes. Version lives in Package.wxs
# — bump it there before dispatching so Intune LOB update detection sees a new
# ProductVersion.
on:
workflow_dispatch:
@@ -23,7 +23,7 @@ permissions:
env:
MSI_PROJECT_DIR: src/Agent/AutopilotMonitor.BootstrapMsi
RUNNER_SCRIPT: src/Agent/AutopilotMonitor.BootstrapMsi/Invoke-BootstrapDownload.ps1
RUNNER_SCRIPT: scripts/Bootstrap/Start-AutopilotMonitor.ps1
STABLE_MSI: AutopilotMonitor-Bootstrap.msi
# Authenticode via Azure Key Vault; identity and vault come from repository variables.
# CODE_SIGN_EXPECTED_SUBJECT is optional and pins the publisher in the verify steps.
@@ -42,8 +42,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v7
# The runner executes as SYSTEM on every enrolling device — a parse error or
# The loader executes as SYSTEM on every enrolling device — a parse error or
# non-ASCII mojibake (PS 5.1 codepage roulette) must fail the build, not the fleet.
# bootstrap-script-gates.yml runs the same checks plus Pester on pushes; this job
# stands on its own because it can be dispatched at any time.
- name: Validate runner script (parse + ASCII-only)
shell: pwsh
run: |
@@ -118,10 +120,10 @@ jobs:
DOTNET_ROLL_FORWARD: LatestMajor
run: dotnet tool install --no-cache --global AzureSignTool --version 7.0.1
# BEFORE the build: wix embeds the file as it finds it, so a script signed afterwards
# BEFORE the build: wix embeds the file as it finds it, so a loader signed afterwards
# would leave the copy inside the MSI unsigned. Devices under an AllSigned policy need
# the embedded runner to carry a signature of its own.
- name: Sign bootstrap runner script (Authenticode)
# the embedded loader to carry a signature of its own.
- name: Sign bootstrap loader (Authenticode)
shell: pwsh
env:
DOTNET_ROLL_FORWARD: LatestMajor
@@ -169,7 +171,7 @@ jobs:
$env:STABLE_MSI
if ($LASTEXITCODE -ne 0) { throw "azuresigntool failed with exit code $LASTEXITCODE" }
# The MSI and the runner it carries -- verified here, before anything is published.
# The MSI and the loader it carries -- verified here, before anything is published.
foreach ($file in @($env:STABLE_MSI, $env:RUNNER_SCRIPT)) {
$sig = Get-AuthenticodeSignature -FilePath $file
if ($sig.Status -ne 'Valid') { throw "Signature on $file is '$($sig.Status)', expected 'Valid'." }
@@ -124,4 +124,15 @@ Describe 'publish contract' {
It 'pins the expected publisher by subject, not by thumbprint' {
$script:loaderText -match '\$ExpectedPublisher\s*=\s*"\*O=' | Should -BeTrue
}
# The bootstrap MSI installs this same file and runs it with -LogFileName so the delivery
# channel stays visible in a diagnostics package. Renaming the parameter on either side
# would leave the MSI channel silently logging under the wrong name.
It 'keeps the -LogFileName contract the bootstrap MSI relies on' {
$script:loaderText -match '\[string\]\$LogFileName' | Should -BeTrue
$wxs = Get-Content (Join-Path $PSScriptRoot '..\..\src\Agent\AutopilotMonitor.BootstrapMsi\Package.wxs') -Raw
$wxs.Contains('[INSTALLFOLDER]Start-AutopilotMonitor.ps1') | Should -BeTrue
$wxs.Contains('-LogFileName bootstrap-msi.log') | Should -BeTrue
}
}
+18 -5
View File
@@ -3,9 +3,11 @@
Stage 1 of the Autopilot Monitor bootstrap: fetches the current installer and runs it.
.DESCRIPTION
This is the file an administrator assigns in Intune. It carries no enrollment logic at
all: it downloads the published Install-AutopilotMonitor.ps1, proves that it was signed
by Autopilot Monitor, and runs it in this process.
This is the file an administrator assigns in Intune, and the same file the bootstrap MSI
installs and runs -- one implementation for both delivery channels; the MSI is only the
packaging. It carries no enrollment logic at all: it downloads the published
Install-AutopilotMonitor.ps1, proves that it was signed by Autopilot Monitor, and runs it
in this process.
Why two stages: the installer holds every guard, relax rule and download path, and those
change. Whoever assigns the installer directly has to replace their Intune copy each
@@ -32,6 +34,11 @@
Installer URL. Defaults to the published stable installer; the -Dev variant of this
loader is rendered from that default at publish time.
.PARAMETER LogFileName
Log file inside the agent log directory. The MSI channel passes bootstrap-msi.log so the
delivery channel stays visible in a diagnostics package; the diag tooling keys on these
names, so do not rename them.
.PARAMETER BootstrapArguments
Everything else is passed through to the installer unchanged, so any parameter the
installer accepts can be set here without this file knowing about it.
@@ -47,6 +54,8 @@
Runtime : Windows PowerShell 5.1, SYSTEM context, during Autopilot enrollment
.CHANGELOG
2026-09-08 v1.1 Also used by the bootstrap MSI, which replaced its own copy of this
logic; -LogFileName keeps the channel visible in diagnostics.
2026-09-08 v1.0 Initial version: two-stage bootstrap, publisher-pinned verification.
#>
@@ -55,12 +64,16 @@ param(
[Parameter(Mandatory = $false)]
[string]$BootstrapUrl = "https://download.autopilotmonitor.com/agent/Install-AutopilotMonitor.ps1",
[Parameter(Mandatory = $false)]
[ValidatePattern('^[A-Za-z0-9._-]+$')]
[string]$LogFileName = "bootstrap-loader.log",
[Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)]
[string[]]$BootstrapArguments
)
# Script version (bump on meaningful changes; see .CHANGELOG above)
$ScriptVersion = "1.0"
$ScriptVersion = "1.1"
# The publisher every downloaded stage-2 script must carry. A -like pattern on the subject,
# NOT a thumbprint: certificates are renewed, deployed loaders are not.
@@ -68,7 +81,7 @@ $ExpectedPublisher = "*O=glueckkanja AG*"
$LoaderBasePath = "$env:ProgramData\AutopilotMonitor"
$LoaderLogPath = "$LoaderBasePath\Logs"
$LogFile = "$LoaderLogPath\bootstrap-loader.log"
$LogFile = "$LoaderLogPath\$LogFileName"
$BootstrapPath = "$LoaderBasePath\Install-AutopilotMonitor.ps1"
$DownloadAttempts = 3
@@ -1,55 +0,0 @@
# Invoke-BootstrapDownload.ps1 - thin runner embedded in the Bootstrap MSI.
#
# The MSI is only a delivery vehicle (MDM LOB channel installs earlier than the IME
# script/app batches during Autopilot Device Preparation). This runner downloads the
# CURRENT Install-AutopilotMonitor.ps1 from the download endpoint and executes it, so
# every piece of real bootstrap logic (guards, relax rules, agent download) stays
# server-updatable without re-shipping the MSI.
#
# Fail-soft by design: the runner always exits 0 so a transient network failure never
# marks the LOB install as failed; diagnostics go to bootstrap-msi.log.
# ASCII only - no special characters in this file.
$ErrorActionPreference = 'Stop'
$baseDir = Join-Path $env:ProgramData 'AutopilotMonitor'
$logDir = Join-Path $baseDir 'Logs'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$log = Join-Path $logDir 'bootstrap-msi.log'
function Write-Log([string]$message) {
Add-Content -Path $log -Value ("[{0}] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $message)
}
try {
Write-Log "MSI bootstrap runner started (user=$env:USERNAME, pid=$PID)."
if (Test-Path 'HKLM:\SOFTWARE\AutopilotMonitor\Deployed') {
Write-Log 'Deployed marker present - skipping, another channel already installed the agent.'
exit 0
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$url = 'https://download.autopilotmonitor.com/agent/Install-AutopilotMonitor.ps1'
$dest = Join-Path $baseDir 'Install-AutopilotMonitor.ps1'
for ($attempt = 1; $attempt -le 3; $attempt++) {
try {
Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $dest
break
} catch {
Write-Log "Download attempt $attempt failed: $_"
if ($attempt -eq 3) { throw }
Start-Sleep -Seconds 10
}
}
Write-Log "Bootstrap script downloaded to $dest - executing."
# PS 5.1 file redirection (*>>) writes UTF-16LE, which garbles a log otherwise
# written via Add-Content (ANSI). Route through Add-Content for one encoding.
& $dest *>&1 | ForEach-Object { $_.ToString() } | Add-Content -Path $log
Write-Log "Bootstrap script finished (exit code $LASTEXITCODE)."
exit 0
} catch {
Write-Log ("MSI bootstrap runner failed: " + ($_ | Out-String))
exit 0
}
@@ -4,16 +4,19 @@
LOB MSIs are installed by the OMA-DM stack (EnterpriseDesktopAppManagement CSP) during
the first MDM sync, i.e. BEFORE/parallel to the Device Preparation SLDM app batch -
earlier than any IME-delivered script (Batch 2) or Win32 app can run. The MSI itself
carries no bootstrap logic: it installs one runner script and executes it once. The
runner downloads the CURRENT server-hosted Install-AutopilotMonitor.ps1, keeping all
real logic updatable without re-shipping the MSI.
carries no bootstrap logic and no downloader of its own: it installs the SAME stage-1
loader that is assigned directly in Intune (scripts/Bootstrap/Start-AutopilotMonitor.ps1)
and runs it once. The loader downloads the current server-hosted
Install-AutopilotMonitor.ps1 and verifies its publisher before executing it, so all real
logic stays updatable without re-shipping the MSI, and both delivery channels share one
implementation.
Build: wix build Package.wxs -arch x64 -o bin\AutopilotMonitor-Bootstrap.msi
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package Name="Autopilot Monitor Bootstrap"
Manufacturer="Autopilot Monitor"
Version="1.0.2"
Version="1.1.0"
UpgradeCode="1fbe5515-d346-445c-b8b8-14ee79f97522"
Scope="perMachine"
Compressed="yes">
@@ -28,8 +31,13 @@
</StandardDirectory>
<ComponentGroup Id="MainComponents" Directory="INSTALLFOLDER">
<Component Id="RunnerScript" Guid="8c8e8962-719f-458b-b291-dd99d3f1dc30">
<File Id="RunnerPs1" Source="Invoke-BootstrapDownload.ps1" KeyPath="yes" />
<!-- Source is relative to the build working directory (the project dir), not to this
file. New component GUID because the key path changed. -->
<Component Id="RunnerScript" Guid="A972F561-01B5-400A-8C2A-47D12BA97DCA">
<File Id="RunnerPs1"
Source="..\..\..\scripts\Bootstrap\Start-AutopilotMonitor.ps1"
Name="Start-AutopilotMonitor.ps1"
KeyPath="yes" />
</Component>
</ComponentGroup>
@@ -38,11 +46,12 @@
</Feature>
<!-- Deferred + no-impersonate: runs as SYSTEM after files land. Return=ignore keeps
the LOB install green even if the runner hits a transient error (the runner is
additionally fail-soft itself); diagnostics live in bootstrap-msi.log. -->
the LOB install green even if the loader hits a transient error (the loader is
additionally fail-soft itself). -LogFileName keeps this channel distinguishable in
a diagnostics package; the diag tooling keys on that name. -->
<CustomAction Id="RunBootstrap"
Directory="INSTALLFOLDER"
ExeCommand="&quot;[System64Folder]WindowsPowerShell\v1.0\powershell.exe&quot; -NoProfile -ExecutionPolicy Bypass -File &quot;[INSTALLFOLDER]Invoke-BootstrapDownload.ps1&quot;"
ExeCommand="&quot;[System64Folder]WindowsPowerShell\v1.0\powershell.exe&quot; -NoProfile -ExecutionPolicy Bypass -File &quot;[INSTALLFOLDER]Start-AutopilotMonitor.ps1&quot; -LogFileName bootstrap-msi.log"
Execute="deferred"
Impersonate="no"
Return="ignore" />