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


01 // The Problem

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.

⚡ Hard Deadline

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.

DateEventImpact on Unpatched VMs
24 June 2026KEK CA 2011 + UEFI CA 2011 expireNo future Secure Boot DB/KEK updates, no new DBX revocations
October 2026Windows Production PCA 2011 expiresBoot Manager signing chain breaks for future updates
OngoingFuture dbx revocation pushIf 2011 cert added to dbx via Windows Update → machine won't boot
Post-2026OS reinstall / recoveryNew Microsoft boot media (2023-signed) may refuse to boot
02 // The Trust Chain

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:

PK
Platform Key
Exp: N/A
KEK
Key Exchange Key
Exp: Jun 2026
DB
Signature DB
Exp: Jun / Oct 2026
DBX
Forbidden DB
Revocation list
StoreFull NamePurposeWho Controls It
PKPlatform KeyTop-level trust anchor. Only the PK holder can update the KEK. There is exactly one PK per system.OEM / Microsoft OEM
KEKKey Exchange KeyControls who can add/remove entries from DB and DBX. Signing authority for database updates.Microsoft
DBSignature DatabaseTrusted signatures and certificate hashes. Bootloader must be signed by a cert in DB to be allowed to run.Microsoft
DBXForbidden Signatures DBRevoked/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.

03 // NVRAM Internals

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):

powershell — reading raw UEFI cert bytes
# 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, Subject

The 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.

04 // The Manual Approach

Why the Manual Method Doesn't Scale

Before building the automated solution, the process looked like this for each VM:

1
Power off VM, attach FAT32 virtual disk containing cert files
Create a FAT32-formatted VMDK, copy 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.
2
Add uefi.allowAuthBypass = TRUE to VMX config
This disables Secure Boot authentication temporarily, allowing unauthenticated cert enrollment. It is persistent — you must manually remove it after. If forgotten, the VM boots with Secure Boot effectively disabled.
3
Boot VM into EFI Setup UI, navigate menus to enroll each cert
Requires someone watching the VM console in vCenter at exactly the right moment to catch the UEFI POST screen. Navigate to Secure Boot configuration, enroll PK, then KEK, then DB — three separate operations, each requiring file selection from the FAT32 disk.
4
Remove disk, remove bypass flag, reboot, verify, run registry fix
Detach the FAT32 disk, remove the VMX flag, reboot into Windows, then manually run the registry commands and trigger the Secure-Boot-Update task. Verify with PowerShell cmdlets.

At 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.

⚠ Security Note

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.

05 // The Core Insight

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 $vm

On 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)
ℹ Why ESXi 8.0.2 Specifically

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 PowerShell
06 // Certificate Math

Why 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:

manual-vs-automated.diff
MANUAL: Inject WindowsUEFICA2023.der into DB store via EFI UI
+
AUTO: ESXi regenerates DB with 2023 certs from built-in templates
MANUAL: Inject kek2023.der into KEK store via EFI UI
+
AUTO: ESXi regenerates KEK with 2023 cert from built-in templates
MANUAL: Inject WindowsOEMDevicesPK.der into PK store via EFI UI
+
AUTO: ESXi writes own placeholder PK → we replace it programmatically

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.

07 // The Reboot Sequence

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:

1
NVRAM Regeneration Boot
ESXi cannot update an existing NVRAM file — it only generates a new one when no NVRAM exists. This boot is what causes ESXi to write the fresh NVRAM with 2023 cert templates. No other mechanism triggers NVRAM regeneration.
~2-3 minutes
2
Secure-Boot-Update Task Completion Boot
After setting 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.
~3-5 minutes
3
UEFI SetupMode Boot (PK Enrollment)
The PK store is only writeable when the UEFI is in Setup Mode — a state where the PK store is empty or explicitly unlocked. ESXi 8's 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.
~2-3 minutes
4
Post-PK Enrollment Verification Boot
After 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).
~2-3 minutes

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.

08 // VMX Deep Dive

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.

allowAuthBypass (manual)
Persistent until manually removed. Disables all Secure Boot auth while set.
overrideOnce (automated)
Consumed on first boot. Self-clears. Narrow scope: SetupMode only, not full auth bypass.
ESXi version requirement
ESXi 8.x only. Not available on ESXi 7.x — manual FAT32 method required there.
Script safety net
Explicit clear in catch and finally blocks — cleared even on exception.
09 // The Hidden Trap

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.

✓ The Fix

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.

10 // Registry Deep Dive

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:

BitHexMeaning
0x0004Bit 2DB update required (Windows UEFI CA)
0x0040Bit 6KEK update required
0x0900Bits 8+11Additional DB/KEK entries
0x4000Bit 14Boot Manager update required
0x5944AllAll cert slots require updating (set this to trigger full update)
0x0000NoneAll 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 Missing immediately after
  • It gets re-written by the task on the next boot cycle — not immediately
  • ConfidenceLevel = Under Observation can 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 -AsBat
11 // Idempotency

Smart 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
    }
}
12 // Verification

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=US
13 // Full PK Enrollment

PK 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
    }
}
14 // Results

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=SetupMode reliably 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. 0x0000 always 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. 0xC0000100 is STATUS_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.Firmware returns "bios", not EFI. Secure Boot doesn't exist on BIOS VMs. These must be converted before remediation (MBR2GPT + vSphere firmware change).

Production Readiness Checklist

RequirementHow to VerifyStatus
ESXi hosts ≥ 8.0.2Get-VMHost | Select Name, VersionRequired
VM hardware version ≥ 13Get-VM | Select Name, HardwareVersionRequired
Firmware = EFI (not BIOS)$vm.ExtensionData.Config.FirmwareRequired
VMware Tools = toolsOk$vm.Guest.ExtensionData.ToolsStatusStrongly recommended
Not a Domain ControllerProductType = 1 (member server)Required
BitLocker inactiveGet-BitLockerVolume -MountPoint C:Suspend first if active
WindowsOEMDevicesPK.derSHA256: verify against Microsoft repoRequired for PK enrollment
📎 Source

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.

Run Invoke-SecureBootRemediation.ps1 as Part-1




Install UFEIv2 Module as Part-2

 Run the Post Remediation script as Part-3



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.




Status should be changed to Fully remediated with UEFICA2023Status - Updated



Popular posts from this blog

On-Prem Storage To Azure Storage - Part-2

On-Prem Storage To Azure Storage - Part-1