WSUS01 is a test Windows Server 2025 Server Core VM (Proxmox VM 312) that installs itself with no clicks. It uses VirtIO disks and network and comes up running WSUS (Windows Server Update Services). You give it the Windows ISO and an administrator password, run one script on the Proxmox host, and about 20 minutes later WSUS answers on port 8530. This post is the complete rebuild guide, and every block has a Copy button.
How it works
create-vm.shon the Proxmox host downloads the current stable virtio-win driver ISO. It builds a small "UNATTEND" ISO that holdsautounattend.xml(with your password filled in) andsetup-wsus.ps1, then creates and starts VM 312 with three CDs: the Windows ISO, the UNATTEND ISO and the virtio-win ISO.- Windows Setup finds
autounattend.xmlon the CD by itself. It loads the VirtIO SCSI and network drivers withdrvloadso it can see the disks, and copies the same drivers toX:\virtio(the Setup RAM drive). It wipes Disk 0 and installs image 1 (Standard Evaluation, Server Core). - In the
offlineServicingpass, the drivers fromX:\virtioare injected into the new Windows before its first boot. Without this, first boot crashes withINACCESSIBLE_BOOT_DEVICE. - Windows sets the Administrator password and logs on once by itself. It then runs
setup-wsus.ps1from the UNATTEND CD, which:- installs all VirtIO drivers and the QEMU guest agent,
- turns on RDP, ping and WinRM,
- formats the 250 GB disk as
W:, - installs the WSUS role (Windows Internal Database) and runs
wsusutil postinstall, - runs a category-only sync, picks exactly two products and four classifications, and turns automatic sync off.
- Timing on this host: about 4–10 minutes from start to first logon, and about 15 minutes for the script (mostly the category sync). The script writes
C:\setup-wsus.donewhen it's finished.
This is a test build. No updates are approved and there's no full or daily sync, so it doesn't download update files.
Machines involved
| Machine | Role | LAN IP |
|---|---|---|
| VM 312 (WSUS01) | Windows Server 2025 Core eval + WSUS | DHCP, e.g. x.x.0.177 (a rebuild gets a new MAC, so a new IP) |
| Proxmox host (pve) | Runs the build scripts | x.x.0.195 |
VM sizing: 4 cores (cpu host), 8 GB RAM, SeaBIOS, q35. Disks are on one virtio-scsi-pci controller: scsi0 80 GB on local-lvm (C:) and scsi1 250 GB on Bulk (W:, WSUS content). The NIC is VirtIO on vmbr0. The VM starts on boot.
The ISO used here is Windows_Server_2025 _x64FRE_zh-cn.iso. It's the Evaluation edition (180 days, no product key needed), and Chinese is the only UI language on it. The unattend file sets a US keyboard and en-US formats, but Setup and the console are in Chinese.
Where to run each command
- Proxmox host shell: in the Proxmox web UI click pve → Shell (or SSH to the host as root).
- VM 312 (WSUS01) console: Proxmox web UI → 312 (WSUS01) → Console. Server Core opens a command prompt; type
powershellfor PowerShell. - A Windows client (only for the last step): an elevated Command Prompt on the PC you want to point at WSUS.
Rebuild, step by step
1. Put the Windows ISO in the Bulk ISO store
Proxmox web UI → pve → Bulk → ISO Images → Upload. Upload the Server 2025 ISO, and set the file name to exactly WinServer2025_zh-cn.iso (no space). It's 8.4 GB.
Check it's there (Proxmox host shell):
ls -l /mnt/pve/Bulk/template/iso/WinServer2025_zh-cn.iso
2. Make a working folder on the host
Proxmox host shell:
mkdir -p /root/wsus312 && cd /root/wsus312
3. Create every file
Paste each block into the Proxmox host shell. Each block writes one file into /root/wsus312/.
autounattend.xml: the Setup answer file. @@ADMIN_PASSWORD@@ is a placeholder that create-vm.sh replaces with your password.
cat > /root/wsus312/autounattend.xml <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<!-- Windows Server 2025 (zh-CN eval ISO) Server Core, unattended. VM 312 on Proxmox. -->
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<SetupUILanguage><UILanguage>zh-CN</UILanguage></SetupUILanguage>
<InputLocale>0409:00000409</InputLocale>
<SystemLocale>zh-CN</SystemLocale>
<UILanguage>zh-CN</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<RunSynchronous>
<!-- Load VirtIO SCSI driver from the virtio-win CD so Setup can see the disks -->
<RunSynchronousCommand wcm:action="add">
<Order>1</Order>
<Path>cmd /c for %d in (D E F G H I J K) do if exist %d:\vioscsi\2k25\amd64\vioscsi.inf drvload %d:\vioscsi\2k25\amd64\vioscsi.inf</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>2</Order>
<Path>cmd /c for %d in (D E F G H I J K) do if exist %d:\NetKVM\2k25\amd64\netkvm.inf drvload %d:\NetKVM\2k25\amd64\netkvm.inf</Path>
</RunSynchronousCommand>
<!-- Copy drivers to the fixed WinPE RAM drive X: so offlineServicing can inject them into the installed OS -->
<RunSynchronousCommand wcm:action="add">
<Order>3</Order>
<Path>cmd /c for %d in (D E F G H I J K) do if exist %d:\vioscsi\2k25\amd64\vioscsi.inf xcopy /e /i /y %d:\vioscsi\2k25\amd64 X:\virtio\vioscsi</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>4</Order>
<Path>cmd /c for %d in (D E F G H I J K) do if exist %d:\NetKVM\2k25\amd64\netkvm.inf xcopy /e /i /y %d:\NetKVM\2k25\amd64 X:\virtio\netkvm</Path>
</RunSynchronousCommand>
</RunSynchronous>
<DiskConfiguration>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add"><Order>1</Order><Type>Primary</Type><Size>500</Size></CreatePartition>
<CreatePartition wcm:action="add"><Order>2</Order><Type>Primary</Type><Extend>true</Extend></CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add"><Order>1</Order><PartitionID>1</PartitionID><Format>NTFS</Format><Label>System</Label><Active>true</Active></ModifyPartition>
<ModifyPartition wcm:action="add"><Order>2</Order><PartitionID>2</PartitionID><Format>NTFS</Format><Label>Windows</Label><Letter>C</Letter></ModifyPartition>
</ModifyPartitions>
</Disk>
</DiskConfiguration>
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add"><Key>/IMAGE/INDEX</Key><Value>1</Value></MetaData>
</InstallFrom>
<InstallTo><DiskID>0</DiskID><PartitionID>2</PartitionID></InstallTo>
</OSImage>
</ImageInstall>
<UserData>
<AcceptEula>true</AcceptEula>
<FullName>Administrator</FullName>
<Organization>503mail</Organization>
</UserData>
</component>
</settings>
<settings pass="offlineServicing">
<!-- Inject VirtIO SCSI + NIC drivers into the installed image (drvload alone => 0x7B on first boot) -->
<component name="Microsoft-Windows-PnpCustomizationsNonWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="1"><Path>X:\virtio</Path></PathAndCredentials>
</DriverPaths>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<ComputerName>WSUS01</ComputerName>
<TimeZone>Eastern Standard Time</TimeZone>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<InputLocale>0409:00000409</InputLocale>
<SystemLocale>zh-CN</SystemLocale>
<UILanguage>zh-CN</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<UserAccounts>
<AdministratorPassword><Value>@@ADMIN_PASSWORD@@</Value><PlainText>true</PlainText></AdministratorPassword>
</UserAccounts>
<AutoLogon>
<Enabled>true</Enabled>
<LogonCount>1</LogonCount>
<Username>Administrator</Username>
<Password><Value>@@ADMIN_PASSWORD@@</Value><PlainText>true</PlainText></Password>
</AutoLogon>
<OOBE>
<HideEULAPage>true</HideEULAPage>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<CommandLine>cmd /c for %d in (D E F G H I J K) do if exist %d:\setup-wsus.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File %d:\setup-wsus.ps1</CommandLine>
<Description>Guest agent, RDP, data disk, WSUS role</Description>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>
EOF
setup-wsus.ps1: runs once at first logon. It installs the drivers and guest agent, opens remote access, formats the data disk, then installs and configures WSUS.
cat > /root/wsus312/setup-wsus.ps1 <<'EOF'
# First-logon setup for WSUS01 (VM 312). Runs from the unattend CD via FirstLogonCommands.
# Log: C:\setup-wsus.log Done marker: C:\setup-wsus.done
Start-Transcript -Path C:\setup-wsus.log -Append
$ErrorActionPreference = 'Continue'
# Administrator password never expires
Set-LocalUser -Name Administrator -PasswordNeverExpires $true
# All VirtIO drivers (net, balloon, serial...) + QEMU guest agent from the virtio-win CD
$gt = Get-PSDrive -PSProvider FileSystem | ForEach-Object { Join-Path $_.Root 'virtio-win-gt-x64.msi' } | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($gt) { Start-Process msiexec.exe -ArgumentList "/i `"$gt`" /qn /norestart" -Wait }
$ga =Get-PSDrive -PSProvider FileSystem | ForEach-Object { Join-Path $_.Root 'guest-agent\qemu-ga-x86_64.msi' } | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($ga) { Start-Process msiexec.exe -ArgumentList "/i `"$ga`" /qn /norestart" -Wait }
# Remote Desktop + ping + WinRM (rule names/groups are language-neutral)
Set-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0
Enable-NetFirewallRule -Group '@FirewallAPI.dll,-28752'
Enable-NetFirewallRule -Name 'FPS-ICMP4-ERQ-In'
Enable-PSRemoting -Force -SkipNetworkProfileCheck
# WSUS content disk (second, blank disk) -> W:
Get-Disk | Where-Object PartitionStyle -eq 'RAW' | Select-Object -First 1 |
Initialize-Disk -PartitionStyle GPT -PassThru |
New-Partition -DriveLetter W -UseMaximumSize |
Format-Volume -FileSystem NTFS -NewFileSystemLabel WSUS -Confirm:$false
New-Item -ItemType Directory -Force W:\WSUS | Out-Null
# WSUS role (Windows Internal Database) + post-install
Install-WindowsFeature -Name UpdateServices -IncludeManagementTools
& 'C:\Program Files\Update Services\Tools\wsusutil.exe' postinstall CONTENT_DIR=W:\WSUS
New-NetFirewallRule -DisplayName 'WSUS 8530-8531' -Direction Inbound -Protocol TCP -LocalPort 8530,8531 -Action Allow
# Basic WSUS config: sync from Microsoft Update, English + Chinese, then categories-only sync
$wsus = Get-WsusServer -Name localhost -PortNumber 8530
$cfg = $wsus.GetConfiguration()
$cfg.SyncFromMicrosoftUpdate = $true
$cfg.AllUpdateLanguagesEnabled = $false
$cfg.SetEnabledUpdateLanguages([string[]]@('en','zh-cn'))
$cfg.Save()
$sub = $wsus.GetSubscription()
$sub.StartSynchronizationForCategoryOnly()
$deadline = (Get-Date).AddMinutes(90)
while ($sub.GetSynchronizationStatus() -ne 'NotProcessing' -and (Get-Date) -lt $deadline) { Start-Sleep 30 }
"Category sync result: $($sub.GetLastSynchronizationInfo().Result)"
# EXACT products (Server 2025 = "Microsoft Server Operating System-24H2") + Defender.
# SetUpdateCategories replaces the default subscription (which includes the whole "Windows" family);
# Set-WsusProduct only adds to it.
$prods = New-Object Microsoft.UpdateServices.Administration.UpdateCategoryCollection
$wsus.GetUpdateCategories() | Where-Object { $_.Title -in 'Microsoft Server Operating System-24H2','Microsoft Defender Antivirus' } | ForEach-Object { [void]$prods.Add($_) }
$sub.SetUpdateCategories($prods)
$classes = New-Object Microsoft.UpdateServices.Administration.UpdateClassificationCollection
$wsus.GetUpdateClassifications() | Where-Object { $_.Title -in 'Critical Updates','Security Updates','Definition Updates','Updates' } | ForEach-Object { [void]$classes.Add($_) }
$sub.SetUpdateClassifications($classes)
# Test build: no full catalog sync and no daily sync (start one later with $sub.StartSynchronization())
$sub.SynchronizeAutomatically = $false
$sub.Save()
"Enabled products:"; $sub.GetUpdateCategories() | ForEach-Object Title
"Enabled classifications:"; $sub.GetUpdateClassifications() | ForEach-Object Title
Set-Content C:\setup-wsus.done (Get-Date)
Stop-Transcript
EOF
create-vm.sh: builds the UNATTEND ISO, then creates and starts VM 312.
cat > /root/wsus312/create-vm.sh <<'EOF'
#!/bin/bash
# Build VM 312 WSUS01 (Windows Server 2025 Core eval, zh-CN, VirtIO) - runs on the Proxmox host.
# Usage: bash create-vm.sh <administrator-password>
# Inputs in $SRC (default: the PC share D:\Proxmox-backups\iso-transfer = /mnt/pve/pc-backups/iso-transfer):
# autounattend.xml, setup-wsus.ps1, and WinServer2025_zh-cn.iso unless it is already in the Bulk ISO store
set -e
ADMIN_PW="${1:?usage: create-vm.sh <administrator-password>}"
SRC="${SRC:-/mnt/pve/pc-backups/iso-transfer}"
ISO=/mnt/pve/Bulk/template/iso
VMID=312
# 1. Server ISO -> Bulk ISO store
[ -f $ISO/WinServer2025_zh-cn.iso ] || cp $SRC/WinServer2025_zh-cn.iso $ISO/
# 2. Latest stable VirtIO drivers ISO (has 2k25 drivers)
[ -f $ISO/virtio-win-stable.iso ] || wget -q -O $ISO/virtio-win-stable.iso \
https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso
# 3. Small ISO holding autounattend.xml + setup-wsus.ps1 (Setup scans all CDs for autounattend.xml)
mkdir -p /root/wsus-unattend
cp $SRC/setup-wsus.ps1 /root/wsus-unattend/
# Password goes in at build time so it is not stored in autounattend.xml (avoid | & \ in it: sed)
sed "s|@@ADMIN_PASSWORD@@|$ADMIN_PW|g" $SRC/autounattend.xml > /root/wsus-unattend/autounattend.xml
genisoimage -quiet -J -r -V UNATTEND -o $ISO/wsus312-unattend.iso /root/wsus-unattend
# 4. VM: ONE virtio-scsi-pci controller so scsi0 = Windows Disk 0 (virtio-scsi-single made the data disk Disk 0). VirtIO SCSI disks (80G OS on local-lvm, 250G WSUS content on Bulk), VirtIO NIC
qm create $VMID --name WSUS01 --ostype win11 --machine q35 --bios seabios \
--cores 4 --memory 8192 --cpu host --agent 1 \
--scsihw virtio-scsi-pci \
--scsi0 local-lvm:80,discard=on,ssd=1 \
--scsi1 Bulk:250,discard=on,ssd=1 \
--ide2 Bulk:iso/WinServer2025_zh-cn.iso,media=cdrom \
--sata2 Bulk:iso/wsus312-unattend.iso,media=cdrom \
--sata3 Bulk:iso/virtio-win-stable.iso,media=cdrom \
--net0 virtio,bridge=vmbr0 \
--boot 'order=scsi0;ide2' --onboot 1
# 5. Start - install runs unattended (~15-25 min incl. WSUS role + first category sync)
qm start $VMID
qm status $VMID
EOF
check-vm.sh: status from the host through the guest agent. Shows the IP, the setup log tail, the done marker, sync progress, WsusService, and the disks.
cat > /root/wsus312/check-vm.sh <<'EOF'
#!/bin/bash
# Status of VM 312 via the QEMU guest agent - runs on the Proxmox host.
VMID=312
qm agent $VMID ping 2>/dev/null || { echo "Guest agent not up yet (normal until setup-wsus.ps1 installs it)"; exit 0; }
qm agent $VMID network-get-interfaces | grep -o '"ip-address" : "[0-9.]*"'
qm guest exec $VMID --timeout 60 -- powershell -NoProfile -Command 'Get-Content C:\setup-wsus.log -Tail 40; Test-Path C:\setup-wsus.done; $s=(Get-WsusServer -Name localhost -PortNumber 8530).GetSubscription(); $p=$s.GetSynchronizationProgress(); "Sync: $($s.GetSynchronizationStatus()) phase=$($p.Phase) $($p.ProcessedItems)/$($p.TotalItems)";Get-Service WsusService | Format-List Name,Status; Get-Volume -DriveLetter C,W | Format-Table DriveLetter,FileSystemLabel,Size,SizeRemaining' | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("out-data","")); print(d.get("err-data",""))'
EOF
check-wsus-config.sh: shows the WSUS products and classifications that are turned on.
cat > /root/wsus312/check-wsus-config.sh <<'EOF'
#!/bin/bash
# Show WSUS enabled products/classifications + selfupdate folder on VM 312 - runs on the Proxmox host.
qm guest exec 312 --timeout 120 -- powershell -NoProfile -Command '$w=Get-WsusServer -Name localhost -PortNumber 8530; $c=$w.GetSubscription(); "PRODUCTS:"; $c.GetUpdateCategories() | ForEach-Object Title; "CLASSIFICATIONS:"; $c.GetUpdateClassifications() | ForEach-Object Title; "SELFUPDATE:"; Get-ChildItem "C:\Program Files\Update Services\SelfUpdate" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 5 FullName | Format-Table -HideTableHeaders' | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("out-data","")); print(d.get("err-data",""))'
EOF
run-in-vm.sh: runs any PowerShell file inside the VM through the guest agent.
cat > /root/wsus312/run-in-vm.sh <<'EOF'
#!/bin/bash
# Run a PowerShell script file inside VM 312 via the guest agent (script piped on stdin).
# Usage: bash run-in-vm.sh /mnt/pve/pc-backups/iso-transfer/some-script.ps1
qm guest exec 312 --timeout 600 --pass-stdin 1 -- powershell -NoProfile -ExecutionPolicy Bypass -Command - < "$1" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("out-data","")); print(d.get("err-data",""))'
EOF
stop-wsus-sync.ps1: stops a running sync and turns off automatic sync. You don't need it on a fresh build; it's here in case a sync gets started.
cat > /root/wsus312/stop-wsus-sync.ps1 <<'EOF'
# Test build: stop any running WSUS sync and turn off the daily sync.
$sub = (Get-WsusServer -Name localhost -PortNumber 8530).GetSubscription()
if ($sub.GetSynchronizationStatus() -ne 'NotProcessing') { $sub.StopSynchronization() }
# Save() throws while the sync is still "Stopping"
while ($sub.GetSynchronizationStatus() -ne 'NotProcessing') { Start-Sleep 5 }
$sub.SynchronizeAutomatically = $false
$sub.Save()
"Sync status: $($sub.GetSynchronizationStatus()) Automatic: $($sub.SynchronizeAutomatically)"
EOF
4. If VM 312 already exists, remove it
This destroys VM 312 and its disks. Proxmox host shell:
qm stop 312; qm destroy 312 --purge
5. Build and start the VM
Proxmox host shell. Replace YourPassword with the Administrator password you want. Don't use the characters |, & or \ in it, because sed puts it into the answer file.
SRC=/root/wsus312 bash /root/wsus312/create-vm.sh 'YourPassword'
The last line it prints should be status: running. You'll also see a thin-pool over-provisioning warning from LVM; it's harmless (see Notes).
6. Watch it install
Proxmox web UI → 312 (WSUS01) → Console. You'll see Windows Setup run with no prompts, reboot a few times, then a command prompt that runs setup-wsus.ps1.
7. Check it from the host
Proxmox host shell. Run this every few minutes. Until the guest agent is installed, it prints Guest agent not up yet, which is normal.
bash /root/wsus312/check-vm.sh
It's finished when the output shows True (the done marker), Status : Running for WsusService, a W volume labelled WSUS, and Sync: NotProcessing. Note the ip-address line; that's the WSUS IP.
Confirm the products and classifications:
bash /root/wsus312/check-wsus-config.sh
Expected: products Microsoft Defender Antivirus and Microsoft Server Operating System-24H2 (Server 2025), and classifications Critical Updates, Definition Updates, Security Updates and Updates.
8. Test WSUS from another machine
From any machine on the LAN, open this in a browser, with <WSUS-IP> replaced by the IP from step 7. It should load a page about the ClientWebService (HTTP 200):
http://<WSUS-IP>:8530/ClientWebService/client.asmx
9. (Optional) Point a Windows client at it
On the client, in an elevated Command Prompt, with <WSUS-IP> replaced:
reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate /v WUServer /t REG_SZ /d http://<WSUS-IP>:8530 /f
reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate /v WUStatusServer /t REG_SZ /d http://<WSUS-IP>:8530 /f
reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU /v UseWUServer /t REG_DWORD /d 1 /f
10. (Later) Start a real catalog sync
Only do this if you want WSUS to fetch the update list from Microsoft. In the VM 312 console, type powershell, then:
(Get-WsusServer -Name localhost -PortNumber 8530).GetSubscription().StartSynchronization()
Troubleshooting: where the logs are
Windows Setup (before first logon)
- Where: the VM screen; inside Windows,
C:\Windows\Panther\setupact.logandsetuperr.log - Open: Proxmox web UI → 312 (WSUS01) → Console
- Look for: if Setup stops at a disk list with a yellow warning ("无法在此驱动器上安装" = can't install on this drive), the data disk became Disk 0. Make sure the VM uses
--scsihw virtio-scsi-pci, notvirtio-scsi-single. A blue screen withINACCESSIBLE_BOOT_DEVICE (0x7B)means the VirtIO SCSI driver wasn't injected; check theofflineServicingsection ofautounattend.xml.
First-logon script
- Where:
C:\setup-wsus.login VM 312 (done markerC:\setup-wsus.done) - Open: Proxmox host shell
bash /root/wsus312/check-vm.sh
- Look for:
Category sync result: Succeeded, then theEnabled products:/Enabled classifications:lists. Red PowerShell errors through the guest agent show up as garbled characters (the Chinese GBK console encoding). To read them, open the file in the VM console instead:
type C:\setup-wsus.log
WSUS service and sync
- Where: WSUS log
C:\Program Files\Update Services\LogFiles\SoftwareDistribution.log; postinstall log%TEMP%\WSUS_PostInstall_*.log - Open: VM 312 console →
powershell
Get-Content 'C:\Program Files\Update Services\LogFiles\SoftwareDistribution.log' -Tail 50
- Look for:
WsusServiceshould be Running (seecheck-vm.sh).Save()errors that say the subscription is running mean a sync is still stopping; wait forNotProcessingand try again (stop-wsus-sync.ps1already does this)./selfupdate/wuident.cabreturning 404 is harmless, because only XP-era clients use it.
Guest agent
- Open: Proxmox host shell
qm agent 312 ping && echo AGENT_UP
- Look for:
QEMU guest agent is not runningis normal untilsetup-wsus.ps1installs it, a few minutes after first logon.
Quick health check
Proxmox host shell:
bash /root/wsus312/check-vm.sh | grep -E 'ip-address|Sync:|^True|Status :'
Notes
- Run scripts in the VM:
bash /root/wsus312/run-in-vm.sh /root/wsus312/stop-wsus-sync.ps1pipes a.ps1into PowerShell inside the VM through the guest agent. Typingqm guest exec 312 -- cmd /c ...by hand through several shells mangled the quotes, so script files are more reliable. - Disk order gotcha: with
virtio-scsi-single, every disk gets its own controller, and Windows listed the 250 GB disk as Disk 0. With onevirtio-scsi-pcicontroller,scsi0is Disk 0. - Driver gotcha:
drvloadonly loads a driver into Setup's own RAM-disk Windows. The installed Windows needs it injected separately (PnpCustomizationsNonWinPEinofflineServicing, pointing atX:\virtio). - WSUS product gotcha: after
postinstall, the default subscription already includes the whole Windows product family.Set-WsusProductonly adds products, so the first sync pulled about 135k updates.SetUpdateCategories()sets the exact list (about 20k with Server 2025 + Defender). - Thin pool warning:
qm createwarns that the total thin volume sizes exceed thelocal-lvmpool. Thin disks only use what's written, but keep an eye on pool usage. - Evaluation: 180 days. Check what's left with
slmgr /dliin the VM console. - Change the password later: in the VM console,
net user Administrator *asks for a new one.