Secure Boot vCenter Deployment
Fully automated PowerShell script to replace expiring Secure Boot certificates across hundreds of Windows Server VMs — eliminating the need for manual EFI console interaction, FAT32 disks, or per-VM operator involvement.
If you need the full deployment files - https://github.com/cloudmigrator/vMware
Microsoft's Boot Certs Are Expiring
Every Windows Server VM with Secure Boot enabled in our VMware environment trusts a set of cryptographic certificates that Microsoft issued back in 2011. On 24 June 2026, those certificates begin to expire — and if we do nothing, those VMs silently fall out of Microsoft's Secure Boot servicing boundary forever.
The Microsoft Corporation KEK CA 2011 and Microsoft UEFI CA 2011 both expire on 24 June 2026. The Windows Production PCA 2011 — which signs the Windows bootloader itself — expires in October 2026.
The good news, per Microsoft's official documentation: machines will not immediately fail to boot after June 2026. Existing Windows installations continue running. Standard Windows patches keep installing. What stops is the ability to receive any future boot-level security updates: Boot Manager patches, DBX revocation list updates, and mitigations for bootkit vulnerabilities like BlackLotus (CVE-2023-24932).
In a managed environment operating under Essential 8, this is not optional. With ~200 Windows Server VMs to remediate across the McDonald's Australia vSphere estate and a 27-day window remaining as of writing, automating this was not a choice — it was a necessity.
| Date | Event | Impact on Unpatched VMs |
|---|---|---|
| 24 June 2026 | KEK CA 2011 + UEFI CA 2011 expire | No future Secure Boot DB/KEK updates, no new DBX revocations |
| October 2026 | Windows Production PCA 2011 expires | Boot Manager signing chain breaks for future updates |
| Ongoing | Future dbx revocation push | If 2011 cert added to dbx via Windows Update → machine won't boot |
| Post-2026 | OS reinstall / recovery | New Microsoft boot media (2023-signed) may refuse to boot |
Understanding the Secure Boot Trust Chain
Before diving into the automation, you need to understand what Secure Boot is actually checking. The trust chain is anchored by four certificate stores in your VM's NVRAM:
| Store | Full Name | Purpose | Who Controls It |
|---|---|---|---|
PK | Platform Key | Top-level trust anchor. Only the PK holder can update the KEK. There is exactly one PK per system. | OEM / Microsoft OEM |
KEK | Key Exchange Key | Controls who can add/remove entries from DB and DBX. Signing authority for database updates. | Microsoft |
DB | Signature Database | Trusted signatures and certificate hashes. Bootloader must be signed by a cert in DB to be allowed to run. | Microsoft |
DBX | Forbidden Signatures DB | Revoked/banned signatures. Takes precedence over DB. Used to block known-bad bootloaders. | Microsoft |
The hierarchy matters: the PK signs the KEK, the KEK signs updates to DB/DBX, and DB contains the certs that validate bootloaders. Break any link in this chain and Secure Boot either fails silently or refuses to boot.
What Actually Lives in Your NVRAM File
On a VMware VM, the entire Secure Boot certificate state is stored in a file on the datastore with a .nvram extension — for example, [VMFS203] SUNNYHANDA2/SUNNYHANDA2.nvram. This file is the VM's simulated UEFI Non-Volatile RAM. Unlike physical hardware where this lives in flash storage on the motherboard, VMware virtualises it as a flat binary file.
You can inspect the raw UEFI variables from inside a running Windows VM using the Get-SecureBootUEFI cmdlet (part of the SecureBoot module):
# Read raw bytes from the DB store
$db = Get-SecureBootUEFI -Name db
# The bytes include ASN.1-encoded certificate data
# A simple ASCII scan tells us whether 2023 certs are present
$dbStr = [System.Text.Encoding]::ASCII.GetString($db.Bytes)
$has2023 = $dbStr -match "2023"
Write-Host "DB contains 2023 cert string: $has2023"
# Get full cert details via UEFIv2 module
(Get-UEFISecureBootCerts db).signature | Select-Object Thumbprint, SubjectThe raw byte check (-match "2023") is a fast heuristic — the human-readable subject string "Windows UEFI CA 2023" appears literally in the ASN.1-encoded certificate DER data. It is not a cryptographic validation, but it is reliable enough for bulk assessment.
The NVRAM file also stores the PK variable. On a freshly provisioned VMware VM running ESXi 8.x, this contains an ESXi-generated placeholder key — what our script reports as Valid_Other. It is a valid PK in the sense that Secure Boot enforces the trust chain, but it is not the Microsoft OEM Platform Key that completes the Windows Secure Boot certification chain.
Why the Manual Method Doesn't Scale
Before building the automated solution, the process looked like this for each VM:
WindowsOEMDevicesPK.der, kek2023.der, WindowsUEFICA2023.der to it, then attach as a second disk in vCenter. Requires knowing exactly which slot UEFI will mount it from.uefi.allowAuthBypass = TRUE to VMX configAt large scale VMs — this approach is not feasible. Even at 20 minutes per VM with a single operator, that is over 166 hours of hands-on console work. The allowAuthBypass flag is a persistent security risk if left on. And every step requires a human making correct decisions at the right moment.
uefi.allowAuthBypass = TRUE is a broad bypass — it disables Secure Boot authentication entirely for as long as it is set. The overrideOnce parameter used in the automated approach is fundamentally different: it is single-use and self-clears after exactly one boot regardless of what happens.
The NVRAM Regeneration Trick
The key insight that makes the automated approach possible: ESXi 8.0.2+ ships with updated UEFI certificate templates baked into the hypervisor itself. When ESXi boots a VM with no NVRAM file present, it generates a new one from those templates. On ESXi 8.0.2+, those templates include the 2023 KEK and DB certificates.
So instead of manually injecting three cert files into three UEFI stores through a console UI, we can simply:
# Step 1: Rename the existing NVRAM file (preserving it as backup)
# ESXi will not modify an existing NVRAM — it only generates a new one if none exists
$nvramPath = "[VMFS203] SUNNYHANDA2/SUNNYHANDA2.nvram"
$nvramOldPath = "[VMFS203] SUNNYHANDA2/SUNNYHANDA2.nvram_old"
$si = Get-View ServiceInstance
$fileMgr = Get-View $si.Content.FileManager
$dc = (Get-Datacenter -VM $vm).ExtensionData.MoRef
$task = $fileMgr.MoveDatastoreFile_Task($nvramPath, $dc, $nvramOldPath, $dc, $false)
# Step 2: Power on — ESXi sees no .nvram, generates a new one with 2023 certs
Start-VM -VM $vmOn the next boot, ESXi detects the missing NVRAM and generates a fresh one. Because we're on ESXi 8.0.2+, that fresh NVRAM contains:
DB: Windows UEFI CA 2023 + Microsoft UEFI CA 2023 (from ESXi templates)KEK: Microsoft Corporation KEK 2K CA 2023 (from ESXi templates)PK: ESXi-generated placeholder (Valid_Other— needs replacing)
ESXi 8.0.0 and 8.0.1 ship with the older 2011 cert templates. If you rename NVRAM on those versions, the regenerated NVRAM will contain the old certs and your cert checks will come back false. The preflight check blocks this scenario explicitly by reading $vmHost.Version and comparing against 8.0.2.
After the NVRAM regeneration boot, we verify the certs are actually present before proceeding — reading the raw bytes from the live guest, not trusting the ESXi version check alone:
$checkScript = @'
$dbBytes = (Get-SecureBootUEFI -Name db -ErrorAction Stop).Bytes
$kekBytes = (Get-SecureBootUEFI -Name KEK -ErrorAction Stop).Bytes
$dbStr = [System.Text.Encoding]::ASCII.GetString($dbBytes)
$kekStr = [System.Text.Encoding]::ASCII.GetString($kekBytes)
"DB_2023=" + ($dbStr -match "2023").ToString()
"KEK_2023=" + ($kekStr -match "2023").ToString()
'@
$result = Invoke-VMScript -VM $vm -ScriptText $checkScript `
-GuestCredential $cred -ScriptType PowerShellWhy One Cert File vs Three
A question that comes up when comparing manual to automated: the manual process requires three separate cert files (PK, KEK, DB). The automated script needs only one (WindowsOEMDevicesPK.der). Here is why:
The PK is the only store that ESXi does not populate correctly from Microsoft's perspective. ESXi deliberately installs its own PK in the regenerated NVRAM — it needs this to manage Secure Boot enforcement at the hypervisor level. This ESXi placeholder PK reads as Valid_Other when you inspect the raw bytes — a valid PK structurally, but not Microsoft's WindowsOEMDevicesPK.
For the full Microsoft Secure Boot certification chain, the PK must be WindowsOEMDevicesPK.der from Microsoft's secureboot_objects repository. That is the only file the automated script needs to inject manually.
Why There Are Four Reboots
The most common concern from infrastructure managers: "this process requires 6-7 reboots." In practice, a clean automated run requires exactly 4 reboots. Here is what each one does and why it cannot be combined with any other:
AvailableUpdates = 0x5944 and triggering the \Microsoft\Windows\PI\Secure-Boot-Update scheduled task, a reboot is required for the Boot Manager update phase to complete. The task finalises its work during the early boot cycle — it cannot complete while the OS is fully running.uefi.secureBootMode.overrideOnce = SetupMode VMX parameter triggers exactly one SetupMode boot. This is the direct programmatic replacement for sitting at the EFI Setup UI and manually enrolling the PK.Format-SecureBootUEFI | Set-SecureBootUEFI writes the new PK, the VM must boot once under the new certificate chain to confirm enforcement is active and the PK is correctly seated. This boot also clears the SetupMode VMX flag (which self-clears regardless).Additional reboots in testing came from interrupted runs requiring re-execution — the smart resume capability (section 11) eliminates this by detecting completed steps and skipping them on re-run.
The overrideOnce VMX Parameter
This is the single most important technical difference between the manual and automated approaches for PK enrollment. Available only on ESXi 8.x, the parameter is set via the vSphere VMODL API:
function Set-VMExtraConfig {
param($VM, [string]$Key, [string]$Value)
$spec = New-Object VMware.Vim.VirtualMachineConfigSpec
$opt = New-Object VMware.Vim.OptionValue
$opt.Key = $Key
$opt.Value = $Value # Pass "" to remove the key
$spec.ExtraConfig = @($opt)
$VM.ExtensionData.ReconfigVM($spec)
}
# Set SetupMode for exactly ONE boot
Set-VMExtraConfig -VM $vm -Key "uefi.secureBootMode.overrideOnce" -Value "SetupMode"
# Reboot — ESXi sees the flag, boots in Setup Mode, then self-clears the flag
Restart-VMGuest -VM $vm -Confirm:$false
# Script explicitly clears it too (in finally block) — safety net
# If enrollment fails, the flag is still cleared
Set-VMExtraConfig -VM $vm -Key "uefi.secureBootMode.overrideOnce" -Value ""The self-clearing behaviour is what makes this safe. Even if the script crashes, the network drops, or the operator forces a stop, the flag was consumed on the first boot. The VM will never boot into Setup Mode unexpectedly a second time.
catch and finally blocks — cleared even on exception.Guest Operations Agent: The Hidden Bottleneck
This was one of the most frustrating bugs to diagnose. After every reboot, the script calls Wait-VMTools and waits for toolsOk or toolsOld before proceeding. Yet the very next Invoke-VMScript call would fail with:
Invoke-VMScript: The guest operations agent could not be contacted.The root cause: VMware Tools has two separate internal execution engines. The service status (toolsOk) reflects whether the VMware Tools service is running. But Invoke-VMScript communicates through a separate component — the VMware Guest Operations Agent (VGAUTH service) — which takes longer to initialise after a reboot.
The initial fix was to use Invoke-VMScript -ScriptType Bat as a probe (echo ready). This worked most of the time, but the Bat execution engine initialises faster than the PowerShell engine. Since all our actual remediation scripts use ScriptType PowerShell, the Bat probe would succeed while the PowerShell engine was still starting — causing the next real call to fail.
Probe with ScriptType PowerShell — the same engine that all subsequent calls use. Only return "agent ready" once the PowerShell engine specifically is responding, not just any engine.
function Wait-GuestOpsAgent {
param($VM, $Credential, [int]$TimeoutSeconds = 180, [int]$RetryIntervalSecs = 15)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
Write-Log " Waiting for guest operations agent (PowerShell engine)..."
do {
try {
# CRITICAL: Use PowerShell, NOT Bat
# VMware Tools has separate engines — Bat starts faster than PS
# Probing with Bat can return "ready" while PS engine is still cold
$probe = Invoke-VMScript -VM $VM `
-ScriptText 'Write-Output "agent_ready"' `
-GuestCredential $Credential `
-ScriptType PowerShell ` # <-- This is the key change
-ErrorAction Stop
if ($probe.ScriptOutput -match 'agent_ready') {
Write-Log " Guest operations agent (PowerShell) is ready" "OK"
return $true
}
} catch {
Write-Log " Not yet ready: $($_.Exception.Message -replace '\r?\n',' ')"
}
Start-Sleep -Seconds $RetryIntervalSecs
} while ((Get-Date) -lt $deadline)
return $false
}Additionally, even after the probe succeeds, there is a brief window where the agent can service one connection and then briefly become unavailable. A 5-second buffer sleep and a $VM = Get-VM -Name $VM.Name object refresh before the final Get-GuestPKStatus call eliminated the last instance of this race condition.
Registry Keys & The Update State Machine
The Windows Secure Boot update mechanism is driven by two registry locations and a scheduled task. Understanding the state machine is essential for knowing what "done" actually looks like.
The AvailableUpdates Bitmask
Located at HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\AvailableUpdates, this DWORD is a bitmask that tells the Secure-Boot-Update task which certificate slots need updating:
| Bit | Hex | Meaning |
|---|---|---|
0x0004 | Bit 2 | DB update required (Windows UEFI CA) |
0x0040 | Bit 6 | KEK update required |
0x0900 | Bits 8+11 | Additional DB/KEK entries |
0x4000 | Bit 14 | Boot Manager update required |
0x5944 | All | All cert slots require updating (set this to trigger full update) |
0x0000 | None | All updates complete — this is the definitive completion signal |
Setting AvailableUpdates = 0x5944 tells Windows "everything needs updating." As the Secure-Boot-Update task completes each slot, it clears the corresponding bit. When all bits are cleared (0x0000), the update is complete. This is the most reliable completion signal — more authoritative than UEFICA2023Status.
The UEFICA2023Status Trap
The second registry key at HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\UEFICA2023Status tracks the human-readable status: NotStarted, InProgress, or Updated. However, this key has several quirks that caused false negatives in early versions of the script:
- It can be a DWORD (0=NotStarted, 1=InProgress, 2=Updated) or a string depending on the Windows build
- Our script deletes it as part of clearing stale state — it reads as
Missingimmediately after - It gets re-written by the task on the next boot cycle — not immediately
ConfidenceLevel = Under Observationcan appear even when all certs are correctly enrolled — this is Microsoft telemetry, not a local cert status
# Normalise UEFICA2023Status regardless of DWORD vs string type
$rawStatus = $reg.UEFICA2023Status
if ($rawStatus -is [int] -or $rawStatus -is [int32] -or $rawStatus -is [uint32]) {
$statusInt = [int]$rawStatus
} elseif ($rawStatus -match '^\d+$') {
$statusInt = [int]$rawStatus
} elseif ($rawStatus -eq "Updated") {
$statusInt = 2
} else {
$statusInt = 0 # NotStarted or unknown
}
# AvailableUpdates = 0x0000 is the authoritative completion signal
# UEFICA2023Status may lag — always cross-check with AvailableUpdates
$availableUpdates = Get-ItemPropertyValue `
"HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot" `
-Name AvailableUpdates -ErrorAction SilentlyContinue
$isComplete = ($availableUpdates -eq 0) -or ($statusInt -eq 2)Running the Task as SYSTEM
The registry writes require elevation above what a standard admin account provides through Invoke-VMScript due to UAC filtering. The script uses a scheduled task wrapper to execute with SYSTEM privileges:
$regFixScript = @'
schtasks /create /tn "TempSBFix_$$" /sc once /st 00:00 /ru SYSTEM /tr `
"powershell.exe -NonInteractive -ExecutionPolicy Bypass -Command `
\"reg add 'HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot' /v AvailableUpdates /t REG_DWORD /d 0x5944 /f\"" /f
schtasks /run /tn "TempSBFix_$$"
timeout /t 15 /nobreak
schtasks /delete /tn "TempSBFix_$$" /f
'@
Invoke-GuestPS -VM $vm -Script $regFixScript -Credential $cred -AsBatSmart Resume: Making the Script Idempotent
At scale, interrupted runs are inevitable — wrong credentials, network drops, forced CTRL+C. A non-idempotent script that re-runs all 4 reboots on every execution would be unusable. The smart resume system detects completed steps before touching anything:
# ── SMART RESUME DETECTION ──────────────────────────────────────────
$skipNvramRename = $false
$skipCertUpdate = $false
# 1. Check .nvram_old on datastore (no guest credentials needed)
$nvramOldExists = Test-NvramOldExists -VM $VM -Paths $paths
if ($nvramOldExists) {
$skipNvramRename = $true
$rec.NVRAMRenamed = $true
Write-Log " [Resume] .nvram_old found -- NVRAM rename already done"
}
# 2. Check guest cert/task state (only if VM is on and NVRAM was already renamed)
if ($skipNvramRename -and $VM.PowerState -eq "PoweredOn") {
$priorStatus = Get-GuestSecureBootStatus -VM $VM -Credential $Credential
$certsDone = ($priorStatus["KEK_2023"] -eq "True" -and
$priorStatus["DB_2023"] -eq "True")
$availDone = ($priorStatus["AvailableUpdates"] -eq "0x0000")
if ($certsDone -and $availDone) {
$skipCertUpdate = $true
Write-Log " [Resume] Certs present + 0x0000 -- cert steps will be skipped"
}
$currentPKStatus = Get-GuestPKStatus -VM $VM -Credential $Credential
}
# If everything is done → validation only, zero reboots
if ($skipNvramRename -and $skipCertUpdate -and $pkAlreadyValid) {
# Just re-read cert state and report — no changes made
return Invoke-ValidationOnly -VM $VM -Credential $Credential
}The datastore check for .nvram_old uses the vSphere SearchDatastore_Task API with the exact filename as the match pattern — an important detail, since using a glob like *.nvram* with an incorrect folder path format caused the initial detection to silently return false even when the file existed:
function Test-NvramOldExists {
param($VM, [hashtable]$Paths)
try {
$dsName = $Paths.Nvram -replace '^\[(.+?)\].*', '$1'
$ds = Get-Datastore -Name $dsName
$browser = Get-View $ds.ExtensionData.Browser
# CRITICAL: SearchDatastore_Task expects full datastore path WITH brackets
# "[VMFS203] SUNNYHANDA2" — NOT just "SUNNYHANDA2"
$folder = $Paths.Nvram -replace '/[^/]+$', '' # Keep [ds] prefix
$spec = New-Object VMware.Vim.HostDatastoreBrowserSearchSpec
$spec.MatchPattern = @(($Paths.NvramOld -split '/')[-1]) # Exact filename match
$taskRef = $browser.SearchDatastore_Task($folder, $spec)
# ... wait for task, parse results
}
}Verifying Certificates via Byte-Level Reads
The verification step reads actual certificate bytes from the live UEFI stores inside the running guest — not from Windows certificate stores, not from the registry. This is the ground truth: if the bytes are not in NVRAM, the cert is not enrolled.
$checkScript = @'
$result = @{}
# Read raw DB and KEK bytes directly from UEFI variables
try {
$dbBytes = (Get-SecureBootUEFI -Name db -ErrorAction Stop).Bytes
$kekBytes = (Get-SecureBootUEFI -Name KEK -ErrorAction Stop).Bytes
$dbStr = [System.Text.Encoding]::ASCII.GetString($dbBytes)
$kekStr = [System.Text.Encoding]::ASCII.GetString($kekBytes)
$result["DB_2023"] = ($dbStr -match "2023").ToString()
$result["KEK_2023"] = ($kekStr -match "2023").ToString()
} catch {
$result["DB_2023"] = "Error"
$result["KEK_2023"] = "Error"
}
# Read PK and classify it
$pk = Get-SecureBootUEFI -Name PK -ErrorAction SilentlyContinue
if ($pk -eq $null -or $pk.Bytes.Count -eq 0) {
"PK=Invalid_NULL"
} else {
$pkStr = [System.Text.Encoding]::ASCII.GetString($pk.Bytes)
if ($pkStr -match "Windows OEM Devices") { "PK=Valid_WindowsOEM" }
elseif ($pkStr -match "Microsoft") { "PK=Valid_Microsoft" }
else { "PK=Valid_Other" }
}
# Output key=value pairs for easy parsing
foreach ($k in $result.Keys) { "$k=$($result[$k])" }
'@The PK=Valid_Other classification is what triggers PK enrollment. The subject string "Windows OEM Devices" appears in the ASN.1 subject of WindowsOEMDevicesPK.der — confirmed by inspecting the cert with OpenSSL:
# Inspect the PK cert to understand what string to match
openssl x509 -in WindowsOEMDevicesPK.der -inform DER -text -noout | grep Subject
# Subject: CN=Windows OEM Devices PK 2023, O=Microsoft Corporation, C=USPK Enrollment: End-to-End Code
The PK enrollment function is the most complex part of the script. It must copy a binary certificate file into the guest VM, run a PowerShell enrollment cmdlet, and handle the timing of the SetupMode boot cleanly. The cert is transferred as a base64-encoded string to avoid any binary transfer issues through Invoke-VMScript:
function Invoke-PKEnrollment {
param($VM, [PSCredential]$Credential)
try {
# PK 1/5: Set SetupMode for exactly one boot
Set-VMExtraConfig -VM $VM -Key "uefi.secureBootMode.overrideOnce" -Value "SetupMode"
# PK 2/5: Power off then on — ESXi boots in SetupMode (PK store writeable)
Stop-VM -VM $VM -Confirm:$false | Out-Null
Start-VM -VM $VM -Confirm:$false | Out-Null
Wait-VMTools -VM $VM -TimeoutSeconds 150 | Out-Null
Wait-GuestOpsAgent -VM $VM -Credential $Credential -TimeoutSeconds 180 | Out-Null
$VM = Get-VM -Name $VM.Name
# PK 3/5: Transfer cert file as base64 — avoids binary encoding issues
$derBytes = [System.IO.File]::ReadAllBytes((Resolve-Path $PKDerPath).Path)
$derB64 = [Convert]::ToBase64String($derBytes)
$copyScript = "[System.IO.File]::WriteAllBytes(" +
"'C:\Windows\Temp\WindowsOEMDevicesPK.der', " +
"[Convert]::FromBase64String('$derB64'))"
Invoke-GuestPS -VM $VM -Script $copyScript -Credential $Credential | Out-Null
# PK 4/5: Enroll PK via Format-SecureBootUEFI | Set-SecureBootUEFI
$enrollScript = @'
$cert = "C:\Windows\Temp\WindowsOEMDevicesPK.der"
$owner = "55555555-0000-0000-0000-000000000000"
$time = "2025-10-23T11:00:00Z"
Format-SecureBootUEFI -Name PK -CertificateFilePath $cert `
-SignatureOwner $owner -FormatWithCert -Time $time |
Set-SecureBootUEFI -Time $time
"EnrollDone"
'@
Invoke-GuestPS -VM $VM -Script $enrollScript -Credential $Credential | Out-Null
# PK 5/5: Clear VMX flag (self-clears anyway but explicit is better)
Set-VMExtraConfig -VM $VM -Key "uefi.secureBootMode.overrideOnce" -Value ""
# Final reboot + agent wait + 5s buffer for stability
Restart-VMGuest -VM $VM -Confirm:$false | Out-Null
Wait-VMTools -VM $VM -TimeoutSeconds 150 | Out-Null
Wait-GuestOpsAgent -VM $VM -Credential $Credential -TimeoutSeconds 180 | Out-Null
$VM = Get-VM -Name $VM.Name
Start-Sleep -Seconds 5 # Buffer after agent probe succeeds
# Verify PK is now Valid_WindowsOEM
$finalPK = Get-GuestPKStatus -VM $VM -Credential $Credential
return ($finalPK -eq "Valid_WindowsOEM")
} catch {
# ALWAYS clear the VMX flag — even on exception
try { Set-VMExtraConfig -VM $VM -Key "uefi.secureBootMode.overrideOnce" -Value "" } catch {}
throw
}
}Results and Key Observations
After building, iterating, and validating the script across multiple test VMs (lab ESXi 8.0.3 + production aussdc1vcn001.corp.pri), here is what stood out:
What Worked Exactly as Designed
- NVRAM regeneration consistently produced 2023 KEK and DB certs on ESXi 8.0.3
overrideOnce=SetupModereliably triggered UEFI Setup Mode for PK enrollment- Smart resume correctly detected prior-run state across all tested failure scenarios
- Credential validation with re-prompt prevented account lockouts from typo credentials
- The datastore file rename (FileManager API) was more reliable than PSDrive approaches
Surprises and Gotchas
- UEFICA2023Status can show "NotStarted" with AvailableUpdates = 0x0000. These states appear contradictory but both are valid — the task deleted and re-created the status key on the same boot cycle.
0x0000always wins as the authority. - The SearchDatastore_Task folder path must include the
[datastoreName]prefix. Stripping it causes silent empty results — the most subtle bug in the entire script. - PowerShell vs Bat execution engine timing is not the same. Probing with Bat returns success while the PS engine is still cold. This caused "agent could not be contacted" errors on the very call after a successful probe.
PK status in SetupMode boot: CheckFailed: Variable is currently undefined: 0xC0000100— this is expected.0xC0000100isSTATUS_VARIABLE_NOT_FOUND. In SetupMode, the PK UEFI variable is intentionally undefined. This is correct behaviour, not an error worth blocking on.- BIOS-firmware VMs fail silently at the firmware check.
$VM.ExtensionData.Config.Firmwarereturns"bios", not EFI. Secure Boot doesn't exist on BIOS VMs. These must be converted before remediation (MBR2GPT + vSphere firmware change).
Production Readiness Checklist
| Requirement | How to Verify | Status |
|---|---|---|
| ESXi hosts ≥ 8.0.2 | Get-VMHost | Select Name, Version | Required |
| VM hardware version ≥ 13 | Get-VM | Select Name, HardwareVersion | Required |
| Firmware = EFI (not BIOS) | $vm.ExtensionData.Config.Firmware | Required |
| VMware Tools = toolsOk | $vm.Guest.ExtensionData.ToolsStatus | Strongly recommended |
| Not a Domain Controller | ProductType = 1 (member server) | Required |
| BitLocker inactive | Get-BitLockerVolume -MountPoint C: | Suspend first if active |
| WindowsOEMDevicesPK.der | SHA256: verify against Microsoft repo | Required for PK enrollment |
The complete script — Invoke-SecureBootRemediation.ps1 — includes full preflight checks, smart resume, credential re-prompt, confirmation UI, rollback mode, snapshot lifecycle management, and output CSV reporting. It is structured around VMware PowerCLI's Invoke-VMScript transport (no WinRM required) and is PowerShell 5.1 compatible throughout.
With 27 days remaining until the June 24 deadline, the batch execution plan runs web/app servers first (highest exposure), then SQL Always On nodes (secondary before primary), then FTP servers (during low-transfer overnight windows), then the ~390-server bulk member-server batch via -Force flag. Domain Controllers are handled separately via a dedicated runbook.
Install UFEIv2 Module as Part-2
Reboot at the vm, and re-run the remediation script - Final status will change and if it doesnt then please wait for 5 minutes and reboot again. After the second reboot, UEFICA2023Status will change to InProgress, which means Windows is trying to inject new certs into Secure Boot. wait for 5 minutes and re-run remediation script again.