From ace85fb1744649a8aedc27b7c420a6ad401e5996 Mon Sep 17 00:00:00 2001 From: Chris Nutter Date: Thu, 17 Sep 2026 10:15:54 -0700 Subject: [PATCH] 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 --- oobe-ltsc.ps1 | 218 +++++++++++++++++++++++++++++++++++ Script.ps1 => oobe-rm-ai.ps1 | 0 2 files changed, 218 insertions(+) create mode 100644 oobe-ltsc.ps1 rename Script.ps1 => oobe-rm-ai.ps1 (100%) diff --git a/oobe-ltsc.ps1 b/oobe-ltsc.ps1 new file mode 100644 index 0000000..cf4ce1d --- /dev/null +++ b/oobe-ltsc.ps1 @@ -0,0 +1,218 @@ +# OOBE for Windows LTSC virtual machines. Runs fully unattended (no prompts), e.g. from autounattend.xml: +# powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Windows\Setup\Scripts\oobe-ltsc.ps1 +# Log: C:\ProgramData\OOBE\oobe-ltsc.log + +# 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 1 +} + +$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-ltsc.log') -Append | Out-Null + +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() + + $Process = Start-Process -FilePath $GuestTools -ArgumentList '/install', '/quiet', '/norestart' -Wait -PassThru + Write-Output "VirtIO guest tools installer exited with code $($Process.ExitCode) (0 = success, 3010 = reboot required)" + } +} + +# 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 + Start-Process -FilePath $MeshInstaller -ArgumentList '-fullinstall' -WindowStyle Hidden -Wait + } + 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 +$WingetInstaller = Join-Path $WorkDir 'winget-install.ps1' +if (Save-Download 'https://github.com/asheroto/winget-install/releases/latest/download/winget-install.ps1' $WingetInstaller) { + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $WingetInstaller -Force +} + +# 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." diff --git a/Script.ps1 b/oobe-rm-ai.ps1 similarity index 100% rename from Script.ps1 rename to oobe-rm-ai.ps1