From e4741ccc960622bad87b369ee1b327ca9c667d12 Mon Sep 17 00:00:00 2001 From: "jacob.b.nelson2" Date: Sun, 17 May 2026 19:31:18 -0500 Subject: [PATCH] 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 --- .gitignore | 20 +++ Makefile | 48 ++++++ README.md | 157 +++++++++++++++++ build.sh | 51 ++++++ http/Autounattend.xml | 286 +++++++++++++++++++++++++++++++ scripts/01-debloat-apps.ps1 | 71 ++++++++ scripts/02-disable-telemetry.ps1 | 63 +++++++ scripts/03-disable-services.ps1 | 32 ++++ scripts/04-performance.ps1 | 43 +++++ scripts/05-cleanup.ps1 | 31 ++++ variables.pkrvars.hcl.example | 39 +++++ windows11.pkr.hcl | 139 +++++++++++++++ 12 files changed, 980 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100755 build.sh create mode 100644 http/Autounattend.xml create mode 100644 scripts/01-debloat-apps.ps1 create mode 100644 scripts/02-disable-telemetry.ps1 create mode 100644 scripts/03-disable-services.ps1 create mode 100644 scripts/04-performance.ps1 create mode 100644 scripts/05-cleanup.ps1 create mode 100644 variables.pkrvars.hcl.example create mode 100644 windows11.pkr.hcl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a806590 --- /dev/null +++ b/.gitignore @@ -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 +*~ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1218b2d --- /dev/null +++ b/Makefile @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..70e26df --- /dev/null +++ b/README.md @@ -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 (~12–15 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:" +``` + +```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:` 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 | diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..bed5940 --- /dev/null +++ b/build.sh @@ -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}" \ + . diff --git a/http/Autounattend.xml b/http/Autounattend.xml new file mode 100644 index 0000000..82a8b00 --- /dev/null +++ b/http/Autounattend.xml @@ -0,0 +1,286 @@ + + + + + + + + + en-US + + en-US + en-US + en-US + en-US + + + + + + + D:\amd64\w11 + + + E:\amd64\w11 + + + F:\amd64\w11 + + + G:\amd64\w11 + + + + + + + + + + 1 + cmd.exe /c reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassSecureBootCheck /t REG_DWORD /d 1 /f + + + 2 + cmd.exe /c reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f + + + + true + + + + + + + /IMAGE/NAME + Windows 11 Pro + + + + 0 + 3 + + OnError + + + + + + true + Administrator + Lab + + W269N-WFGWX-YVC9B-4J6C9-T83GX + OnError + + + + + + + 0 + true + + + 1 + EFI + 100 + + + 2 + MSR + 16 + + + 3 + Primary + true + + + + + 1 + 1 + + FAT32 + + + 2 + 2 + + + 3 + 3 + + NTFS + C + + + + + + + + + + + + + WINDOWS11-LITE + UTC + + + + true + + + + 0 + + + + + + + + + en-US + en-US + en-US + en-US + en-US + + + + + + true + true + true + true + Work + 3 + true + true + + + + + + Packer1234! + 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> diff --git a/scripts/01-debloat-apps.ps1 b/scripts/01-debloat-apps.ps1 new file mode 100644 index 0000000..dbb4847 --- /dev/null +++ b/scripts/01-debloat-apps.ps1 @@ -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 diff --git a/scripts/02-disable-telemetry.ps1 b/scripts/02-disable-telemetry.ps1 new file mode 100644 index 0000000..05d0d45 --- /dev/null +++ b/scripts/02-disable-telemetry.ps1 @@ -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 +} diff --git a/scripts/03-disable-services.ps1 b/scripts/03-disable-services.ps1 new file mode 100644 index 0000000..0a4d3ef --- /dev/null +++ b/scripts/03-disable-services.ps1 @@ -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 + } +} diff --git a/scripts/04-performance.ps1 b/scripts/04-performance.ps1 new file mode 100644 index 0000000..bb7f55e --- /dev/null +++ b/scripts/04-performance.ps1 @@ -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 diff --git a/scripts/05-cleanup.ps1 b/scripts/05-cleanup.ps1 new file mode 100644 index 0000000..a9b0b5b --- /dev/null +++ b/scripts/05-cleanup.ps1 @@ -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." diff --git a/variables.pkrvars.hcl.example b/variables.pkrvars.hcl.example new file mode 100644 index 0000000..cd2aeab --- /dev/null +++ b/variables.pkrvars.hcl.example @@ -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" diff --git a/windows11.pkr.hcl b/windows11.pkr.hcl new file mode 100644 index 0000000..ebba5d9 --- /dev/null +++ b/windows11.pkr.hcl @@ -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", + ] + } +}