security(sprint1): §1.1/1.3/1.4 — WinRM HTTPS/5986, SHA256 pinning infra, IP regex per-octet

§1.4 — Replace loose IP ValidatePattern with per-octet regex in 4 scripts
  (Invoke-CIJob, Invoke-RemoteBuild, Get-BuildArtifacts, Wait-VMReady)

§1.1 — Migrate all WinRM connections from HTTP/5985 to HTTPS/5986
  - Deploy post-install.ps1: remove AllowUnencrypted=true; self-signed cert + HTTPS listener
    already present; firewall rule restricted to 192.168.79.0/24
  - Setup-WinBuild2025.ps1: assert AllowUnencrypted=false
  - Prepare-WinBuild2025.ps1: preflight uses TCP/5986; Test-WSMan -UseSSL -Port 5986;
    New-PSSession -UseSSL -Port 5986; host AllowUnencrypted save/restore removed (not needed for HTTPS)
  - Invoke-RemoteBuild, Get-BuildArtifacts: New-PSSession -UseSSL -Port 5986 -Authentication Basic
  - Wait-VMReady: New-WSManSessionOption + Test-WSMan -Port 5986 -UseSSL
  - Validate-DeployState, Validate-SetupState: Invoke-Command -UseSSL -Port 5986,
    AllowUnencrypted check → false; AllowUnencrypted host-side removed from both
  - Docs: PLAN, README, ARCHITECTURE, BEST-PRACTICES, HOST-SETUP, WINDOWS-TEMPLATE-SETUP,
    LINUX-TEMPLATE-SETUP, TODO updated

§1.3 — SHA256 hash pinning infrastructure
  - Assert-Hash function + $script:Hashes hashtable added to Setup-WinBuild2025.ps1
  - Assert-Hash called after dotnet-install.ps1, python installer, vs_buildtools.exe downloads
  - Hashes initialized to '' (warn-skip); must be filled before next snapshot refresh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone
2026-05-10 14:46:41 +02:00
parent 41611d46a7
commit 68cde01c9d
17 changed files with 193 additions and 183 deletions
+52 -8
View File
@@ -18,9 +18,9 @@
Validation: Domain/Private/Public all Enabled=False
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 3 — WinRM: validation only — Deploy ran Enable-PSRemoting, Basic auth over HTTPS,
AllowUnencrypted=false (HTTPS-only), MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25.
Validation: service Running+Automatic, AllowUnencrypted=false, Auth/Basic, memory
Step 4 — Local build user account ($BuildUsername, PasswordNeverExpires, Administrators)
Validation: user exists, enabled, PasswordNeverExpires, admin membership
Step 4b — UAC: validation only — Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1.
@@ -161,6 +161,44 @@ function Assert-Step {
Write-Host " [OK] $Description" -ForegroundColor Green
}
function Assert-Hash {
# Verifies SHA256 of a downloaded file against a pinned expected value.
# If $Expected is empty the check is skipped with a warning (unpinned installer).
# To pin: download the file, run (Get-FileHash <path> -Algorithm SHA256).Hash, fill in below.
param(
[Parameter(Mandatory)] [string] $FilePath,
[Parameter(Mandatory)] [string] $Label,
[string] $Expected = ''
)
if (-not $Expected) {
Write-Host " [WARN] Hash not pinned for $Label — set `$script:Hashes to enforce (see §1.3)" -ForegroundColor Yellow
return
}
$actual = (Get-FileHash $FilePath -Algorithm SHA256).Hash.ToUpper()
if ($actual -ne $Expected.ToUpper()) {
throw "Hash mismatch for ${Label}:`n expected: $($Expected.ToUpper())`n actual: $actual`nPossible MITM or wrong installer version — do NOT proceed."
}
Write-Host " [OK] SHA256 verified: $Label" -ForegroundColor Green
}
# ── Pinned SHA256 hashes for downloaded installers (§1.3) ─────────────────────
# Fill in the hash values below. Get them by:
# (Invoke-WebRequest '<URL>' -UseBasicParsing -OutFile 'tmp.bin'; (Get-FileHash 'tmp.bin' -Algorithm SHA256).Hash)
# Update when the corresponding version param changes. Leave '' to skip (warns at runtime).
$script:Hashes = @{
# python-<PythonVersion>-amd64.exe from https://www.python.org/downloads/release/python-XYZ/
# Update $script:Hashes['Python'] when $PythonVersion changes.
'Python' = ''
# dotnet-install.ps1 from https://dot.net/v1/dotnet-install.ps1
# Changes with each .NET SDK release cycle — verify after any dotnet version bump.
'DotNetInstallScript' = ''
# vs_buildtools.exe bootstrapper from aka.ms/vs/stable/vs_buildtools.exe
# Stable within a VS release cycle — update when VS major version changes.
'VSBuildToolsBootstrapper' = ''
}
# ── Pre-flight: remove temp files from any previous partial run ───────────────
# These files are normally deleted at the end of each step that creates them.
# A previous interrupted run may have left them behind — remove before starting.
@@ -203,9 +241,9 @@ Assert-Step 'Defender' 'GPO DisableAntiSpyware=1 (set by Deploy-WinBuild2025)' {
# ── Step 3: WinRM (validation only — configured by Deploy-WinBuild2025) ───────
Write-Step "WinRM state (validation only — configured at deploy time)"
# 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.
# Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth over HTTPS,
# AllowUnencrypted=false (HTTPS/5986 only), TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h,
# MaxProcesses=25, StartupType=Automatic. Validated here before the build user and toolchain steps.
Assert-Step 'WinRM' 'service Running' {
(Get-Service WinRM -ErrorAction Stop).Status -eq 'Running'
@@ -213,8 +251,8 @@ Assert-Step 'WinRM' 'service Running' {
Assert-Step 'WinRM' 'service StartType Automatic' {
(Get-Service WinRM -ErrorAction Stop).StartType -eq 'Automatic'
}
Assert-Step 'WinRM' 'AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted).Value -eq 'true'
Assert-Step 'WinRM' 'AllowUnencrypted=false (HTTPS-only)' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted).Value -eq 'false'
}
Assert-Step 'WinRM' 'Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic).Value -eq 'true'
@@ -589,6 +627,8 @@ else {
$dotnetInstallScript = "$env:TEMP\dotnet-install.ps1"
Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' `
-OutFile $dotnetInstallScript -UseBasicParsing
Assert-Hash -FilePath $dotnetInstallScript -Label 'dotnet-install.ps1' `
-Expected $script:Hashes['DotNetInstallScript']
Write-Host "Installing .NET SDK $DotNetSdkVersion (channel)..."
& $dotnetInstallScript `
@@ -635,6 +675,8 @@ else {
$pyInstallerPath = 'C:\CI\python_installer.exe'
Write-Host "Downloading Python $PythonVersion installer..."
Invoke-WebRequest -Uri $pyInstallerUrl -OutFile $pyInstallerPath -UseBasicParsing
Assert-Hash -FilePath $pyInstallerPath -Label "python-$PythonVersion-amd64.exe" `
-Expected $script:Hashes['Python']
Write-Host "Installing Python $PythonVersion (all users, prepend PATH)..."
$pyProc = Start-Process -FilePath $pyInstallerPath -ArgumentList @(
@@ -689,6 +731,8 @@ else {
Write-Host "Downloading VS Build Tools installer..."
Remove-Item $vsInstallerPath -ErrorAction SilentlyContinue # remove any stale/corrupt copy
Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath -UseBasicParsing
Assert-Hash -FilePath $vsInstallerPath -Label 'vs_buildtools.exe' `
-Expected $script:Hashes['VSBuildToolsBootstrapper']
# Remove Zone.Identifier ADS — files downloaded from internet are marked Zone 3 (Internet).
# SYSTEM account cannot execute them without this unblock step.