refactor(template): §7 — move OS hardening to Deploy, Setup becomes validation-only

§7.1 — Setup Steps 1/3/4b/5b/5c converted to Assert-Step only:
  - Deploy post-install.ps1 now owns: Firewall (all profiles disabled),
    WinRM MaxMemory=2048MB/IdleTimeout=2h/MaxProcesses=25/StartupType=Automatic,
    lock screen (NoLockScreen), SM GPO policy key + scheduled task disable,
    DisableCAD in Winlogon, OOBE non-policy key.
  - Setup no longer re-applies these — validates them, throws if Deploy missed any.

§7.2 — WU lifecycle Strategy B:
  - Deploy: replaces sc.exe disable with GPO locks only (NoAutoUpdate=1,
    DisableWindowsUpdateAccess=1); services remain Manual (Windows default).
  - Setup Step 6 Step A already clears these locks before COM API; Step 6b
    permanently disables services post-update. No Setup changes needed.

§7.3 — Docs updated:
  - Deploy .DESCRIPTION reflects full hardening ownership.
  - Setup .DESCRIPTION marks validation-only steps, updates step ordering rationale.
  - WINDOWS-TEMPLATE-SETUP.md: three-script architecture table, CI-WinBuild.vmx
    path, Fase C step list + rationale + validation table updated.

TODO: §7.1/7.2/7.3 marked done. §7.4 e2e pending (requires new VM snapshot).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone
2026-05-10 01:27:29 +02:00
parent ea882c99d7
commit 260e275b89
4 changed files with 199 additions and 265 deletions
+66 -170
View File
@@ -8,40 +8,40 @@
This script is run ONE TIME inside the template VM before taking the
"BaseClean" snapshot. After the snapshot is taken, never modify the VM.
Installs and configures (each step followed by Assert-Step validation):
Step 1 — Firewall: all profiles disabled first — removes any block on subsequent steps
Prerequisite: Deploy-WinBuild2025.ps1 must have completed first (unattended OS
install + post-install.ps1 hardening). Steps 1, 3, 4b, 5b, 5c are validation-only
— they assert that Deploy already set the expected state rather than re-applying it.
Steps 4, 5, 6/6b, 7-10 perform actual CI customisation.
Steps and Assert-Step validation:
Step 1 — Firewall: validation only — Deploy disabled all profiles.
Validation: Domain/Private/Public all Enabled=False
Step 2 — Defender state validation only (disabled by Deploy-WinBuild2025
during unattended install via DisableAntiSpyware GPO + RTP off).
Validation: GPO DisableAntiSpyware=1. No exclusion paths — with
Defender off the scanner isn't running, so exclusions are moot.
Step 3 — WinRM: Basic auth, AllowUnencrypted, MaxMemoryPerShellMB=2048, 2h timeout
Firewall + Defender already off — no interference during config
Step 2 — Defender: validation only — Deploy set DisableAntiSpyware=1 GPO + RTP off.
Validation: GPO DisableAntiSpyware=1
Step 3 — WinRM: validation only — Deploy ran Enable-PSRemoting, Basic auth,
AllowUnencrypted, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25.
Validation: service Running+Automatic, AllowUnencrypted, Auth/Basic, memory
Step 4 — Local build user account ($BuildUsername, PasswordNeverExpires, Administrators)
Validation: user exists, enabled, PasswordNeverExpires, admin membership
Step 4b — UAC disabled (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
Step 4b — UAC: validation only — Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1.
Validation: both registry values verified
Step 5 — CI working directories: C:\CI\build, C:\CI\output, C:\CI\scripts
Must run before Windows Update — WU worker writes temp files to C:\CI
Validation: each path exists as Container
Step 5b — Explorer LaunchTo=1 (This PC) for current user + Default profile hive
Step 5b — Explorer LaunchTo=1: validation only — Deploy set HKCU + Default hive.
Validation: HKCU LaunchTo=1
Step 5c — CI UX hardening (all UI tweaks before the long-running WU step):
Server Manager auto-start off (machine policy + HKCU Administrator +
Default User hive → ci_build inherits at first logon)
Ctrl+Alt+Del at login disabled (DisableCAD=1) — CI VMs never need it
OOBE privacy/telemetry prompt suppressed (DisablePrivacyExperience=1,
AllowTelemetry=0) — prevents first-logon interactive prompt in clones
Validation: machine policy key, HKCU key, DisableCAD, OOBE policy, telemetry
Step 6 — Windows Update via Scheduled Task as SYSTEM (avoids WinRM token limit)
Runs with Firewall + Defender off, C:\CI present — faster, no scan overhead
Step 5c — CI UX hardening: validation only — Deploy set lock screen, Server Manager
(policy + HKLM + task + HKCU + Default hive), DisableCAD (Policies\System
+ Winlogon), OOBE/telemetry suppressed.
Validation: NoLockScreen, SM policy, SM task, DisableCAD, OOBE, telemetry
Step 6 — Windows Update via Scheduled Task as SYSTEM (avoids WinRM token limit).
Clears Deploy's GPO locks (Step A), starts services (Step D), runs COM API,
reports result code. Skipped with -SkipWindowsUpdate.
Validation: result code 0/2/3, no reboot required (else exit 3010)
Step 6b — Windows Update permanently disabled (post-update lockdown)
Step 6b — Windows Update permanently disabled (post-update lockdown — always runs).
wuauserv + UsoSvc set to Disabled; GPO keys NoAutoUpdate=1,
DisableWindowsUpdateAccess=1; WU scheduled tasks disabled
Runs unconditionally (even with -SkipWindowsUpdate) so snapshot
captures WU off — linked clones never trigger background updates
DisableWindowsUpdateAccess=1; WU scheduled tasks disabled.
Snapshot captures WU permanently off — linked clones never auto-update.
Validation: wuauserv StartType=Disabled, GPO keys present
Step 7 — .NET SDK (channel $DotNetSdkVersion) via dotnet-install.ps1
Validation: dotnet resolvable, version channel match, machine PATH
@@ -50,28 +50,27 @@
Step 9 — Visual Studio Build Tools 2026 via Scheduled Task as SYSTEM
Validation: MSBuild.exe exists, executes, PATH, VC dir present
Step 10 — Toolchain cross-check (dotnet, python, msbuild)
Throws on any missing tool (was: warn-only)
Throws on any missing tool
Cleanup — Disk cleanup: cleanmgr, SoftwareDistribution, CBS, TEMP, Prefetch, DISM
wuauserv already Disabled (Step 6b) — not restarted after cache clear
SoftwareDistribution\Download cleared best-effort (WaaSMedicSvc may repopulate)
Validation: wuauserv StartType Disabled (SoftwareDistribution check removed —
WaaSMedicSvc tamper-protection restores files immediately)
Validation: wuauserv StartType Disabled
Final — Pre-snapshot gate: 7 cross-cutting checks across all steps
Throws if any check fails — prevents snapshot of broken state
Step ordering rationale:
Firewall first — removes any network block before WinRM config and WU downloads
Defender second — eliminates scan overhead before WinRM, WU, and installer downloads
WinRM third configured clean with no Firewall/Defender interference
User + UAC — quick registry ops; UAC off before dirs/tools avoids elevation prompts
CI dirs — C:\CI must exist before WU worker writes temp files there
UI tweaks — all fast registry writes batched before the slow WU step
WU sixth — all prereqs (dirs, user, tweaks) done; Firewall+Defender off
WU disable — immediately after WU completes; snapshot captures WU permanently off
Toolchain — .NET / Python / VS last; WU done, no scan on installer payloads
Firewall (Step 1) — validates Deploy disabled all profiles; assertion before WinRM/WU
Defender (Step 2) — validates Deploy disabled RTP; assertion before tool install
WinRM (Step 3) validates Deploy configured WinRM; assertion before remote steps
User (Step 4) — creates ci_build; must exist before UAC and dirs assertions
UAC (Step 4b) — validates Deploy disabled UAC (elevation-free installs in 7-9)
CI dirs (Step 5) — C:\CI must exist before WU worker writes temp files there
UI tweaks (5b/5c)validates Deploy UX settings; fast assertions before slow WU
WU (Step 6) — all prereqs validated; WU runs with Firewall+Defender already off
WU disable (Step 6b) — immediately after WU; snapshot captures WU permanently off
Toolchain (7-9) — .NET/Python/VS last; WU done, no scan on installer payloads
NOTE: Git is NOT installed. The host clones the repository and copies
the source tree into the VM via WinRM (Option B isolation model).
NOTE: Git is NOT installed by default. See §6.6 (Tier-1 Toolchain) in TODO.md
for the planned addition. The host clones and copies source via WinRM until then.
╔══════════════════════════════════════════════════════════════════════╗
║ NETWORK REQUIREMENT DURING SETUP ║
@@ -173,16 +172,12 @@ foreach ($f in $knownTempFiles) {
}
}
# ── Step 1: Firewall ──────────────────────────────────────────────────────────
Write-Step "Disabling Windows Firewall (all profiles) — isolated lab VM"
# ── Step 1: Firewall (validation only — disabled by Deploy-WinBuild2025) ──────
Write-Step "Firewall state (validation onlydisabled at deploy time)"
# Disable first: removes any block on subsequent WinRM config and WU downloads.
# This VM lives on VMnet8 NAT behind the VMware NAT gateway — no inbound exposure
# from outside the host, so full disable is acceptable.
Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False
Write-Host "Windows Firewall disabled on all profiles."
# Deploy-WinBuild2025.ps1 post-install.ps1 called Set-NetFirewallProfile -Enabled False
# on all profiles. Validated here before WinRM and WU assertions proceed.
# Validation
foreach ($p in 'Domain','Private','Public') {
Assert-Step 'Firewall' "$p profile disabled" {
(Get-NetFirewallProfile -Profile $p -ErrorAction Stop).Enabled -eq $false
@@ -201,29 +196,13 @@ Assert-Step 'Defender' 'GPO DisableAntiSpyware=1 (set by Deploy-WinBuild2025)' {
((Get-ItemProperty -Path $key -Name DisableAntiSpyware -ErrorAction SilentlyContinue).DisableAntiSpyware -eq 1)
}
# ── Step 3: WinRM ─────────────────────────────────────────────────────────────
Write-Step "Configuring WinRM"
# ── Step 3: WinRM (validation only — configured by Deploy-WinBuild2025) ───────
Write-Step "WinRM state (validation only — configured at deploy time)"
# Firewall and Defender already off — no interference during WinRM hardening.
Enable-PSRemoting -Force -SkipNetworkProfileCheck 3>$null | Out-Null
# Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth,
# AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25,
# and StartupType=Automatic. Validated here before the build user and toolchain steps.
# Allow unencrypted (HTTP/5985) — acceptable for an isolated lab network.
# Migrate to HTTPS/5986 with a self-signed cert for hardened environments.
winrm set winrm/config/service '@{AllowUnencrypted="true"}' | Out-Null
winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null
# Increase shell memory and timeout limits for long builds
winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>$null
Set-Item WSMan:\localhost\Shell\IdleTimeOut 7200000 -Force 3>$null # 2 hours
# Ensure WinRM starts automatically
Set-Service -Name WinRM -StartupType Automatic 3>$null
Start-Service WinRM 3>$null
Write-Host "WinRM configured."
# Validation
Assert-Step 'WinRM' 'service Running' {
(Get-Service WinRM -ErrorAction Stop).Status -eq 'Running'
}
@@ -294,19 +273,13 @@ Assert-Step 'User' "user '$BuildUsername' member of Administrators" {
[bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername)
}
# ── Step 4b: Disable UAC ─────────────────────────────────────────────────────
Write-Step "Disabling UAC (isolated lab VM)"
# ── Step 4b: UAC (validation only — disabled by Deploy-WinBuild2025) ─────────
Write-Step "UAC state (validation only — disabled at deploy time)"
# EnableLUA=0 disables UAC entirely — admin processes get full token without prompt.
# LocalAccountTokenFilterPolicy=1 ensures remote admin connections (WinRM) also
# get an elevated token (avoids the filtered-token issue with runProgramInGuest).
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' `
-Name EnableLUA -Value 0 -Type DWord -Force
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' `
-Name LocalAccountTokenFilterPolicy -Value 1 -Type DWord -Force
Write-Host "UAC disabled."
# Deploy-WinBuild2025.ps1 post-install.ps1 set EnableLUA=0 (no prompt for admin
# processes) and LocalAccountTokenFilterPolicy=1 (WinRM remote connections get full
# elevated token, required for runProgramInGuest and remote admin commands).
# Validation
$uacPolicyKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Assert-Step 'UAC' 'EnableLUA=0' {
(Get-ItemProperty -Path $uacPolicyKey -Name EnableLUA -ErrorAction Stop).EnableLUA -eq 0
@@ -335,109 +308,32 @@ foreach ($dir in $ciDirs) {
}
}
# ── Step 5b: Explorer settings ───────────────────────────────────────────────
Write-Step "Configuring Explorer defaults"
# ── Step 5b: Explorer settings (validation only — configured by Deploy-WinBuild2025) ──
Write-Step "Explorer defaults (validation only — configured at deploy time)"
# Deploy-WinBuild2025.ps1 post-install.ps1 set LaunchTo=1 for HKCU (Administrator)
# and the Default User hive (so ci_build and any new user inherits at first logon).
# Open Explorer to 'This PC' (Devices and Drives) instead of Quick Access.
# LaunchTo=1 = This PC, LaunchTo=2 = Quick Access (default)
$explorerAdvKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
Set-ItemProperty -Path $explorerAdvKey -Name 'LaunchTo' -Value 1 -Type DWord -Force
# Also apply to the Default User profile so ci_build and any new user inherits it.
$defaultHive = 'C:\Users\Default\NTUSER.DAT'
$mountName = 'TempDefaultUser'
if (Test-Path $defaultHive) {
reg load "HKU\$mountName" $defaultHive | Out-Null
$defKey = "Registry::HKU\$mountName\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
if (-not (Test-Path $defKey)) { New-Item -Path $defKey -Force | Out-Null }
Set-ItemProperty -Path $defKey -Name 'LaunchTo' -Value 1 -Type DWord -Force
[gc]::Collect()
reg unload "HKU\$mountName" | Out-Null
Write-Host "Explorer: 'This PC' set for current user and default profile."
}
else {
Write-Host "Explorer: 'This PC' set for current user (default hive not found)."
}
# Validation
Assert-Step 'Explorer' "LaunchTo=1 (HKCU current user)" {
(Get-ItemProperty -Path $explorerAdvKey -Name LaunchTo -ErrorAction Stop).LaunchTo -eq 1
}
# ── Step 5c: CI UX hardening ────────────────────────────────────────────────
Write-Step "CI UX hardening (lock screen off, Server Manager off, no CAD, no OOBE)"
# ── Step 5c: CI UX hardening (validation only — configured by Deploy-WinBuild2025)
Write-Step "CI UX hardening state (validation only — configured at deploy time)"
# All UI tweaks batched here — fast registry writes before the slow WU step.
# Deploy-WinBuild2025.ps1 post-install.ps1 set: lock screen (NoLockScreen=1), Server
# Manager (policy + HKLM + task + HKCU + Default hive), DisableCAD (Policies\System +
# Winlogon), OOBE DisablePrivacyExperience (policy + non-policy), AllowTelemetry=0.
# 1 — Lock screen: disable entirely (CI VMs never need a lock screen)
# NoLockScreen=1 suppresses the lock screen shown before login on console sessions.
$personPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization'
if (-not (Test-Path $personPolicyKey)) { New-Item -Path $personPolicyKey -Force | Out-Null }
Set-ItemProperty -Path $personPolicyKey -Name 'NoLockScreen' -Value 1 -Type DWord -Force
Write-Host "Lock screen disabled."
# 2 — Server Manager: do not auto-open at logon
# Three layers needed on WS2025:
# a) Machine policy key (GPO path — read by ServerManager policy check)
# b) HKLM non-policy key (read directly by ServerManager.exe on WS2025)
# c) Scheduled Task '\Microsoft\Windows\Server Manager\ServerManager'
# — this is the actual launcher; registry alone is insufficient on WS2025
# d) HKCU + Default hive for per-user belt-and-suspenders
$smPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager'
if (-not (Test-Path $smPolicyKey)) { New-Item -Path $smPolicyKey -Force | Out-Null }
Set-ItemProperty -Path $smPolicyKey -Name 'DoNotOpenAtLogon' -Value 1 -Type DWord -Force
$smHKLMKey = 'HKLM:\SOFTWARE\Microsoft\ServerManager'
if (-not (Test-Path $smHKLMKey)) { New-Item -Path $smHKLMKey -Force | Out-Null }
Set-ItemProperty -Path $smHKLMKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
Disable-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' `
-TaskName 'ServerManager' -ErrorAction SilentlyContinue | Out-Null
# Current session user (Administrator)
$smUserKey = 'HKCU:\SOFTWARE\Microsoft\ServerManager'
if (-not (Test-Path $smUserKey)) { New-Item -Path $smUserKey -Force | Out-Null }
Set-ItemProperty -Path $smUserKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
# Default User hive — ci_build and any future account inherits at first logon
$smDefaultHive = 'C:\Users\Default\NTUSER.DAT'
$smMountName = 'TempDefaultUserSM'
if (Test-Path $smDefaultHive) {
reg load "HKU\$smMountName" $smDefaultHive | Out-Null
$smDefKey = "Registry::HKU\$smMountName\SOFTWARE\Microsoft\ServerManager"
if (-not (Test-Path $smDefKey)) { New-Item -Path $smDefKey -Force | Out-Null }
Set-ItemProperty -Path $smDefKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
[gc]::Collect()
reg unload "HKU\$smMountName" | Out-Null
Write-Host "Server Manager: policy + HKLM + task + Administrator HKCU + Default hive set."
}
else {
Write-Host "Server Manager: policy + HKLM + task + Administrator HKCU set (Default hive not found)."
}
# 3 — Disable Ctrl+Alt+Del at interactive login
# Set in both locations: Winlogon (classic) + Policies\System (policy, authoritative on WS2025).
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' `
-Name 'DisableCAD' -Value 1 -Type DWord -Force
$smPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager'
$smHKLMKey = 'HKLM:\SOFTWARE\Microsoft\ServerManager'
$smUserKey = 'HKCU:\SOFTWARE\Microsoft\ServerManager'
$systemPolicyKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Set-ItemProperty -Path $systemPolicyKey -Name 'DisableCAD' -Value 1 -Type DWord -Force
Write-Host "Ctrl+Alt+Del at login disabled (Winlogon + Policies\System)."
$oobePolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE'
$dcKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'
# 4 — Suppress OOBE privacy/telemetry prompt
$oobePolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE'
if (-not (Test-Path $oobePolicyKey)) { New-Item -Path $oobePolicyKey -Force | Out-Null }
Set-ItemProperty -Path $oobePolicyKey -Name 'DisablePrivacyExperience' -Value 1 -Type DWord -Force
$oobeKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE'
if (-not (Test-Path $oobeKey)) { New-Item -Path $oobeKey -Force | Out-Null }
Set-ItemProperty -Path $oobeKey -Name 'DisablePrivacyExperience' -Value 1 -Type DWord -Force
$dcKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'
if (-not (Test-Path $dcKey)) { New-Item -Path $dcKey -Force | Out-Null }
Set-ItemProperty -Path $dcKey -Name 'AllowTelemetry' -Value 0 -Type DWord -Force
Write-Host "OOBE privacy prompt and telemetry disabled."
# Validation
Assert-Step 'CIUX' 'lock screen disabled (NoLockScreen=1)' {
(Get-ItemProperty -Path $personPolicyKey -Name NoLockScreen -ErrorAction Stop).NoLockScreen -eq 1
}