Compare commits

...

9 Commits

Author SHA1 Message Date
Chris Nutter f208e07d72 Fix first-logon provisioning never starting
- Bound every installer wait with WaitForExit and a timeout instead of
  Start-Process -Wait, which in Windows PowerShell also waits for any
  process the installer leaves running and can block forever. This
  covers the VirtIO step in the answer files and the guest tools, Mesh
  agent and Winget installs in the scripts.
- specialize pass (SYSTEM) sets ConsentPromptBehaviorAdmin=0 so nothing
  at first logon can stall on a UAC prompt.
- Step 4 now logs to C:\ProgramData\OOBE\firstlogon.log, saves the
  script to C:\ProgramData\OOBE and starts it elevated in its own window.
- Scripts start their transcript before the admin check, so an early
  stop still leaves a log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 22:01:11 -07:00
Chris Nutter 713463cd94 Rename default account to silviodante and set owner/organization
Local admin is now silviodante (Silvio Dante), password unchanged.
Registered owner is Silvio Dante and organization is Bada Bing! in both
answer files and the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 21:26:58 -07:00
Chris Nutter 6140dd2090 Rewrite README for the VM and desktop variants
Replace the dead /oobe link with /oobe-vms and /oobe-desktop, and
document what each script does, how to build the ISOs, how to install,
and the placeholder account.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 21:18:52 -07:00
Chris Nutter c4d42aa54c separated church and state 2026-09-21 21:12:41 -07:00
Chris Nutter b9a8ded252 Reorganize project: move answer file to unattend/, drop unused scripts
- Move unattend-vms-WIPE.xml to unattend/vms-WIPE-DISK.xml; it now runs
  oobe-ltsc.ps1 as its final first-logon command
- Remove MeshAgent.ps1 (folded into oobe-ltsc.ps1) and WinRE.ps1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 10:38:29 -07:00
Chris Nutter 59cb60ffb1 added autounattend 2026-09-17 10:23:53 -07:00
Chris Nutter ace85fb174 Split Script.ps1 into oobe-rm-ai.ps1 and unattended oobe-ltsc.ps1
oobe-ltsc.ps1 targets LTSC VMs deployed via autounattend.xml: installs
VirtIO guest tools from the mounted ISO, silently installs the Mesh
agent, disables sleep/hibernate, enables RDP/SSH/WinRM, and restarts
when finished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 10:15:54 -07:00
Chris Nutter 9937f40e16 Add MeshAgent.ps1 2026-05-02 14:44:10 -07:00
Chris Nutter 8c016564a5 Update Script.ps1 2025-11-30 14:16:44 -08:00
9 changed files with 1233 additions and 804 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build/

View File

@ -1,5 +1,66 @@
# Windows OOBE Script
# Windows OOBE Scripts
First thing to run when logged into windows for the first time.
Unattended setup for Windows 10 Enterprise LTSC 2021: an answer file installs Windows, then a PowerShell script configures the machine on first logon and restarts it.
`irm https://url.isworking.fyi/oobe | iex`
| Variant | Answer file | OOBE script | Disk | ISO |
|---|---|---|---|---|
| VMs (Unraid/KVM, VirtIO) | `unattend/vms-WIPE-DISK.xml` | `oobe-vms.ps1` | **Wipes Disk 0 without asking** | `build/virtio-vms.iso` |
| Desktops, laptops, any VM | `unattend/desktop-PROMPT-DISK.xml` | `oobe-desktop.ps1` | Prompts you to pick the disk/partition | `build/desktop.iso` |
## Run a script by hand
From an elevated PowerShell:
```powershell
irm https://url.isworking.fyi/oobe-vms | iex
irm https://url.isworking.fyi/oobe-desktop | iex
```
The short links point at the raw files on `master`, so pushing a change updates what new installs run. Logs go to `C:\ProgramData\OOBE\oobe-<name>.log`.
## What the scripts do
Both:
- Install the MeshCentral agent silently as a service
- Enable RDP, ping, the `C$` admin share, and OpenSSH Server (PowerShell as the default shell)
- Set the network to Private, turn off the UAC prompt, and turn on dark mode with no mouse acceleration (current user and default profile)
- Install Winget
- Restart when finished
`oobe-vms.ps1` also:
- Installs the VirtIO drivers, QEMU guest agent and SPICE agent from the mounted virtio-win ISO (downloads them if it isn't mounted)
- Never sleeps or hibernates (enforced by policy), never turns off the display or disks, and stops Windows powering down the network adapter
- Enables WinRM and keeps the clock synced
`oobe-desktop.ps1` also:
- Uses the Balanced plan: dims the display after 5 minutes (only works on screens Windows controls the brightness of) and sleeps after 30 minutes
## Build the ISOs
Requires `curl` and `xorriso`:
```sh
./build-isos.sh
```
This downloads the latest virtio-win ISO (only when a new release is out) and writes to `build/`, which git ignores:
- `virtio-vms.iso`: virtio-win plus `autounattend.xml` (the VM answer file) and `oobe-vms.ps1`
- `desktop.iso`: `autounattend.xml` (the desktop answer file) and `oobe-desktop.ps1`
Rerun it after changing an answer file. Changes to the scripts reach new installs through the short links without a rebuild. The copy on the ISO is only used if the download fails.
## Install
**VM (Unraid):** put the LTSC ISO in "OS Install ISO" and `virtio-vms.iso` in "VirtIO Drivers ISO", then boot. Everything is automatic, and **Disk 0 is erased**.
**Desktop:** copy `autounattend.xml` and `oobe-desktop.ps1` from `desktop.iso` to the root of the LTSC install USB. For a VM, attach `desktop.iso` as a second CD instead. Setup stops once to ask where to install; everything else is automatic.
Setup only reads a file named exactly `autounattend.xml` at the root of a disc or USB drive.
## Accounts
Both answer files create a local administrator `silviodante` with the placeholder password `mdwelcome`, stored in plain text. VMs log in automatically every boot; desktops only on the first logon, then show the sign-in screen. Change the password in the answer file before using this for anything that matters.

View File

@ -1,66 +0,0 @@
# Check if running with elevated privileges
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Host "Please run this script as an administrator."
Exit
}
# Enable ping (ICMP Echo) requests
New-NetFirewallRule -DisplayName "Allow ICMP Echo Request" -Protocol ICMPv4 -IcmpType 8 -Enabled True
# Turn on Dark Mode
New-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize -Name AppsUseLightTheme -Value 0 -PropertyType DWORD -Force
# Disable Mouse Acceleration
Set-ItemProperty -Path "HKCU:\Control Panel\Mouse" -Name MouseSensitivity -Value 0
# Allow Remote Desktop
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
# Enable c$
reg add "HKLM\SYSTEM\CurrentControlSet\Services\lanmanserver\parameters" /f /v AutoShareWks /t REG_DWORD /d 0
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v "LocalAccountTokenFilterPolicy" /t REG_DWORD /d 1 /f
# Disable UAC prompt
Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\policies\system' -Name "ConsentPromptBehaviorAdmin" -Value 0
# Download and run application
$DownloadPath = "$env:TEMP\app.exe"
Invoke-WebRequest -Uri "https://rmm.iamchrisama.com/meshagents?id=4&meshid=zxl@U2zM95zh9ZNqah@9mUEjCJ3ptGOE6s5cjsGniacVU1fjRXtVKCKlKJN4aJQW&installflags=0" -OutFile $DownloadPath
Start-Process -FilePath $DownloadPath -Wait
# Download and install ScreenConnect client
$ScreenConnectUrl = "https://iamchrisama.screenconnect.com/Bin/ScreenConnect.ClientSetup.exe?e=Access&y=Guest"
$ScreenConnectPath = "$env:TEMP\ScreenConnect.ClientSetup.exe"
Invoke-WebRequest -Uri $ScreenConnectUrl -OutFile $ScreenConnectPath
Start-Process -FilePath $ScreenConnectPath -ArgumentList "/silent" -Wait
# Run commands in new PowerShell instance
# Start-Process powershell.exe -ArgumentList "-NoProfile -Command {irm https://massgrave.dev/get | iex}"
# Start-Process powershell.exe -ArgumentList "-NoProfile -Command {irm https://christitus.com/win | iex}"
# Disable sleep and enable high performance mode, enable hibernation, and display black after 30 minutes
powercfg -change -standby-timeout-ac 0
powercfg -setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
powercfg /hibernate on
powercfg -change -monitor-timeout-ac 30
# Install Winget
#Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
irm https://github.com/asheroto/winget-install/releases/latest/download/winget-install.ps1 | iex
# Install OpenSSH
Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH*'
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'
if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {
Write-Output "Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it..."
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
} else {
Write-Output "Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists."
}

735
WinRE.ps1
View File

@ -1,735 +0,0 @@
################################################################################################
#
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
################################################################################################
Param (
[Parameter(HelpMessage="Work Directory for patch WinRE")][string]$workDir="",
[Parameter(Mandatory=$true,HelpMessage="Path of target package")][string]$packagePath
)
# ------------------------------------
# Help functions
# ------------------------------------
# Log message
function LogMessage([string]$message)
{
$message = "$([DateTime]::Now) - $message"
Write-Host $message
}
function IsTPMBasedProtector
{
$DriveLetter = $env:SystemDrive
LogMessage("Checking BitLocker status")
$BitLocker = Get-WmiObject -Namespace "Root\cimv2\Security\MicrosoftVolumeEncryption" -Class "Win32_EncryptableVolume" -Filter "DriveLetter = '$DriveLetter'"
if(-not $BitLocker)
{
LogMessage("No BitLocker object")
return $False
}
$protectionEnabled = $False
switch ($BitLocker.GetProtectionStatus().protectionStatus){
("0"){
LogMessage("Unprotected")
break
}
("1"){
LogMessage("Protected")
$protectionEnabled = $True
break
}
("2"){
LogMessage("Uknown")
break
}
default{
LogMessage("NoReturn")
break
}
}
if (!$protectionEnabled)
{
LogMessage("Bitlocker isn't enabled on the OS")
return $False
}
$ProtectorIds = $BitLocker.GetKeyProtectors("0").volumekeyprotectorID
$return = $False
foreach ($ProtectorID in $ProtectorIds){
$KeyProtectorType = $BitLocker.GetKeyProtectorType($ProtectorID).KeyProtectorType
switch($KeyProtectorType){
"1"{
LogMessage("Trusted Platform Module (TPM)")
$return = $True
break
}
"4"{
LogMessage("TPM And PIN")
$return = $True
break
}
"5"{
LogMessage("TPM And Startup Key")
$return = $True
break
}
"6"{
LogMessage("TPM And PIN And Startup Key")
$return = $True
break
}
default {break}
}#endSwitch
}#EndForeach
if ($return)
{
LogMessage("Has TPM-based protector")
}
else
{
LogMessage("Doesn't have TPM-based protector")
}
return $return
}
function SetRegistrykeyForSuccess
{
reg add HKLM\SOFTWARE\Microsoft\PushButtonReset /v WinREPathScriptSucceed /d 1 /f
}
function TargetfileVersionExam([string]$mountDir)
{
# Exam target binary
$targetBinary=$mountDir + "\Windows\System32\bootmenuux.dll"
LogMessage("TargetFile: " + $targetBinary)
$realNTVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($targetBinary).ProductVersion
$versionString = "$($realNTVersion.Split('.')[0]).$($realNTVersion.Split('.')[1])"
$fileVersion = $($realNTVersion.Split('.')[2])
$fileRevision = $($realNTVersion.Split('.')[3])
LogMessage("Target file version: " + $realNTVersion)
if (!($versionString -eq "10.0"))
{
LogMessage("Not Windows 10 or later")
return $False
}
$hasUpdated = $False
#Windows 10, version 1507 10240.19567
#Windows 10, version 1607 14393.5499
#Windows 10, version 1809 17763.3646
#Windows 10, version 2004 1904X.2247
#Windows 11, version 21H2 22000.1215
#Windows 11, version 22H2 22621.815
switch ($fileVersion) {
"10240" {
LogMessage("Windows 10, version 1507")
if ($fileRevision -ge 19567)
{
LogMessage("Windows 10, version 1507 with revision " + $fileRevision + " >= 19567, updates have been applied")
$hasUpdated = $True
}
break
}
"14393" {
LogMessage("Windows 10, version 1607")
if ($fileRevision -ge 5499)
{
LogMessage("Windows 10, version 1607 with revision " + $fileRevision + " >= 5499, updates have been applied")
$hasUpdated = $True
}
break
}
"17763" {
LogMessage("Windows 10, version 1809")
if ($fileRevision -ge 3646)
{
LogMessage("Windows 10, version 1809 with revision " + $fileRevision + " >= 3646, updates have been applied")
$hasUpdated = $True
}
break
}
"19041" {
LogMessage("Windows 10, version 2004")
if ($fileRevision -ge 2247)
{
LogMessage("Windows 10, version 2004 with revision " + $fileRevision + " >= 2247, updates have been applied")
$hasUpdated = $True
}
break
}
"22000" {
LogMessage("Windows 11, version 21H2")
if ($fileRevision -ge 1215)
{
LogMessage("Windows 11, version 21H2 with revision " + $fileRevision + " >= 1215, updates have been applied")
$hasUpdated = $True
}
break
}
"22621" {
LogMessage("Windows 11, version 22H2")
if ($fileRevision -ge 815)
{
LogMessage("Windows 11, version 22H2 with revision " + $fileRevision + " >= 815, updates have been applied")
$hasUpdated = $True
}
break
}
default {
LogMessage("Warning: unsupported OS version")
}
}
return $hasUpdated
}
function PatchPackage([string]$mountDir, [string]$packagePath)
{
# Exam target binary
$hasUpdated = TargetfileVersionExam($mountDir)
if ($hasUpdated)
{
LogMessage("The update has already been added to WinRE")
SetRegistrykeyForSuccess
return $False
}
# Add package
LogMessage("Apply package:" + $packagePath)
Dism /Add-Package /Image:$mountDir /PackagePath:$packagePath
if ($LASTEXITCODE -eq 0)
{
LogMessage("Successfully applied the package")
}
else
{
LogMessage("Applying the package failed with exit code: " + $LASTEXITCODE)
return $False
}
# Cleanup recovery image
LogMessage("Cleanup image")
Dism /image:$mountDir /cleanup-image /StartComponentCleanup /ResetBase
if ($LASTEXITCODE -eq 0)
{
LogMessage("Cleanup image succeed")
}
else
{
LogMessage("Cleanup image failed: " + $LASTEXITCODE)
return $False
}
return $True
}
# ------------------------------------
# Execution starts
# ------------------------------------
# Check breadcrumb
if (Test-Path HKLM:\Software\Microsoft\PushButtonReset)
{
$values = Get-ItemProperty -Path HKLM:\Software\Microsoft\PushButtonReset
if (!(-not $values))
{
if (Get-Member -InputObject $values -Name WinREPathScriptSucceed)
{
$value = Get-ItemProperty -Path HKLM:\Software\Microsoft\PushButtonReset -Name WinREPathScriptSucceed
if ($value.WinREPathScriptSucceed -eq 1)
{
LogMessage("This script was previously run successfully")
exit 1
}
}
}
}
# Get WinRE info
$WinREInfo = Reagentc /info
$findLocation = $False
foreach ($line in $WinREInfo)
{
$params = $line.Split(':')
if ($params.count -le 1)
{
continue
}
if ($params[1].Lenght -eq 0)
{
continue
}
$content = $params[1].Trim()
if ($content.Lenght -eq 0)
{
continue
}
$index = $content.IndexOf("\\?\")
if ($index -ge 0)
{
LogMessage("Find \\?\ at " + $index + " for [" + $content + "]")
$WinRELocation = $content
$findLocation = $True
}
}
if (!$findLocation)
{
LogMessage("WinRE Disabled")
exit 1
}
LogMessage("WinRE Enabled. WinRE location:" + $WinRELocation)
$WinREFile = $WinRELocation + "\winre.wim"
if ([string]::IsNullorEmpty($workDir))
{
LogMessage("No input for mount directory")
LogMessage("Use default path from temporary directory")
$workDir = [System.IO.Path]::GetTempPath()
}
LogMessage("Working Dir: " + $workDir)
$name = "CA551926-299B-27A55276EC22_Mount"
$mountDir = Join-Path $workDir $name
LogMessage("MountDir: " + $mountdir)
# Delete existing mount directory
if (Test-Path $mountDir)
{
LogMessage("Mount directory: " + $mountDir + " already exists")
LogMessage("Try to unmount it")
Dism /unmount-image /mountDir:$mountDir /discard
if (!($LASTEXITCODE -eq 0))
{
LogMessage("Warning: unmount failed: " + $LASTEXITCODE)
}
LogMessage("Delete existing mount direcotry " + $mountDir)
Remove-Item $mountDir -Recurse
}
# Create mount directory
LogMessage("Create mount directory " + $mountDir)
New-Item -Path $mountDir -ItemType Directory
# Set ACL for mount directory
LogMessage("Set ACL for mount directory")
icacls $mountDir /inheritance:r
icacls $mountDir /grant:r SYSTEM:"(OI)(CI)(F)"
icacls $mountDir /grant:r *S-1-5-32-544:"(OI)(CI)(F)"
# Mount WinRE
LogMessage("Mount WinRE:")
Dism /mount-image /imagefile:$WinREFile /index:1 /mountdir:$mountDir
if ($LASTEXITCODE -eq 0)
{
# Patch WinRE
if (PatchPackage -mountDir $mountDir -packagePath $packagePath)
{
$hasUpdated = TargetfileVersionExam($mountDir)
if ($hasUpdated)
{
LogMessage("After patch, find expected version for target file")
}
else
{
LogMessage("Warning: After applying the patch, unexpected version found for the target file")
}
LogMessage("Patch succeed, unmount to commit change")
Dism /unmount-image /mountDir:$mountDir /commit
if (!($LASTEXITCODE -eq 0))
{
LogMessage("Unmount failed: " + $LASTEXITCODE)
exit 1
}
else
{
if ($hasUpdated)
{
if (IsTPMBasedProtector)
{
# Disable WinRE and re-enable it to let new WinRE be trusted by BitLocker
LogMessage("Disable WinRE")
reagentc /disable
LogMessage("Re-enable WinRE")
reagentc /enable
reagentc /info
}
# Leave a breadcrumb indicates the script has succeed
SetRegistrykeyForSuccess
}
}
}
else
{
LogMessage("Patch failed or is not applicable, discard unmount")
Dism /unmount-image /mountDir:$mountDir /discard
if (!($LASTEXITCODE -eq 0))
{
LogMessage("Unmount failed: " + $LASTEXITCODE)
exit 1
}
}
}
else
{
LogMessage("Mount failed: " + $LASTEXITCODE)
}
# Cleanup Mount directory in the end
LogMessage("Delete mount direcotry")
Remove-Item $mountDir -Recurse

53
build-isos.sh Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Builds the answer-file ISOs into build/:
# virtio-vms.iso latest virtio-win drivers + unattend/vms-WIPE-DISK.xml (as autounattend.xml) + oobe-vms.ps1
# desktop.iso unattend/desktop-PROMPT-DISK.xml (as autounattend.xml) + oobe-desktop.ps1
# The virtio-win ISO is only downloaded again when a new release is published.
# Requires: curl, xorriso
set -euo pipefail
cd "$(dirname "$0")"
VIRTIO_LATEST=https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio/virtio-win.iso
BUILD=build
STAGE=$BUILD/stage
# Joliet names so Windows sees oobe-*.ps1 and autounattend.xml unmangled (volume labels max 16 chars)
JOLIET=(-joliet on -compliance joliet_long_names)
# Root of the ISO as Windows reads it (Joliet, not Rock Ridge)
show_root() {
echo "== $1"
xorriso -report_about WARNING -read_fs norock -indev "$1" -lsl / 2>/dev/null | grep -E -i 'autounattend|oobe-|guest-tools' | awk '{print " ", $NF}'
}
mkdir -p "$BUILD"
# Resolve "latest" to the versioned file; Fedora's redirects drop to plain http, so force https
VIRTIO_URL=$(curl -fsSIL -o /dev/null -w '%{url_effective}' "$VIRTIO_LATEST" | sed 's#^http://#https://#')
VIRTIO_ISO=$BUILD/$(basename "$VIRTIO_URL")
if [[ -f $VIRTIO_ISO ]]; then
echo "Using cached $VIRTIO_ISO"
else
echo "Downloading $VIRTIO_URL"
curl -fL --retry 3 -o "$VIRTIO_ISO.part" "$VIRTIO_URL"
mv "$VIRTIO_ISO.part" "$VIRTIO_ISO"
fi
# a. VMs: virtio-win + VM answer file + oobe-vms.ps1
# Files are added to a copy of the original image rather than extracting and repacking it:
# virtio-win stores identical drivers once, and repacking would nearly double its size.
rm -f "$BUILD/virtio-vms.iso"
xorriso -report_about WARNING -indev "$VIRTIO_ISO" -outdev "$BUILD/virtio-vms.iso" "${JOLIET[@]}" -volid VIRTIO_VMS \
-map unattend/vms-WIPE-DISK.xml /autounattend.xml \
-map oobe-vms.ps1 /oobe-vms.ps1 \
-commit
# b. Desktops: desktop answer file + oobe-desktop.ps1, no virtio
rm -rf "$STAGE" "$BUILD/desktop.iso"
mkdir -p "$STAGE"
cp unattend/desktop-PROMPT-DISK.xml "$STAGE/autounattend.xml"
cp oobe-desktop.ps1 "$STAGE/"
xorriso -report_about WARNING -outdev "$BUILD/desktop.iso" "${JOLIET[@]}" -volid OOBE_DESKTOP -map "$STAGE" / -commit
rm -rf "$STAGE"
show_root "$BUILD/virtio-vms.iso"
show_root "$BUILD/desktop.iso"
ls -lh "$BUILD"/*.iso

164
oobe-desktop.ps1 Normal file
View File

@ -0,0 +1,164 @@
# OOBE for Windows LTSC desktops and laptops. Runs fully unattended (no prompts).
# Pulled by autounattend.xml at first logon, or run by hand:
# irm https://url.isworking.fyi/oobe-desktop | iex
# Log: C:\ProgramData\OOBE\oobe-desktop.log
$MeshAgentUrl = "https://rmm.iamchrisama.com/meshagents?id=4&meshid=zxl@U2zM95zh9ZNqah@9mUEjCJ3ptGOE6s5cjsGniacVU1fjRXtVKCKlKJN4aJQW&installflags=0"
$ErrorActionPreference = 'Continue'
# The progress bar makes Invoke-WebRequest extremely slow on Windows PowerShell 5.1
$ProgressPreference = 'SilentlyContinue'
# Older LTSC builds don't enable TLS 1.2 by default, which breaks GitHub downloads
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$WorkDir = Join-Path $env:ProgramData 'OOBE'
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
Start-Transcript -Path (Join-Path $WorkDir 'oobe-desktop.log') -Append | Out-Null
# Check if running with elevated privileges (after the transcript starts, so this still leaves a log)
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Warning "Not running as administrator, stopping. Rerun from an elevated PowerShell."
Stop-Transcript | Out-Null
Exit 1
}
function Save-Download($Uri, $OutFile) {
for ($Attempt = 1; $Attempt -le 3; $Attempt++) {
try {
Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing
return $true
} catch {
Write-Warning "Download attempt $Attempt of $Uri failed: $_"
Start-Sleep -Seconds 10
}
}
return $false
}
# Wait for internet access, the network may not be up yet during first logon
function Wait-Network {
$MeshHost = ([uri]$MeshAgentUrl).Host
$Deadline = (Get-Date).AddMinutes(5)
while ((Get-Date) -lt $Deadline) {
$Client = New-Object System.Net.Sockets.TcpClient
try { if ($Client.ConnectAsync($MeshHost, 443).Wait(5000)) { return } } catch {} finally { $Client.Close() }
Write-Output "Waiting for network..."
Start-Sleep -Seconds 5
}
Write-Warning "No network after 5 minutes, continuing anyway."
}
# Balanced plan: dim the display after 5 minutes, sleep after 30 minutes.
# Turning the display off stays at the plan default (10 minutes plugged in).
$Balanced = '381b4222-f694-41f0-9685-ff5bb260df2e'
powercfg /setactive $Balanced
# "Dim display after" (seconds). Only has a visible effect on screens Windows can
# control the brightness of (laptops, all-in-ones); external monitors ignore it.
$DisplaySubgroup = '7516b95f-f776-4464-8c53-06167f40cc99'
$DimTimeout = '17aaa29b-8b43-4b94-aafe-35f64daaf1ee'
powercfg /setacvalueindex $Balanced $DisplaySubgroup $DimTimeout 300
powercfg /setdcvalueindex $Balanced $DisplaySubgroup $DimTimeout 300
powercfg /change standby-timeout-ac 30
powercfg /change standby-timeout-dc 30
powercfg /setactive $Balanced
# Treat the network as Private and suppress the "allow this PC to be discoverable" prompt
Wait-Network
New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Network\NewNetworkWindowOff' -Force | Out-Null
Get-NetConnectionProfile | Where-Object NetworkCategory -ne 'DomainAuthenticated' | Set-NetConnectionProfile -NetworkCategory Private
# Enable ping (ICMP Echo) requests
if (-not (Get-NetFirewallRule -Name 'OOBE-ICMPv4-In' -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -Name 'OOBE-ICMPv4-In' -DisplayName "Allow ICMP Echo Request" -Protocol ICMPv4 -IcmpType 8 -Enabled True | Out-Null
}
# Allow Remote Desktop (firewall groups are referenced by resource ID so this works on non-English installs)
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0
Enable-NetFirewallRule -Group '@FirewallAPI.dll,-28752'
# Enable c$ (AutoShareWks must be 1; 0 disables the admin shares)
reg add "HKLM\SYSTEM\CurrentControlSet\Services\lanmanserver\parameters" /f /v AutoShareWks /t REG_DWORD /d 1
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v "LocalAccountTokenFilterPolicy" /t REG_DWORD /d 1 /f
Enable-NetFirewallRule -Group '@FirewallAPI.dll,-28502'
# Disable UAC prompt
Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\policies\system' -Name "ConsentPromptBehaviorAdmin" -Value 0
# Dark Mode and no mouse acceleration, for the current user and the default profile (future users)
function Set-UserPreferences($Hive) {
reg add "$Hive\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v AppsUseLightTheme /t REG_DWORD /d 0 /f | Out-Null
reg add "$Hive\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v SystemUsesLightTheme /t REG_DWORD /d 0 /f | Out-Null
# "Enhance pointer precision" off
reg add "$Hive\Control Panel\Mouse" /v MouseSpeed /t REG_SZ /d 0 /f | Out-Null
reg add "$Hive\Control Panel\Mouse" /v MouseThreshold1 /t REG_SZ /d 0 /f | Out-Null
reg add "$Hive\Control Panel\Mouse" /v MouseThreshold2 /t REG_SZ /d 0 /f | Out-Null
}
Set-UserPreferences 'HKCU'
$DefaultProfile = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList').Default
reg load 'HKU\OOBEDefault' "$DefaultProfile\NTUSER.DAT" | Out-Null
if ($LASTEXITCODE -eq 0) {
Set-UserPreferences 'HKU\OOBEDefault'
[gc]::Collect()
reg unload 'HKU\OOBEDefault' | Out-Null
}
# Install MeshCentral agent silently as a service
if (Get-Service -Name 'Mesh Agent' -ErrorAction SilentlyContinue) {
Write-Output "Mesh Agent is already installed."
} else {
$MeshInstaller = Join-Path $WorkDir 'meshagent.exe'
if (Save-Download $MeshAgentUrl $MeshInstaller) {
# -fullinstall installs and starts the service without showing the install dialog
$Process = Start-Process -FilePath $MeshInstaller -ArgumentList '-fullinstall' -WindowStyle Hidden -PassThru
if (-not $Process.WaitForExit(300000)) {
Write-Warning "Mesh Agent installer still running after 5 minutes, continuing."
}
}
if (-not (Get-Service -Name 'Mesh Agent' -ErrorAction SilentlyContinue)) {
Write-Warning "Mesh Agent service was not found after install."
}
}
# Install OpenSSH
foreach ($Capability in 'OpenSSH.Client~~~~0.0.1.0', 'OpenSSH.Server~~~~0.0.1.0') {
if ((Get-WindowsCapability -Online -Name $Capability).State -ne 'Installed') {
Add-WindowsCapability -Online -Name $Capability | Out-Null
}
}
Set-Service -Name sshd -StartupType 'Automatic'
Start-Service sshd
# Use PowerShell instead of cmd.exe for SSH sessions
if (Test-Path 'HKLM:\SOFTWARE\OpenSSH') {
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force | Out-Null
}
if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {
Write-Output "Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it..."
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
} else {
Write-Output "Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists."
}
# Install Winget (LTSC ships without the Store / App Installer)
# Run in a child process (the installer script calls exit, which would end this script too),
# with a time limit so it can't hold up the restart. Its output goes to winget-install*.log.
$WingetInstaller = Join-Path $WorkDir 'winget-install.ps1'
if (Save-Download 'https://github.com/asheroto/winget-install/releases/latest/download/winget-install.ps1' $WingetInstaller) {
$Process = Start-Process powershell.exe -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $WingetInstaller, '-Force' -PassThru `
-RedirectStandardOutput (Join-Path $WorkDir 'winget-install.log') -RedirectStandardError (Join-Path $WorkDir 'winget-install-errors.log')
if ($Process.WaitForExit(900000)) {
Write-Output "Winget installer exited with code $($Process.ExitCode)"
} else {
Write-Warning "Winget installer still running after 15 minutes, continuing."
}
}
# Restart to finish OpenSSH and driver installation.
# Delayed so the script can exit cleanly and the log is flushed before Windows goes down.
Write-Output "OOBE complete, restarting in 10 seconds."
Stop-Transcript | Out-Null
shutdown.exe /r /t 10 /d p:4:1 /c "OOBE complete, restarting to finish installation."

236
oobe-vms.ps1 Normal file
View File

@ -0,0 +1,236 @@
# OOBE for Windows LTSC virtual machines (Unraid / KVM with VirtIO). Runs fully unattended (no prompts).
# Pulled by autounattend.xml at first logon, or run by hand:
# irm https://url.isworking.fyi/oobe-vms | iex
# Log: C:\ProgramData\OOBE\oobe-vms.log
$MeshAgentUrl = "https://rmm.iamchrisama.com/meshagents?id=4&meshid=zxl@U2zM95zh9ZNqah@9mUEjCJ3ptGOE6s5cjsGniacVU1fjRXtVKCKlKJN4aJQW&installflags=0"
$ErrorActionPreference = 'Continue'
# The progress bar makes Invoke-WebRequest extremely slow on Windows PowerShell 5.1
$ProgressPreference = 'SilentlyContinue'
# Older LTSC builds don't enable TLS 1.2 by default, which breaks GitHub downloads
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$WorkDir = Join-Path $env:ProgramData 'OOBE'
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
Start-Transcript -Path (Join-Path $WorkDir 'oobe-vms.log') -Append | Out-Null
# Check if running with elevated privileges (after the transcript starts, so this still leaves a log)
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Warning "Not running as administrator, stopping. Rerun from an elevated PowerShell."
Stop-Transcript | Out-Null
Exit 1
}
function Save-Download($Uri, $OutFile) {
for ($Attempt = 1; $Attempt -le 3; $Attempt++) {
try {
Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing
return $true
} catch {
Write-Warning "Download attempt $Attempt of $Uri failed: $_"
Start-Sleep -Seconds 10
}
}
return $false
}
# Wait for internet access, the network may not be up yet during first logon
function Wait-Network {
$MeshHost = ([uri]$MeshAgentUrl).Host
$Deadline = (Get-Date).AddMinutes(5)
while ((Get-Date) -lt $Deadline) {
$Client = New-Object System.Net.Sockets.TcpClient
try { if ($Client.ConnectAsync($MeshHost, 443).Wait(5000)) { return } } catch {} finally { $Client.Close() }
Write-Output "Waiting for network..."
Start-Sleep -Seconds 5
}
Write-Warning "No network after 5 minutes, continuing anyway."
}
# Install VirtIO drivers, QEMU guest agent and SPICE agent from the mounted virtio-win ISO.
# This runs first because the network adapter may still need the NetKVM driver.
if (Get-Service -Name 'QEMU-GA' -ErrorAction SilentlyContinue) {
Write-Output "VirtIO guest tools are already installed."
} else {
$GuestTools = Get-PSDrive -PSProvider FileSystem | ForEach-Object { Join-Path $_.Root 'virtio-win-guest-tools.exe' } | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($GuestTools) {
Write-Output "Found VirtIO guest tools at $GuestTools"
} else {
Write-Output "virtio-win ISO not mounted, downloading guest tools..."
Wait-Network
$GuestTools = Join-Path $WorkDir 'virtio-win-guest-tools.exe'
if (-not (Save-Download 'https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio/virtio-win-guest-tools.exe' $GuestTools)) {
$GuestTools = $null
}
}
if ($GuestTools) {
# Trust the Red Hat publisher certificate (installer and driver catalogs) so driver installs can't raise a security prompt
$Catalogs = Get-ChildItem -Path (Split-Path $GuestTools) -Filter '*.cat' -Recurse -ErrorAction SilentlyContinue | ForEach-Object FullName
$Store = New-Object System.Security.Cryptography.X509Certificates.X509Store('TrustedPublisher', 'LocalMachine')
$Store.Open('ReadWrite')
@($GuestTools) + @($Catalogs) | ForEach-Object { (Get-AuthenticodeSignature $_).SignerCertificate } |
Where-Object { $_ -and $_.Subject -match 'Red Hat' } |
ForEach-Object { $Store.Add($_) }
$Store.Close()
# Wait on the installer only, with a time limit: Start-Process -Wait also waits for anything
# the installer leaves running (agents, services), which can block forever
$Process = Start-Process -FilePath $GuestTools -ArgumentList '/install', '/quiet', '/norestart' -PassThru
if ($Process.WaitForExit(900000)) {
Write-Output "VirtIO guest tools installer exited with code $($Process.ExitCode) (0 = success, 3010 = reboot required)"
} else {
Write-Warning "VirtIO guest tools installer still running after 15 minutes, continuing."
}
}
}
# Disable sleep and hibernation, use high performance mode, never turn off display or disks
$HighPerformance = '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c'
$Scheme = $HighPerformance
if (-not (powercfg /list | Select-String $HighPerformance)) {
# The plan is hidden on some builds; recreate it from the built-in template
if ("$(powercfg /duplicatescheme $HighPerformance)" -match '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}') { $Scheme = $Matches[0] }
}
powercfg /setactive $Scheme
foreach ($Timeout in 'standby-timeout', 'hibernate-timeout', 'monitor-timeout', 'disk-timeout') {
powercfg /change "$Timeout-ac" 0
powercfg /change "$Timeout-dc" 0
}
powercfg /hibernate off
# Hybrid sleep and the hidden "unattended sleep" timeout (can sleep a VM nobody is logged into)
$SleepSubgroup = '238c9fa8-0aad-41ed-83f4-97be242c8f20'
foreach ($Setting in '94ac6d29-73ce-41a6-809f-6363ba21b47e', '7bc4a2f9-d8fc-4469-b07b-33eb785aaca0') {
powercfg /setacvalueindex SCHEME_CURRENT $SleepSubgroup $Setting 0
powercfg /setdcvalueindex SCHEME_CURRENT $SleepSubgroup $Setting 0
}
powercfg /setactive SCHEME_CURRENT
# Enforce through policy so a plan change or feature update can't bring sleep back
$PowerPolicies = @(
'29f6c1db-86da-48c5-9fdb-f2b67b1f44da' # Sleep timeout = never
'9d7815a6-7ee4-497e-8888-515a05f02364' # Hibernate timeout = never
'7bc4a2f9-d8fc-4469-b07b-33eb785aaca0' # Unattended sleep timeout = never
'94ac6d29-73ce-41a6-809f-6363ba21b47e' # Hybrid sleep = off
'abfc2519-3608-4c2a-94ea-171b0ed546ab' # Allow standby states (S1-S3) = off, removes Sleep from the power menu
)
foreach ($Policy in $PowerPolicies) {
reg add "HKLM\SOFTWARE\Policies\Microsoft\Power\PowerSettings\$Policy" /v ACSettingIndex /t REG_DWORD /d 0 /f | Out-Null
reg add "HKLM\SOFTWARE\Policies\Microsoft\Power\PowerSettings\$Policy" /v DCSettingIndex /t REG_DWORD /d 0 /f | Out-Null
}
# Disable Fast Startup
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Power" /v HiberbootEnabled /t REG_DWORD /d 0 /f | Out-Null
# Don't let Windows power down the network adapter
Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Disable-NetAdapterPowerManagement -NoRestart -ErrorAction SilentlyContinue
# Treat the network as Private and suppress the "allow this PC to be discoverable" prompt
Wait-Network
New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Network\NewNetworkWindowOff' -Force | Out-Null
Get-NetConnectionProfile | Where-Object NetworkCategory -ne 'DomainAuthenticated' | Set-NetConnectionProfile -NetworkCategory Private
# Enable ping (ICMP Echo) requests
if (-not (Get-NetFirewallRule -Name 'OOBE-ICMPv4-In' -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -Name 'OOBE-ICMPv4-In' -DisplayName "Allow ICMP Echo Request" -Protocol ICMPv4 -IcmpType 8 -Enabled True | Out-Null
}
# Allow Remote Desktop (firewall groups are referenced by resource ID so this works on non-English installs)
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0
Enable-NetFirewallRule -Group '@FirewallAPI.dll,-28752'
# Enable c$ (AutoShareWks must be 1; 0 disables the admin shares)
reg add "HKLM\SYSTEM\CurrentControlSet\Services\lanmanserver\parameters" /f /v AutoShareWks /t REG_DWORD /d 1
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v "LocalAccountTokenFilterPolicy" /t REG_DWORD /d 1 /f
Enable-NetFirewallRule -Group '@FirewallAPI.dll,-28502'
# Disable UAC prompt
Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\policies\system' -Name "ConsentPromptBehaviorAdmin" -Value 0
# Enable WinRM / PowerShell remoting
Enable-PSRemoting -Force -SkipNetworkProfileCheck | Out-Null
# Keep the clock in sync (w32time is trigger-start only on workgroup machines)
Set-Service -Name w32time -StartupType Automatic
Start-Service -Name w32time
w32tm /resync /force | Out-Null
# Dark Mode and no mouse acceleration, for the current user and the default profile (future users)
function Set-UserPreferences($Hive) {
reg add "$Hive\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v AppsUseLightTheme /t REG_DWORD /d 0 /f | Out-Null
reg add "$Hive\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v SystemUsesLightTheme /t REG_DWORD /d 0 /f | Out-Null
# "Enhance pointer precision" off
reg add "$Hive\Control Panel\Mouse" /v MouseSpeed /t REG_SZ /d 0 /f | Out-Null
reg add "$Hive\Control Panel\Mouse" /v MouseThreshold1 /t REG_SZ /d 0 /f | Out-Null
reg add "$Hive\Control Panel\Mouse" /v MouseThreshold2 /t REG_SZ /d 0 /f | Out-Null
}
Set-UserPreferences 'HKCU'
$DefaultProfile = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList').Default
reg load 'HKU\OOBEDefault' "$DefaultProfile\NTUSER.DAT" | Out-Null
if ($LASTEXITCODE -eq 0) {
Set-UserPreferences 'HKU\OOBEDefault'
[gc]::Collect()
reg unload 'HKU\OOBEDefault' | Out-Null
}
# Install MeshCentral agent silently as a service
if (Get-Service -Name 'Mesh Agent' -ErrorAction SilentlyContinue) {
Write-Output "Mesh Agent is already installed."
} else {
$MeshInstaller = Join-Path $WorkDir 'meshagent.exe'
if (Save-Download $MeshAgentUrl $MeshInstaller) {
# -fullinstall installs and starts the service without showing the install dialog
$Process = Start-Process -FilePath $MeshInstaller -ArgumentList '-fullinstall' -WindowStyle Hidden -PassThru
if (-not $Process.WaitForExit(300000)) {
Write-Warning "Mesh Agent installer still running after 5 minutes, continuing."
}
}
if (-not (Get-Service -Name 'Mesh Agent' -ErrorAction SilentlyContinue)) {
Write-Warning "Mesh Agent service was not found after install."
}
}
# Install OpenSSH
foreach ($Capability in 'OpenSSH.Client~~~~0.0.1.0', 'OpenSSH.Server~~~~0.0.1.0') {
if ((Get-WindowsCapability -Online -Name $Capability).State -ne 'Installed') {
Add-WindowsCapability -Online -Name $Capability | Out-Null
}
}
Set-Service -Name sshd -StartupType 'Automatic'
Start-Service sshd
# Use PowerShell instead of cmd.exe for SSH sessions
if (Test-Path 'HKLM:\SOFTWARE\OpenSSH') {
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force | Out-Null
}
if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {
Write-Output "Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it..."
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
} else {
Write-Output "Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists."
}
# Install Winget (LTSC ships without the Store / App Installer)
# Run in a child process (the installer script calls exit, which would end this script too),
# with a time limit so it can't hold up the restart. Its output goes to winget-install*.log.
$WingetInstaller = Join-Path $WorkDir 'winget-install.ps1'
if (Save-Download 'https://github.com/asheroto/winget-install/releases/latest/download/winget-install.ps1' $WingetInstaller) {
$Process = Start-Process powershell.exe -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $WingetInstaller, '-Force' -PassThru `
-RedirectStandardOutput (Join-Path $WorkDir 'winget-install.log') -RedirectStandardError (Join-Path $WorkDir 'winget-install-errors.log')
if ($Process.WaitForExit(900000)) {
Write-Output "Winget installer exited with code $($Process.ExitCode)"
} else {
Write-Warning "Winget installer still running after 15 minutes, continuing."
}
}
# Restart to finish driver, guest tools and OpenSSH installation.
# Delayed so the script can exit cleanly and the log is flushed before Windows goes down.
Write-Output "OOBE complete, restarting in 10 seconds."
Stop-Transcript | Out-Null
shutdown.exe /r /t 10 /d p:4:1 /c "OOBE complete, restarting to finish installation."

View File

@ -0,0 +1,334 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
============================================================================
autounattend.xml - Windows 10 Enterprise LTSC 2021 (21H2), x64
Target: Physical desktops/laptops AND virtual machines, UEFI or BIOS
Storage: Any. VirtIO drivers load automatically if a virtio-win ISO is
attached; otherwise use "Load driver" on the disk screen.
Behavior: STOPS at "Where do you want to install Windows?" so you pick
(and delete/format) partitions yourself - nothing is wiped
without you. Then installs, autologons ONCE, installs VirtIO
guest tools if a virtio-win disc is present, and downloads and
runs oobe-desktop.ps1 from https://url.isworking.fyi/oobe-desktop.
BUILD: ./build-isos.sh produces build/desktop.iso with this file as
autounattend.xml plus oobe-desktop.ps1 (offline fallback).
VM: attach it as a second CD next to the LTSC ISO.
PC: copy autounattend.xml and oobe-desktop.ps1 to the root of
the LTSC install USB (or a second USB stick).
Credentials baked in below: silviodante / mdwelcome (stored in CLEAR TEXT)
Search for "EDIT" to find every value you probably want to change.
============================================================================
-->
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<!-- =======================================================================
PASS 1: windowsPE
======================================================================= -->
<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">
<SetupUILanguage>
<UILanguage>en-US</UILanguage>
</SetupUILanguage>
<InputLocale>0409:00000409</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<!-- ===============================================================
VIRTIO DRIVER INJECTION - the load-bearing part.
WinPE has no viostor/vioscsi driver, so without this the vdisk is
invisible on the disk screen. On physical PCs these paths don't
exist and are skipped; the disk screen's Load Driver button is
still available for anything else (RAID/VMD/NVMe controllers).
Drive letters in WinPE are not deterministic: the LTSC disc and the
VirtIO disc land on D:/E: in either order, so both are listed, plus
F: as insurance. Setup logs a warning for paths that don't exist
and continues, so the extra entries cost nothing.
viostor = VirtIO block bus (Unraid vdisk bus: virtio)
vioscsi = VirtIO SCSI bus (Unraid vdisk bus: virtio-scsi)
NetKVM = network, so WinPE has connectivity if you ever need it
=============================================================== -->
<component name="Microsoft-Windows-PnpCustomizationsWinPE"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
<Path>D:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
<Path>E:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
<Path>F:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
<Path>D:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="5">
<Path>E:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="6">
<Path>F:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="7">
<Path>D:\NetKVM\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="8">
<Path>E:\NetKVM\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="9">
<Path>F:\NetKVM\w10\amd64</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">
<!-- No DiskConfiguration and no InstallTo on purpose: Setup prompts
for the target disk/partition, which is the only screen shown. -->
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/INDEX</Key>
<Value>1</Value>
</MetaData>
</InstallFrom>
<WillShowUI>OnError</WillShowUI>
</OSImage>
</ImageInstall>
<UserData>
<!-- EDIT: generic KMS client key for Enterprise LTSC 2021. Gets Setup
past the prompt; does NOT activate. N edition is
92NFX-8DJQP-P6BBQ-THF9C-7CG2H. -->
<ProductKey>
<Key>M7XTQ-FN8P6-TTKYV-9D4CC-J462D</Key>
<WillShowUI>OnError</WillShowUI>
</ProductKey>
<AcceptEula>true</AcceptEula>
<FullName>Silvio Dante</FullName>
<Organization>Bada Bing!</Organization>
</UserData>
</component>
</settings>
<!-- =======================================================================
PASS 2: specialize
======================================================================= -->
<settings pass="specialize">
<!-- Runs as SYSTEM before anyone logs on. Lets admins elevate without a
UAC prompt (oobe-*.ps1 sets the same value later), so nothing at
first logon can stall on an elevation prompt nobody is there to
click: the VirtIO installer, or the RunAs launch in step 4. -->
<component name="Microsoft-Windows-Deployment"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<RunSynchronous>
<RunSynchronousCommand wcm:action="add">
<Order>1</Order>
<Description>Elevate admins without a UAC prompt</Description>
<Path>reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<!-- "*" = random name. A literal name must NOT match any local account. -->
<ComputerName>*</ComputerName>
<!-- EDIT: `tzutil /l` lists every valid value. -->
<TimeZone>Pacific Standard Time</TimeZone>
<RegisteredOwner>Silvio Dante</RegisteredOwner>
<RegisteredOrganization>Bada Bing!</RegisteredOrganization>
</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">
<SkipAutoActivation>true</SkipAutoActivation>
</component>
<!-- RDP on (NLA required) so the machine is reachable remotely. -->
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<fDenyTSConnections>false</fDenyTSConnections>
</component>
<component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<UserAuthentication>1</UserAuthentication>
<SecurityLayer>2</SecurityLayer>
</component>
</settings>
<!-- =======================================================================
PASS 3: oobeSystem
======================================================================= -->
<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">
<InputLocale>0409:00000409</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<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">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<!-- ===============================================================
ACCOUNTS - password is mdwelcome, in clear text, three times.
Treat this file and the merged ISO as secrets.
=============================================================== -->
<UserAccounts>
<AdministratorPassword>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
<LocalAccounts>
<LocalAccount wcm:action="add">
<Name>silviodante</Name>
<DisplayName>Silvio Dante</DisplayName>
<Description>Local administrator for automated builds</Description>
<Group>Administrators</Group>
<Password>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</Password>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<!-- Autologon ONCE, just long enough for the FirstLogonCommands. After
oobe-desktop.ps1 restarts the PC it lands on the sign-in screen. -->
<AutoLogon>
<Enabled>true</Enabled>
<Username>silviodante</Username>
<LogonCount>1</LogonCount>
<Password>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</Password>
</AutoLogon>
<DisableAutoDaylightTimeSet>false</DisableAutoDaylightTimeSet>
<!-- ===============================================================
FIRST LOGON COMMANDS - run once, elevated, as silviodante.
=============================================================== -->
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<Description>Show file extensions</Description>
<CommandLine>reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v HideFileExt /t REG_DWORD /d 0 /f</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>2</Order>
<Description>Allow signed PowerShell scripts</Description>
<CommandLine>powershell -NoProfile -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<!-- VMs only: if a virtio-win disc is attached, install the guest
tools (NetKVM is needed before anything can be downloaded). On
physical PCs no drive has the installer and this does nothing. Scans every drive for
the installer rather than assuming a letter, since the VirtIO
disc moves around post-install. Waits at most 15 minutes, so a stuck
installer can never block the next step. -->
<SynchronousCommand wcm:action="add">
<Order>3</Order>
<Description>Install VirtIO guest tools</Description>
<CommandLine>powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-Volume | Where-Object DriveLetter | ForEach-Object { $exe = $_.DriveLetter + ':\virtio-win-guest-tools.exe'; if (Test-Path $exe) { $p = Start-Process $exe -ArgumentList '/install','/quiet','/norestart' -PassThru; [void]$p.WaitForExit(900000) } }"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<!-- Provisioning: pulls oobe-desktop.ps1 from the repo so the PC always
gets the current version, retrying for ~2.5 minutes while the
network comes up. If that fails, falls back to the copy of
oobe-desktop.ps1 baked into the root of the ISO. The script is saved to
C:\ProgramData\OOBE and started elevated in its own window,
so this step finishes as soon as it has launched it. The script installs
the MeshCentral agent, applies the power/RDP/SSH settings and
restarts the PC itself, so this must stay the LAST command.
Step log: C:\ProgramData\OOBE\firstlogon.log (download / fallback / launch)
Log: C:\ProgramData\OOBE\oobe-desktop.log -->
<SynchronousCommand wcm:action="add">
<Order>4</Order>
<Description>Run oobe-desktop.ps1 (url.isworking.fyi, else ISO copy)</Description>
<CommandLine>powershell -NoProfile -ExecutionPolicy Bypass -Command "$d = Join-Path $env:ProgramData 'OOBE'; New-Item -ItemType Directory -Force $d | Out-Null; Start-Transcript (Join-Path $d 'firstlogon.log') -Append; [Net.ServicePointManager]::SecurityProtocol = 'Tls12'; $s = $null; for ($i = 1; $i -le 10; $i++) { try { $s = irm 'https://url.isworking.fyi/oobe-desktop' -UseBasicParsing; 'Downloaded'; break } catch { 'Attempt ' + $i + ' failed: ' + $_; sleep 15 } }; if (-not $s) { $f = Get-PSDrive -PSProvider FileSystem | % { Join-Path $_.Root 'oobe-desktop.ps1' } | ? { Test-Path $_ } | select -First 1; if ($f) { 'Using ' + $f; $s = Get-Content -Raw $f } }; if ($s) { $p = Join-Path $d 'oobe-desktop.ps1'; Set-Content $p $s; Start-Process powershell -Verb RunAs -ArgumentList ('-NoProfile -ExecutionPolicy Bypass -File ' + $p); 'Started ' + $p } else { Write-Warning 'oobe-desktop.ps1 not found online or on any drive'; sleep 60 }"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>

381
unattend/vms-WIPE-DISK.xml Normal file
View File

@ -0,0 +1,381 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
============================================================================
autounattend.xml - Windows 10 Enterprise LTSC 2021 (21H2), x64
Target: Unraid / KVM virtual machine, UEFI firmware, GPT disk
Storage: VirtIO (or VirtIO-SCSI) vdisk - drivers injected into WinPE
Behavior: Wipes Disk 0, creates ESP/MSR/Windows, installs, autologons,
installs the VirtIO guest tools, then downloads and runs
oobe-vms.ps1 from https://url.isworking.fyi/oobe-vms.
BUILD: ./build-isos.sh produces build/virtio-vms.iso with this file as
autounattend.xml plus oobe-vms.ps1 (offline fallback) at the
root of the latest virtio-win ISO. Attach that ISO in the Unraid VM's
"VirtIO Drivers ISO" slot; leave "OS Install ISO" on the LTSC image.
>>> DESTRUCTIVE: WillWipeDisk erases Disk 0 with no prompt. <<<
Credentials baked in below: silviodante / mdwelcome (stored in CLEAR TEXT)
Search for "EDIT" to find every value you probably want to change.
============================================================================
-->
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<!-- =======================================================================
PASS 1: windowsPE
======================================================================= -->
<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">
<SetupUILanguage>
<UILanguage>en-US</UILanguage>
</SetupUILanguage>
<InputLocale>0409:00000409</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<!-- ===============================================================
VIRTIO DRIVER INJECTION - the load-bearing part.
WinPE has no viostor/vioscsi driver, so without this the vdisk is
invisible and Setup dies with "no drives were found". Because the
disk layout is automated there is no Load Driver button to fall
back on, so this must succeed.
Drive letters in WinPE are not deterministic: the LTSC disc and the
VirtIO disc land on D:/E: in either order, so both are listed, plus
F: as insurance. Setup logs a warning for paths that don't exist
and continues, so the extra entries cost nothing.
viostor = VirtIO block bus (Unraid vdisk bus: virtio)
vioscsi = VirtIO SCSI bus (Unraid vdisk bus: virtio-scsi)
NetKVM = network, so WinPE has connectivity if you ever need it
=============================================================== -->
<component name="Microsoft-Windows-PnpCustomizationsWinPE"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
<Path>D:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
<Path>E:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
<Path>F:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
<Path>D:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="5">
<Path>E:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="6">
<Path>F:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="7">
<Path>D:\NetKVM\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="8">
<Path>E:\NetKVM\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="9">
<Path>F:\NetKVM\w10\amd64</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">
<!-- Standard UEFI/GPT layout: ESP 260MB / MSR 16MB / Windows rest -->
<DiskConfiguration>
<WillShowUI>OnError</WillShowUI>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order>
<Type>EFI</Type>
<Size>260</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>
<Letter>C</Letter>
<Format>NTFS</Format>
</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>3</PartitionID>
</InstallTo>
<InstallToAvailablePartition>false</InstallToAvailablePartition>
<WillShowUI>OnError</WillShowUI>
</OSImage>
</ImageInstall>
<UserData>
<!-- EDIT: generic KMS client key for Enterprise LTSC 2021. Gets Setup
past the prompt; does NOT activate. N edition is
92NFX-8DJQP-P6BBQ-THF9C-7CG2H. -->
<ProductKey>
<Key>M7XTQ-FN8P6-TTKYV-9D4CC-J462D</Key>
<WillShowUI>OnError</WillShowUI>
</ProductKey>
<AcceptEula>true</AcceptEula>
<FullName>Silvio Dante</FullName>
<Organization>Bada Bing!</Organization>
</UserData>
</component>
</settings>
<!-- =======================================================================
PASS 2: specialize
======================================================================= -->
<settings pass="specialize">
<!-- Runs as SYSTEM before anyone logs on. Lets admins elevate without a
UAC prompt (oobe-*.ps1 sets the same value later), so nothing at
first logon can stall on an elevation prompt nobody is there to
click: the VirtIO installer, or the RunAs launch in step 4. -->
<component name="Microsoft-Windows-Deployment"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<RunSynchronous>
<RunSynchronousCommand wcm:action="add">
<Order>1</Order>
<Description>Elevate admins without a UAC prompt</Description>
<Path>reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<!-- "*" = random name. A literal name must NOT match any local account. -->
<ComputerName>*</ComputerName>
<!-- EDIT: `tzutil /l` lists every valid value. -->
<TimeZone>Pacific Standard Time</TimeZone>
<RegisteredOwner>Silvio Dante</RegisteredOwner>
<RegisteredOrganization>Bada Bing!</RegisteredOrganization>
</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">
<SkipAutoActivation>true</SkipAutoActivation>
</component>
<!-- RDP on - the practical way into a headless Unraid VM once the
VirtIO video driver replaces the VNC console. -->
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<fDenyTSConnections>false</fDenyTSConnections>
</component>
<component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<UserAuthentication>1</UserAuthentication>
<SecurityLayer>2</SecurityLayer>
</component>
</settings>
<!-- =======================================================================
PASS 3: oobeSystem
======================================================================= -->
<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">
<InputLocale>0409:00000409</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<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">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<!-- ===============================================================
ACCOUNTS - password is mdwelcome, in clear text, three times.
Treat this file and the merged ISO as secrets.
=============================================================== -->
<UserAccounts>
<AdministratorPassword>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
<LocalAccounts>
<LocalAccount wcm:action="add">
<Name>silviodante</Name>
<DisplayName>Silvio Dante</DisplayName>
<Description>Local administrator for automated VM builds</Description>
<Group>Administrators</Group>
<Password>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</Password>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<!-- Persistent autologon. Drop LogonCount to 1 if you'd rather the VM
land on a logon screen once provisioning finishes. -->
<AutoLogon>
<Enabled>true</Enabled>
<Username>silviodante</Username>
<LogonCount>999</LogonCount>
<Password>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</Password>
</AutoLogon>
<DisableAutoDaylightTimeSet>false</DisableAutoDaylightTimeSet>
<!-- ===============================================================
FIRST LOGON COMMANDS - run once, elevated, as silviodante.
=============================================================== -->
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<Description>Show file extensions</Description>
<CommandLine>reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v HideFileExt /t REG_DWORD /d 0 /f</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>2</Order>
<Description>Allow signed PowerShell scripts</Description>
<CommandLine>powershell -NoProfile -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<!-- NetKVM has to be in place before anything can be downloaded, so
the guest tools go on from the ISO first. Scans every drive for
the installer rather than assuming a letter, since the VirtIO
disc moves around post-install. Waits at most 15 minutes, so a stuck
installer can never block the next step. -->
<SynchronousCommand wcm:action="add">
<Order>3</Order>
<Description>Install VirtIO guest tools</Description>
<CommandLine>powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-Volume | Where-Object DriveLetter | ForEach-Object { $exe = $_.DriveLetter + ':\virtio-win-guest-tools.exe'; if (Test-Path $exe) { $p = Start-Process $exe -ArgumentList '/install','/quiet','/norestart' -PassThru; [void]$p.WaitForExit(900000) } }"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<!-- Provisioning: pulls oobe-vms.ps1 from the repo so the VM always
gets the current version, retrying for ~2.5 minutes while the
network comes up. If that fails, falls back to the copy of
oobe-vms.ps1 baked into the root of the ISO. The script is saved to
C:\ProgramData\OOBE and started elevated in its own window,
so this step finishes as soon as it has launched it. The script installs
the MeshCentral agent, applies the power/RDP/SSH settings and
restarts the VM itself, so this must stay the LAST command.
Step log: C:\ProgramData\OOBE\firstlogon.log (download / fallback / launch)
Log: C:\ProgramData\OOBE\oobe-vms.log -->
<SynchronousCommand wcm:action="add">
<Order>4</Order>
<Description>Run oobe-vms.ps1 (url.isworking.fyi, else ISO copy)</Description>
<CommandLine>powershell -NoProfile -ExecutionPolicy Bypass -Command "$d = Join-Path $env:ProgramData 'OOBE'; New-Item -ItemType Directory -Force $d | Out-Null; Start-Transcript (Join-Path $d 'firstlogon.log') -Append; [Net.ServicePointManager]::SecurityProtocol = 'Tls12'; $s = $null; for ($i = 1; $i -le 10; $i++) { try { $s = irm 'https://url.isworking.fyi/oobe-vms' -UseBasicParsing; 'Downloaded'; break } catch { 'Attempt ' + $i + ' failed: ' + $_; sleep 15 } }; if (-not $s) { $f = Get-PSDrive -PSProvider FileSystem | % { Join-Path $_.Root 'oobe-vms.ps1' } | ? { Test-Path $_ } | select -First 1; if ($f) { 'Using ' + $f; $s = Get-Content -Raw $f } }; if ($s) { $p = Join-Path $d 'oobe-vms.ps1'; Set-Content $p $s; Start-Process powershell -Verb RunAs -ArgumentList ('-NoProfile -ExecutionPolicy Bypass -File ' + $p); 'Started ' + $p } else { Write-Warning 'oobe-vms.ps1 not found online or on any drive'; sleep 60 }"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>