Initial commit: Packer + QEMU/KVM Windows 11 minimal VM

Fully unattended Windows 11 Pro build using Autounattend.xml, swtpm
for TPM 2.0 emulation, and VirtIO drivers. Five PowerShell provisioners
strip bloatware, telemetry, unused services, and apply performance tuning
for a resource-constrained host. Output is a compressed QCOW2 image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 19:31:18 -05:00
commit e4741ccc96
12 changed files with 980 additions and 0 deletions

20
.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Local build variables — contains ISO path and credentials
variables.pkrvars.hcl
# Build output
output/
# Downloaded ISOs (large binaries, not worth tracking)
*.iso
# swtpm state directories created by build.sh
.swtpm*/
# Packer plugin cache
packer_cache/
# Editor / OS noise
.DS_Store
Thumbs.db
*.swp
*~

48
Makefile Normal file
View File

@@ -0,0 +1,48 @@
VIRTIO_VERSION := 0.1.266
VIRTIO_ISO_URL := https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-$(VIRTIO_VERSION)-1/virtio-win-$(VIRTIO_VERSION).iso
VIRTIO_ISO := virtio-win.iso
VAR_FILE := variables.pkrvars.hcl
.PHONY: all build init validate clean distclean help
all: build
## Install Packer QEMU plugin
init:
packer init .
## Download VirtIO drivers ISO (~500 MB, one-time)
$(VIRTIO_ISO):
curl -L --progress-bar -o $(VIRTIO_ISO) $(VIRTIO_ISO_URL)
virtio-iso: $(VIRTIO_ISO)
## Validate the Packer template (does not run the build)
validate: $(VIRTIO_ISO)
SWTPM_SOCK=/tmp/dummy-validate \
packer validate -var "swtpm_sock=/tmp/dummy" -var-file=$(VAR_FILE) .
## Build the VM image (starts swtpm, runs Packer, cleans up)
build: init $(VIRTIO_ISO)
@test -f $(VAR_FILE) || { \
echo "ERROR: $(VAR_FILE) not found."; \
echo " cp variables.pkrvars.hcl.example $(VAR_FILE) and fill it in."; \
exit 1; }
bash build.sh $(VAR_FILE)
## Remove build output (keeps downloaded ISOs)
clean:
rm -rf output/
## Remove build output and downloaded ISOs
distclean: clean
rm -f $(VIRTIO_ISO)
help:
@echo "Targets:"
@echo " build Build the VM image (default)"
@echo " validate Validate Packer config without building"
@echo " init Install Packer plugins"
@echo " virtio-iso Download the VirtIO drivers ISO"
@echo " clean Remove build output"
@echo " distclean Remove build output + downloaded ISOs"

157
README.md Normal file
View File

@@ -0,0 +1,157 @@
# lite-win
A repeatable, minimal Windows 11 Pro VM image built with [Packer](https://www.packer.io/) and QEMU/KVM. No bloatware, no Microsoft account requirement, no unnecessary services.
Designed for a resource-constrained Linux host running Fedora with QEMU/KVM.
---
## What it does
1. Boots the Windows 11 Pro ISO fully unattended via `Autounattend.xml`
2. Partitions the disk (GPT/UEFI), installs Windows, configures WinRM
3. Runs five PowerShell provisioner scripts over WinRM:
- Removes ~30 built-in apps and OneDrive
- Disables telemetry, Cortana, advertising ID, and CEIP
- Disables resource-heavy services (SysMain, Windows Search, Xbox, etc.)
- Applies performance tweaks (High Performance power plan, fixed page file, no animations)
- Cleans up temp files and compacts the OS
4. Produces a thin-provisioned, compressed QCOW2 disk image (~1215 GB)
---
## Prerequisites
Install these on the Fedora host before building:
```bash
sudo dnf install -y qemu-kvm swtpm edk2-ovmf packer
```
> **Packer** may not be in the Fedora repos depending on your version. If `dnf` doesn't find it, download the Linux AMD64 binary from [releases.hashicorp.com/packer](https://releases.hashicorp.com/packer/) and place it in `~/.local/bin/`.
You also need:
- **Windows 11 Pro retail ISO** — download from [microsoft.com/software-download/windows11](https://www.microsoft.com/software-download/windows11) (select the x64 multi-edition ISO)
- **VirtIO drivers ISO** — downloaded automatically by `make virtio-iso`
---
## Quick start
```bash
# 1. Download VirtIO drivers
make virtio-iso
# 2. Create your local variable file
cp variables.pkrvars.hcl.example variables.pkrvars.hcl
```
Edit `variables.pkrvars.hcl` and set:
```hcl
win_iso_path = "/path/to/Win11_24H2_English_x64.iso"
win_iso_checksum = "sha256:<output of: sha256sum /path/to/Win11_*.iso>"
```
```bash
# 3. Build
make build
```
The finished image lands in `output/windows11-lite`.
---
## Running the image
### Recommended: virt-install (repeatable)
```bash
virt-install \
--name windows11-lite \
--memory 4096 \
--vcpus 2 \
--disk output/windows11-lite,format=qcow2,bus=virtio \
--import \
--os-variant win11 \
--network network=default,model=virtio \
--graphics spice,listen=none \
--video qxl \
--boot uefi \
--tpm emulator,model=tpm-tis,version=2.0 \
--noautoconsole
```
Then connect with `virt-manager` or `virt-viewer windows11-lite`.
### virt-manager GUI
1. **File → New VM → Import existing disk image**
2. Browse to `output/windows11-lite`; set OS to *Windows 11*
3. Set RAM and CPUs, then **Customize configuration before install**
4. Set **Firmware**`UEFI x86_64` (not BIOS)
5. Set disk **Bus**`VirtIO`
6. **Add Hardware → TPM** → Emulated, TIS, version 2.0
7. Begin installation
> **All three settings — UEFI boot, VirtIO disk bus, TPM 2.0 — are required.** The image was built against them; mismatching any one causes a boot failure.
---
## Windows activation
The build uses the generic KMS client key `W269N-WFGWX-YVC9B-4J6C9-T83GX` to allow unattended installation. This is **not** an activation key. Activate Windows with your own license after the build.
---
## Variables reference
| Variable | Default | Description |
|---|---|---|
| `win_iso_path` | — | **Required.** Absolute path to the Windows 11 ISO |
| `win_iso_checksum` | — | **Required.** `sha256:<hash>` of the ISO |
| `virtio_iso_path` | `virtio-win.iso` | Path to VirtIO drivers ISO |
| `vm_name` | `windows11-lite` | Output image name |
| `disk_size` | `51200` | Virtual disk size in MB (50 GB) |
| `memory` | `4096` | RAM in MB during the Packer build |
| `cpus` | `2` | vCPUs during the Packer build |
| `output_directory` | `output` | Output directory for the QCOW2 image |
| `winrm_password` | `Packer1234!` | Build-time WinRM password — must match `Autounattend.xml` |
| `ovmf_code` | `/usr/share/edk2/ovmf/OVMF_CODE.fd` | UEFI firmware code (Fedora default) |
| `ovmf_vars` | `/usr/share/edk2/ovmf/OVMF_VARS.fd` | UEFI firmware vars template (Fedora default) |
All variables except `win_iso_path` and `win_iso_checksum` have usable defaults.
---
## Project structure
```
.
├── build.sh # Starts swtpm, runs packer, cleans up on exit
├── Makefile # Convenience targets
├── windows11.pkr.hcl # Packer template
├── variables.pkrvars.hcl.example # Variable template (copy → variables.pkrvars.hcl)
├── http/
│ └── Autounattend.xml # Unattended Windows install answer file
└── scripts/
├── 01-debloat-apps.ps1 # Remove built-in apps and OneDrive
├── 02-disable-telemetry.ps1 # Disable telemetry, Cortana, CEIP
├── 03-disable-services.ps1 # Disable unnecessary services
├── 04-performance.ps1 # Power plan, page file, visual effects
└── 05-cleanup.ps1 # DISM cleanup, temp purge, Compact OS
```
---
## Make targets
| Target | Description |
|---|---|
| `make build` | Build the VM image (default) |
| `make validate` | Validate Packer config without building |
| `make init` | Install Packer plugins |
| `make virtio-iso` | Download the VirtIO drivers ISO |
| `make clean` | Remove build output |
| `make distclean` | Remove build output and downloaded ISOs |

51
build.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Orchestrates swtpm (software TPM 2.0) and runs the Packer build.
# swtpm must be running before QEMU starts; this script handles that lifecycle.
set -euo pipefail
VAR_FILE="${1:-variables.pkrvars.hcl}"
if [[ ! -f "$VAR_FILE" ]]; then
echo "ERROR: $VAR_FILE not found. Copy variables.pkrvars.hcl.example and fill it in." >&2
exit 1
fi
SWTPM_DIR="$(mktemp -d --tmpdir lite-win-swtpm.XXXXXX)"
SWTPM_SOCK="${SWTPM_DIR}/swtpm.sock"
SWTPM_PID="${SWTPM_DIR}/swtpm.pid"
cleanup() {
if [[ -f "$SWTPM_PID" ]]; then
kill "$(cat "$SWTPM_PID")" 2>/dev/null || true
fi
rm -rf "$SWTPM_DIR"
}
trap cleanup EXIT
echo "==> Starting swtpm (TPM 2.0 emulator)..."
swtpm socket \
--tpmstate "dir=${SWTPM_DIR}" \
--ctrl "type=unixio,path=${SWTPM_SOCK}" \
--pid "file=${SWTPM_PID}" \
--log level=1 \
--tpm2 \
--daemon
# Give swtpm a moment to create the socket
for i in $(seq 1 10); do
[[ -S "$SWTPM_SOCK" ]] && break
sleep 0.3
done
if [[ ! -S "$SWTPM_SOCK" ]]; then
echo "ERROR: swtpm socket not created at ${SWTPM_SOCK}" >&2
exit 1
fi
echo "==> swtpm ready at ${SWTPM_SOCK}"
echo "==> Running Packer build..."
packer build \
-var "swtpm_sock=${SWTPM_SOCK}" \
-var-file="${VAR_FILE}" \
.

286
http/Autounattend.xml Normal file
View File

@@ -0,0 +1,286 @@
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<!-- ================================================================
Pass 1: windowsPE — runs inside WinPE before installation begins
================================================================ -->
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SetupUILanguage>
<UILanguage>en-US</UILanguage>
</SetupUILanguage>
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<!-- Inject VirtIO SCSI storage driver so the virtio-scsi disk is visible.
Packer mounts three CD-ROMs (Windows ISO, Autounattend CD, VirtIO ISO)
in unspecified order; searching D-G covers all practical assignments. -->
<component name="Microsoft-Windows-PnpCustomizationsWinPE"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="D">
<Path>D:\amd64\w11</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="E">
<Path>E:\amd64\w11</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="F">
<Path>F:\amd64\w11</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="G">
<Path>G:\amd64\w11</Path>
</PathAndCredentials>
</DriverPaths>
</component>
<component name="Microsoft-Windows-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- Bypass Secure Boot and RAM requirements.
TPM 2.0 is provided by swtpm so no TPM bypass is needed.
OVMF_CODE.fd does not emulate Secure Boot; bypass prevents setup from blocking. -->
<RunSynchronousCommands wcm:action="add">
<RunSynchronousCommand wcm:action="add">
<Order>1</Order>
<Path>cmd.exe /c reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>2</Order>
<Path>cmd.exe /c reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
</RunSynchronousCommands>
<AcceptEula>true</AcceptEula>
<!-- Select Windows 11 Pro by name (safer than index, which varies between ISOs) -->
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/NAME</Key>
<Value>Windows 11 Pro</Value>
</MetaData>
</InstallFrom>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>3</PartitionID>
</InstallTo>
<WillShowUI>OnError</WillShowUI>
</OSImage>
</ImageInstall>
<!-- Generic KMS client key for Windows 11 Pro — allows installation to proceed.
This is NOT an activation key; activate Windows separately after build. -->
<UserData>
<AcceptEula>true</AcceptEula>
<FullName>Administrator</FullName>
<Organization>Lab</Organization>
<ProductKey>
<Key>W269N-WFGWX-YVC9B-4J6C9-T83GX</Key>
<WillShowUI>OnError</WillShowUI>
</ProductKey>
</UserData>
<!-- GPT partition layout for UEFI boot -->
<DiskConfiguration>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order>
<Type>EFI</Type>
<Size>100</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>2</Order>
<Type>MSR</Type>
<Size>16</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>3</Order>
<Type>Primary</Type>
<Extend>true</Extend>
</CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add">
<Order>1</Order>
<PartitionID>1</PartitionID>
<Label>System</Label>
<Format>FAT32</Format>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>2</Order>
<PartitionID>2</PartitionID>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>3</Order>
<PartitionID>3</PartitionID>
<Label>Windows</Label>
<Format>NTFS</Format>
<Letter>C</Letter>
</ModifyPartition>
</ModifyPartitions>
</Disk>
</DiskConfiguration>
</component>
</settings>
<!-- ================================================================
Pass 4: specialize — first boot of the installed OS, pre-OOBE
================================================================ -->
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ComputerName>WINDOWS11-LITE</ComputerName>
<TimeZone>UTC</TimeZone>
</component>
<component name="Microsoft-Windows-Security-SPP-UX"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SkipAutoActivation>true</SkipAutoActivation>
</component>
<component name="Microsoft-Windows-SQMApi"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<CEIPEnabled>0</CEIPEnabled>
</component>
</settings>
<!-- ================================================================
Pass 7: oobeSystem — OOBE configuration and first-logon commands
================================================================ -->
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UILanguageFallback>en-US</UILanguageFallback>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>3</ProtectYourPC>
<SkipUserOOBE>true</SkipUserOOBE>
<SkipMachineOOBE>true</SkipMachineOOBE>
</OOBE>
<!-- Enable the built-in Administrator account with a known password.
The password here must match the winrm_password Packer variable. -->
<UserAccounts>
<AdministratorPassword>
<Value>Packer1234!</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
</UserAccounts>
<AutoLogon>
<Password>
<Value>Packer1234!</Value>
<PlainText>true</PlainText>
</Password>
<Enabled>true</Enabled>
<LogonCount>8</LogonCount>
<Username>Administrator</Username>
</AutoLogon>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<CommandLine>cmd.exe /c net user administrator /active:yes</CommandLine>
</SynchronousCommand>
<!-- Allow OOBE to complete without a network connection (Windows 11 22H2+) -->
<SynchronousCommand wcm:action="add">
<Order>2</Order>
<CommandLine>cmd.exe /c reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</CommandLine>
</SynchronousCommand>
<!-- WinRM setup so Packer provisioners can connect -->
<SynchronousCommand wcm:action="add">
<Order>3</Order>
<CommandLine>cmd.exe /c winrm quickconfig -q</CommandLine>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>4</Order>
<CommandLine>cmd.exe /c winrm set winrm/config/service @{AllowUnencrypted="true"}</CommandLine>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>5</Order>
<CommandLine>cmd.exe /c winrm set winrm/config/service/auth @{Basic="true"}</CommandLine>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>6</Order>
<CommandLine>cmd.exe /c winrm set winrm/config/winrs @{MaxMemoryPerShellMB="512"}</CommandLine>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>7</Order>
<CommandLine>cmd.exe /c netsh advfirewall firewall add rule name="WinRM-HTTP" protocol=TCP dir=in localport=5985 action=allow</CommandLine>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>8</Order>
<CommandLine>cmd.exe /c sc config winrm start= auto</CommandLine>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>

View File

@@ -0,0 +1,71 @@
#Requires -RunAsAdministrator
$apps = @(
# Microsoft bloat
"Microsoft.3DBuilder",
"Microsoft.BingFinance",
"Microsoft.BingNews",
"Microsoft.BingSearch",
"Microsoft.BingSports",
"Microsoft.BingTranslator",
"Microsoft.BingWeather",
"Microsoft.GetHelp",
"Microsoft.Getstarted",
"Microsoft.GamingApp",
"Microsoft.Microsoft3DViewer",
"Microsoft.MicrosoftOfficeHub",
"Microsoft.MicrosoftSolitaireCollection",
"Microsoft.MicrosoftStickyNotes",
"Microsoft.MixedReality.Portal",
"Microsoft.Office.OneNote",
"Microsoft.People",
"Microsoft.PowerAutomateDesktop",
"Microsoft.Print3D",
"Microsoft.SkypeApp",
"Microsoft.Todos",
"Microsoft.WindowsAlarms",
"Microsoft.WindowsCamera",
"Microsoft.windowscommunicationsapps", # Mail + Calendar
"Microsoft.WindowsFeedbackHub",
"Microsoft.WindowsMaps",
"Microsoft.WindowsPhone",
"Microsoft.WindowsSoundRecorder",
"Microsoft.Whiteboard",
"Microsoft.Windows.DevHome",
# Xbox ecosystem
"Microsoft.Xbox.TCUI",
"Microsoft.XboxApp",
"Microsoft.XboxGameOverlay",
"Microsoft.XboxGamingOverlay",
"Microsoft.XboxIdentityProvider",
"Microsoft.XboxSpeechToTextOverlay",
# Media / entertainment
"Microsoft.ZuneMusic",
"Microsoft.ZuneVideo",
"Clipchamp.Clipchamp",
# Teams personal and Cortana
"MicrosoftTeams",
"Microsoft.549981C3F5F10",
# Phone Link
"Microsoft.YourPhone"
)
foreach ($app in $apps) {
Get-AppxPackage -Name $app -AllUsers -ErrorAction SilentlyContinue |
Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue
Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue |
Where-Object { $_.PackageName -like "*$app*" } |
Remove-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue
}
# Uninstall OneDrive
Stop-Process -Name OneDrive -Force -ErrorAction SilentlyContinue
$uninstaller = if (Test-Path "$env:SYSTEMROOT\SysWOW64\OneDriveSetup.exe") {
"$env:SYSTEMROOT\SysWOW64\OneDriveSetup.exe"
} else {
"$env:SYSTEMROOT\System32\OneDriveSetup.exe"
}
if (Test-Path $uninstaller) {
Start-Process $uninstaller -ArgumentList "/uninstall" -Wait -ErrorAction SilentlyContinue
}
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\OneDrive" /v DisableFileSyncNGSC /t REG_DWORD /d 1 /f | Out-Null

View File

@@ -0,0 +1,63 @@
#Requires -RunAsAdministrator
# Telemetry level — policy key can enforce 0 (Security) even on Home/Pro
$telemetryPaths = @(
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection"
)
foreach ($p in $telemetryPaths) {
New-Item -Path $p -Force -ErrorAction SilentlyContinue | Out-Null
Set-ItemProperty -Path $p -Name AllowTelemetry -Value 0 -Type DWord -Force
Set-ItemProperty -Path $p -Name MaxTelemetryAllowed -Value 0 -Type DWord -Force
}
# Advertising ID
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\AdvertisingInfo" -Force -ErrorAction SilentlyContinue | Out-Null
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\AdvertisingInfo" -Name DisabledByGroupPolicy -Value 1 -Type DWord -Force
# Windows Error Reporting
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Error Reporting" /v Disabled /t REG_DWORD /d 1 /f | Out-Null
# Customer Experience Improvement Program
reg add "HKLM\SOFTWARE\Policies\Microsoft\SQMClient\Windows" /v CEIPEnable /t REG_DWORD /d 0 /f | Out-Null
# Feedback notifications
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection" /v DoNotShowFeedbackNotifications /t REG_DWORD /d 1 /f | Out-Null
# Cortana and web search
$searchPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search"
New-Item -Path $searchPath -Force -ErrorAction SilentlyContinue | Out-Null
Set-ItemProperty -Path $searchPath -Name AllowCortana -Value 0 -Type DWord -Force
Set-ItemProperty -Path $searchPath -Name DisableWebSearch -Value 1 -Type DWord -Force
Set-ItemProperty -Path $searchPath -Name ConnectedSearchUseWeb -Value 0 -Type DWord -Force
Set-ItemProperty -Path $searchPath -Name AllowSearchToUseLocation -Value 0 -Type DWord -Force
# Activity History / Timeline
$systemPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\System"
New-Item -Path $systemPath -Force -ErrorAction SilentlyContinue | Out-Null
Set-ItemProperty -Path $systemPath -Name EnableActivityFeed -Value 0 -Type DWord -Force
Set-ItemProperty -Path $systemPath -Name PublishUserActivities -Value 0 -Type DWord -Force
Set-ItemProperty -Path $systemPath -Name UploadUserActivities -Value 0 -Type DWord -Force
# Disable DiagTrack (telemetry) service
Stop-Service DiagTrack -Force -ErrorAction SilentlyContinue
Set-Service DiagTrack -StartupType Disabled -ErrorAction SilentlyContinue
# Disable telemetry-related scheduled tasks
$tasks = @(
@{Path="\Microsoft\Windows\Application Experience"; Name="Microsoft Compatibility Appraiser"},
@{Path="\Microsoft\Windows\Application Experience"; Name="ProgramDataUpdater"},
@{Path="\Microsoft\Windows\Application Experience"; Name="StartupAppTask"},
@{Path="\Microsoft\Windows\Autochk"; Name="Proxy"},
@{Path="\Microsoft\Windows\Customer Experience Improvement Program"; Name="Consolidator"},
@{Path="\Microsoft\Windows\Customer Experience Improvement Program"; Name="UsbCeip"},
@{Path="\Microsoft\Windows\DiskDiagnostic"; Name="Microsoft-Windows-DiskDiagnosticDataCollector"},
@{Path="\Microsoft\Windows\Feedback\Siuf"; Name="DmClient"},
@{Path="\Microsoft\Windows\Feedback\Siuf"; Name="DmClientOnScenarioDownload"},
@{Path="\Microsoft\Windows\Maps"; Name="MapsToastTask"},
@{Path="\Microsoft\Windows\Maps"; Name="MapsUpdateTask"},
@{Path="\Microsoft\Windows\Windows Error Reporting"; Name="QueueReporting"}
)
foreach ($t in $tasks) {
Disable-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction SilentlyContinue | Out-Null
}

View File

@@ -0,0 +1,32 @@
#Requires -RunAsAdministrator
# Services that consume resources without adding value in a minimal VM
$disable = @{
"DiagTrack" = "Connected User Experiences and Telemetry"
"dmwappushservice" = "WAP Push Message Routing"
"SysMain" = "Superfetch — causes high I/O in VMs"
"WSearch" = "Windows Search indexer — CPU/disk heavy"
"MapsBroker" = "Downloaded Maps Manager"
"lfsvc" = "Geolocation Service"
"XblAuthManager" = "Xbox Live Auth Manager"
"XblGameSave" = "Xbox Live Game Save"
"XboxGipSvc" = "Xbox Accessory Management"
"XboxNetApiSvc" = "Xbox Live Networking"
"RetailDemo" = "Retail Demo"
"Fax" = "Fax"
"WbioSrvc" = "Windows Biometric (no fingerprint reader in VM)"
"WMPNetworkSvc" = "Windows Media Player Network Sharing"
"icssvc" = "Mobile Hotspot"
"SharedAccess" = "Internet Connection Sharing"
"wisvc" = "Windows Insider Service"
"PcaSvc" = "Program Compatibility Assistant"
"wercplsupport" = "Problem Reports control panel support"
}
foreach ($name in $disable.Keys) {
$svc = Get-Service -Name $name -ErrorAction SilentlyContinue
if ($svc) {
Stop-Service -Name $name -Force -ErrorAction SilentlyContinue
Set-Service -Name $name -StartupType Disabled -ErrorAction SilentlyContinue
}
}

View File

@@ -0,0 +1,43 @@
#Requires -RunAsAdministrator
# ── Power plan: High Performance ────────────────────────────────────────────
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
# ── Hibernate: off (saves ~RAM-size on disk) ─────────────────────────────────
powercfg /hibernate off
# ── System Restore: off (saves disk and shadow-copy overhead) ────────────────
Disable-ComputerRestore -Drive "C:\" -ErrorAction SilentlyContinue
vssadmin delete shadows /for=C: /all /quiet 2>$null
# ── Visual effects: best performance ─────────────────────────────────────────
$ve = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects"
New-Item -Path $ve -Force -ErrorAction SilentlyContinue | Out-Null
Set-ItemProperty -Path $ve -Name VisualFXSetting -Value 2 -Type DWord
$desktop = "HKCU:\Control Panel\Desktop"
Set-ItemProperty -Path $desktop -Name DragFullWindows -Value "0"
Set-ItemProperty -Path $desktop -Name MenuShowDelay -Value "0"
$adv = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
Set-ItemProperty -Path $adv -Name TaskbarAnimations -Value 0 -Type DWord
Set-ItemProperty -Path $adv -Name DisallowShaking -Value 1 -Type DWord
# ── Page file: fixed size to avoid resize churn ──────────────────────────────
$cs = Get-WmiObject Win32_ComputerSystem -EnableAllPrivileges
$cs.AutomaticManagedPagefile = $false
$cs.Put() | Out-Null
$pf = Get-WmiObject -Query "Select * From Win32_PageFileSetting Where Name='C:\\pagefile.sys'"
if ($pf) {
$pf.InitialSize = 2048
$pf.MaximumSize = 4096
$pf.Put() | Out-Null
}
# ── NTFS: skip Last Access Time updates ──────────────────────────────────────
fsutil behavior set disablelastaccess 1 | Out-Null
# ── Processor scheduling: favor background services (better for VM workloads) ─
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\PriorityControl" `
-Name Win32PrioritySeparation -Value 24 -Type DWord

31
scripts/05-cleanup.ps1 Normal file
View File

@@ -0,0 +1,31 @@
#Requires -RunAsAdministrator
# Windows Update cache
Stop-Service wuauserv -Force -ErrorAction SilentlyContinue
Remove-Item "C:\Windows\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinue
Start-Service wuauserv -ErrorAction SilentlyContinue
# Temp folders
Remove-Item "$env:TEMP\*" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "C:\Windows\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue
# Windows Error Reporting archives
Remove-Item "C:\ProgramData\Microsoft\Windows\WER\*" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "C:\Windows\Minidump\*" -Force -ErrorAction SilentlyContinue
Remove-Item "C:\Windows\MEMORY.DMP" -Force -ErrorAction SilentlyContinue
# Prefetch (small, but consistent)
Remove-Item "C:\Windows\Prefetch\*" -Force -ErrorAction SilentlyContinue
# Event logs
Get-EventLog -LogName * -ErrorAction SilentlyContinue | ForEach-Object {
Clear-EventLog -LogName $_.Log -ErrorAction SilentlyContinue
}
# DISM: remove superseded Windows component store entries
DISM /Online /Cleanup-Image /StartComponentCleanup /ResetBase
# Compact OS: reduces on-disk footprint using XPRESS4K compression
Compact.exe /CompactOS:Always
Write-Output "Cleanup complete."

View File

@@ -0,0 +1,39 @@
# Copy this file to variables.pkrvars.hcl and fill in the required values.
# Do NOT commit variables.pkrvars.hcl — it contains credentials.
# ── Required ─────────────────────────────────────────────────────────────────
# Absolute path to your Windows 11 Pro retail ISO
win_iso_path = "/path/to/Win11_23H2_English_x64v2.iso"
# SHA-256 checksum — generate with: sha256sum /path/to/Win11_*.iso
win_iso_checksum = "sha256:REPLACE_WITH_ACTUAL_CHECKSUM"
# ── Optional overrides (uncomment to change) ──────────────────────────────────
# Path to VirtIO drivers ISO (downloaded by 'make virtio-iso')
# virtio_iso_path = "virtio-win.iso"
# Output VM name
# vm_name = "windows11-lite"
# Virtual disk size in MB (50 GB minimum recommended for Windows 11 + debloat)
# disk_size = 51200
# RAM in MB assigned to the VM during the Packer build
# Increase if the build stalls; this does not affect the final VM's default RAM.
# memory = 4096
# vCPUs during build
# cpus = 2
# Output directory for the finished QCOW2 disk image
# output_directory = "output"
# WinRM password used by Packer to connect during provisioning.
# If changed here, update the matching <Value> fields in http/Autounattend.xml.
# winrm_password = "Packer1234!"
# OVMF firmware paths — these are the Fedora 43 defaults
# ovmf_code = "/usr/share/edk2/ovmf/OVMF_CODE.fd"
# ovmf_vars = "/usr/share/edk2/ovmf/OVMF_VARS.fd"

139
windows11.pkr.hcl Normal file
View File

@@ -0,0 +1,139 @@
packer {
required_plugins {
qemu = {
version = ">= 1.1.0"
source = "github.com/hashicorp/qemu"
}
}
}
variable "win_iso_path" {
type = string
description = "Absolute path to Windows 11 Pro ISO"
}
variable "win_iso_checksum" {
type = string
description = "SHA-256 checksum prefixed with 'sha256:'"
}
variable "virtio_iso_path" {
type = string
default = "virtio-win.iso"
description = "Path to VirtIO drivers ISO (downloaded by 'make virtio-iso')"
}
variable "swtpm_sock" {
type = string
description = "Unix socket path for swtpm TPM emulator (managed by build.sh)"
}
variable "vm_name" {
type = string
default = "windows11-lite"
}
variable "disk_size" {
type = number
default = 51200
description = "Virtual disk size in MB (default 50 GB)"
}
variable "memory" {
type = number
default = 4096
description = "RAM in MB allocated during build"
}
variable "cpus" {
type = number
default = 2
}
variable "output_directory" {
type = string
default = "output"
}
variable "winrm_password" {
type = string
default = "Packer1234!"
sensitive = true
description = "WinRM password used during build; must match Autounattend.xml"
}
variable "ovmf_code" {
type = string
default = "/usr/share/edk2/ovmf/OVMF_CODE.fd"
}
variable "ovmf_vars" {
type = string
default = "/usr/share/edk2/ovmf/OVMF_VARS.fd"
}
source "qemu" "windows11" {
vm_name = var.vm_name
iso_url = "file://${var.win_iso_path}"
iso_checksum = var.win_iso_checksum
output_directory = var.output_directory
disk_size = var.disk_size
memory = var.memory
cpus = var.cpus
machine_type = "q35"
efi_firmware_code = var.ovmf_code
efi_firmware_vars = var.ovmf_vars
# VirtIO for minimal overhead; network also VirtIO
disk_interface = "virtio-scsi"
net_device = "virtio-net"
disk_cache = "unsafe"
disk_compression = true # deflate QCOW2 at build time; ~20-30% smaller image
qemuargs = [
# Software TPM 2.0 via swtpm
["-chardev", "socket,id=chrtpm,path=${var.swtpm_sock}"],
["-tpmdev", "emulator,id=tpm0,chardev=chrtpm"],
["-device", "tpm-tis,tpmdev=tpm0"],
# VirtIO drivers ISO (drive letter assigned dynamically; Autounattend searches D-G)
["-drive", "file=${var.virtio_iso_path},media=cdrom,readonly=on,if=ide"],
# Absolute-position mouse so installer clicks land correctly without X11
["-device", "usb-tablet"],
]
# Autounattend.xml served on a small virtual CD; Windows Setup finds it automatically
cd_files = ["./http/Autounattend.xml"]
cd_label = "AUTOUNATTEND"
communicator = "winrm"
winrm_username = "Administrator"
winrm_password = var.winrm_password
winrm_timeout = "3h"
winrm_insecure = true
winrm_use_ssl = false
headless = true
boot_wait = "5s"
boot_command = ["<spacebar>"]
shutdown_command = "shutdown /s /t 10 /f /d p:4:1 /c \"Packer Shutdown\""
shutdown_timeout = "15m"
}
build {
name = "windows11-lite"
sources = ["source.qemu.windows11"]
provisioner "powershell" {
elevated_user = "Administrator"
elevated_password = var.winrm_password
scripts = [
"./scripts/01-debloat-apps.ps1",
"./scripts/02-disable-telemetry.ps1",
"./scripts/03-disable-services.ps1",
"./scripts/04-performance.ps1",
"./scripts/05-cleanup.ps1",
]
}
}