A Windows build on the proxmox-iso builder fails in ways that all look identical from the Packer log. Diagnose by measuring the VM, not by reading the stall.
Building a Windows template with packer-plugin-proxmox puts a full unattended install between you and any feedback. When something goes wrong, Packer prints Waiting for WinRM to become available... and keeps printing it until the timeout. That one line covers at least six distinct root causes, so the log alone never tells you which you have.
Each trap below costs a full 30–60 minute build to find by trial. Four of them fail silently: Windows rejects an instruction and carries on as if you never gave it.
Diagnose the stall before you theorize
A stalled WinRM wait tells you nothing on its own. Read the disk counters twice, about 60 seconds apart:
Take a console screenshot rather than guessing. From the node:
1. UEFI media needs a boot command
Official Windows media stops atPress any key to boot from CD or DVD... and falls through when nobody presses one. Packer then waits out its entire winrm_timeout against a guest that never started installing.
Confirmed on a real run: eight minutes after power-on, 3.6 MB read (the EFI bootloader and nothing else) and 0 bytes written.
Spread the key presses instead of sending a burst. The prompt reappears on each boot attempt, and keys delivered before it is drawn answer nothing:
sendkey endpoint this uses requires the VM.Console privilege on the API token. With the boot command in place, the same VM read 76 MiB in 90 seconds.
2. DriverPaths belongs to PnpCustomizationsWinPE
If your boot disk sits behind avirtio-scsi-pci controller, WinPE sees no disk until vioscsi loads. Attaching virtio-win.iso is necessary but not sufficient — you must also point the answer file at the driver.
Put DriverPaths under Microsoft-Windows-Setup and it is silently ignored. Setup honours every other element of that component — it skips language, EULA, and edition selection — and drops the one it does not own. The result is the “Select location to install Windows” screen with an empty disk list: 715 MiB read, 0 bytes written, forever.
It belongs to Microsoft-Windows-PnpCustomizationsWinPE:
3. An unidentified network is Public, and Public rules are LocalSubnet
Windows classifies a network it cannot identify as Public. The built-in WinRM and Remote Desktop rules for the Public profile are scoped toLocalSubnet. So the guest listens on 5985 and 3389 and silently drops every connection from another subnet — Packer, Ansible, and any off-subnet RDP client all fail against a demonstrably healthy VM.
Observed live inside such a guest:
-NoScope rule is the one that would allow the connection, and it is inactive purely because of the category. Setting <NetworkLocation>Work</NetworkLocation> in the answer file does not settle this.
Set the category explicitly at first logon, before winrm quickconfig — which refuses outright on a Public network. Retry, because the virtio NIC may not be up yet and there is no profile to set:
Do not write that with PowerShell’s
%{ alias inside a templatefile() source. HCL reads both ${ and %{ as template directives, and the render fails. Spell out ForEach-Object.4. Windows 11 24H2 encrypts the disk, and sysprep then refuses
Automatic device encryption now triggers on any clean install with a TPM present — which a Windows 11 template always has.sysprep /generalize aborts with 0x80310039, “BitLocker is on for the OS volume”.
This fails at the last step, after a 35-minute install has already succeeded.
Fix it at the source, in the specialize pass. OOBE is where the encryption decision happens, so anything later is too late:
5. Never let sysprep power the guest off
This builder has noshutdown_command. stepConvertToTemplate unconditionally calls ShutdownVm, which posts to /nodes/<node>/qemu/<vmid>/status/shutdown, retries three times, and then fails with “could not stop”.
Proxmox errors on a VM that is already stopped. So sysprep /shutdown races the builder and loses the whole build at its final step. Use /quit and let the builder do the power-off it insists on doing.
Related: task_timeout defaults to 1m, which a Windows guest shutdown routinely exceeds — and that shutdown is a Proxmox task the plugin waits on. Raise it.
6. PowerShell’s call operator does not wait for sysprep
sysprep.exe is a GUI-subsystem binary, so & returns immediately. The provisioner finishes while generalization is still running, the builder cuts power mid-reseal, and the result converts cleanly. You get a template that looks fine and whose clones misbehave at OOBE — worse than a failed build, because nothing tells you.
Use Start-Process -Wait, then poll until Windows itself reports the reseal finished:
setupact.log and setuperr.log on timeout. Do not gate on setuperr.log being non-empty — it carries benign lines on a clean run.
The autologon everyone forgets
AFirstLogonCommands block in a post-sysprep answer file needs an <AutoLogon> beside it, with LogonCount set to 1.
Those commands run at the first interactive logon and nowhere else. Without an autologon nobody ever logs in, none of them run, and every clone comes up with neither WinRM nor RDP — unreachable, which is exactly the failure the answer file exists to prevent.
Two dead ends worth not repeating
isoinfo -fshows the ISO 9660 8.3 tree even on a Joliet disc. A generated answer ISO therefore looks like it containsAUTOUNAT.XML. Checkisoinfo -dfor a Joliet record andisoinfo -J -ffor the real names before you blame the ISO.- Verify driver paths by mounting the ISO rather than inferring them from a failure. In one investigation the paths were correct the whole time and the real fault was trap 2.