diff --git a/PLAN.md b/PLAN.md index 45a9b58..e2fbd1c 100644 --- a/PLAN.md +++ b/PLAN.md @@ -16,7 +16,7 @@ - Gitea self-hosted — `http://10.10.20.11:3100` - act_runner v1.0.2 — Windows service su host, label `windows-build` - Build VMs: subnet VMnet8 NAT — `192.168.79.0/24` -- WinRM HTTP/5985 (Basic auth, lab-only) +- WinRM HTTPS/5986 (Basic auth over HTTPS, self-signed cert, SkipCACheck lab-only) ### Architettura implementata 1. Gitea Actions enqueue job → act_runner lo prende diff --git a/README.md b/README.md index 9abf35d..fb00e98 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Ogni build gira in una VM Windows Server 2025 **usa e getta**, ripristinata dall | .NET SDK | 10.0.203 | | Python | 3.13.3 | | Rete | VMnet8 NAT — `192.168.79.0/24` | -| WinRM | HTTP/5985, Basic auth (lab-only) | +| WinRM | HTTPS/5986, Basic auth, self-signed cert (SkipCACheck — lab-only) | --- @@ -181,6 +181,7 @@ e aggiorna `-RepoUrl`, `-BuildCommand` e `-GuestArtifactSource`. ## Note di sicurezza -WinRM in modalità HTTP/Basic è **accettabile solo in ambiente lab isolato**. -Per ambienti di produzione o condivisi, migrare a HTTPS/5986 con certificato self-signed -e rimuovere `AllowUnencrypted=true`. Vedi [docs/BEST-PRACTICES.md](docs/BEST-PRACTICES.md). +WinRM usa **HTTPS/5986** con certificato self-signed e Basic auth. +`-SkipCACheck`/`-SkipCNCheck` sono accettabili in lab isolato (il cert non è di una CA trusted). +Per ambienti condivisi o di produzione, usare un certificato CA valida e rimuovere `-SkipCACheck`. +Vedi [docs/BEST-PRACTICES.md](docs/BEST-PRACTICES.md). diff --git a/TODO.md b/TODO.md index 91989f9..1a6d090 100644 --- a/TODO.md +++ b/TODO.md @@ -65,7 +65,7 @@ ```powershell Enable-PSRemoting -Force -SkipNetworkProfileCheck Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force - Set-Item WSMan:\localhost\Service\AllowUnencrypted $true -Force + # AllowUnencrypted left false — Deploy post-install.ps1 configures HTTPS/5986 listener Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force ``` - [x] **Attiva Windows Server 2025** (prima dello snapshot — i linked clone ereditano l'attivazione): @@ -87,7 +87,7 @@ ``` Installa: WinRM config, utente `ci_build`, .NET SDK 10.0, VS Build Tools 2026, Python 3.13.3. Lo script esegue validazione automatica ad ogni passaggio (`Assert-Step`): - - Pre-flight: IP ottetti 0-255, TCP/5985 raggiungibile, Setup-WinBuild2025.ps1 presente + - Pre-flight: IP ottetti 0-255, TCP/5986 raggiungibile, Setup-WinBuild2025.ps1 presente - Host WinRM: AllowUnencrypted, TrustedHosts - Guest prep: `C:\CI` esiste, file copiato, size match - Post-setup remoto (9 check): WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs @@ -148,25 +148,22 @@ ## 1. Sicurezza & hardening -### 1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986 -File: [template/Setup-WinBuild2025.ps1:204-241](template/Setup-WinBuild2025.ps1) (Step 3 WinRM), [template/Deploy-WinBuild2025.ps1:524-534](template/Deploy-WinBuild2025.ps1) (Enable-PSRemoting + AllowUnencrypted/Basic), [scripts/Invoke-RemoteBuild.ps1:83-91](scripts/Invoke-RemoteBuild.ps1), [scripts/Get-BuildArtifacts.ps1:66-75](scripts/Get-BuildArtifacts.ps1). - -**Motivazione**: la combo `AllowUnencrypted=true` + `Auth/Basic=true` invia credenziali del -`ci_build` (admin) in chiaro su VMnet8. Anche in lab isolato, l'host può essere compromesso -via altri vettori e scrapare la rete VMware. Il piano è già in `docs/BEST-PRACTICES.md §2`; -manca solo l'esecuzione. +### 1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986 — COMPLETATO 2026-05-10 **Azioni**: -- [ ] Generare cert self-signed nel template *prima* dello snapshot. -- [ ] Sostituire `New-PSSession -ComputerName $ip` con `-UseSSL -Port 5986` in `Invoke-RemoteBuild.ps1` e `Get-BuildArtifacts.ps1`. -- [ ] Rimuovere `AllowUnencrypted=true` dal `Setup-WinBuild2025.ps1`. -- [ ] Tenere `-SkipCACheck` lato host: per cert lab self-signed è accettabile, ma documentare la motivazione inline. -- [ ] Regole firewall — limitare WinRM alla subnet build VM (`192.168.79.0/24` — VMnet8 NAT). +- [x] Generare cert self-signed nel template *prima* dello snapshot (già in Deploy post-install.ps1). +- [x] Sostituire `New-PSSession -ComputerName $ip` con `-UseSSL -Port 5986 -Authentication Basic` in `Invoke-RemoteBuild.ps1`, `Get-BuildArtifacts.ps1`, `Prepare-WinBuild2025.ps1`, `Validate-*.ps1`. +- [x] Rimuovere `AllowUnencrypted=true` da Deploy post-install.ps1 e Setup-WinBuild2025.ps1 (assertion → false). +- [x] `-SkipCACheck`/`-SkipCNCheck`/`-SkipRevocationCheck` mantenuti nei `New-PSSessionOption` e `New-WSManSessionOption` (cert lab self-signed — documentato inline). +- [x] Regola firewall `WinRM-HTTPS` in Deploy ristretta a `RemoteAddress '192.168.79.0/24'`. +- [x] `Wait-VMReady.ps1` usa `New-WSManSessionOption` + `Test-WSMan -Port 5986 -UseSSL`. +- [x] `Prepare-WinBuild2025.ps1`: preflight TCP/5986, host non imposta più `AllowUnencrypted`; solo `TrustedHosts` viene salvato/ripristinato. +- [x] `Validate-DeployState.ps1`, `Validate-SetupState.ps1`: connessione HTTPS/5986, check `AllowUnencrypted=false`. ### 1.2 [P0] Restringere `TrustedHosts` lato host (audit-only — già coperto) File: nessuno script imposta `*` sull'host. Stato corrente: - `Setup-Host.ps1` non tocca TrustedHosts (verificato 2026-05-10) -- [template/Prepare-WinBuild2025.ps1:270-294](template/Prepare-WinBuild2025.ps1:270) appende `$VMIPAddress` e ripristina nel `finally` (riga 617) +- [template/Prepare-WinBuild2025.ps1](template/Prepare-WinBuild2025.ps1) appende `$VMIPAddress` ai `TrustedHosts` e ripristina nel `finally` (post-§1.1: solo TrustedHosts, no AllowUnencrypted) - [docs/HOST-SETUP.md:121-126](docs/HOST-SETUP.md:121) raccomanda `'192.168.79.*'` in setup manuale **Azione residua**: audit eventuali host già configurati con `*` (eredità manuale) e @@ -175,32 +172,28 @@ sostituire con la subnet build: Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate ``` -### 1.3 [P0] Pinning hash SHA256 degli installer -File: [template/Setup-WinBuild2025.ps1:707-755](template/Setup-WinBuild2025.ps1) (Step 8 Python), [template/Setup-WinBuild2025.ps1:757-885](template/Setup-WinBuild2025.ps1) (Step 9 VS Build Tools), [template/Setup-WinBuild2025.ps1:660-705](template/Setup-WinBuild2025.ps1) (Step 7 dotnet-install.ps1). +### 1.3 [P0] Pinning hash SHA256 degli installer — STRUTTURA AGGIUNTA 2026-05-10 -**Motivazione**: Python, `dotnet-install.ps1`, `vs_buildtools.exe` vengono scaricati senza -verifica integrità. Un MITM nella rete dell'host (anche temporaneo) può iniettare binari -compromessi nel template, che poi viene snapshottato e usato da *ogni* build. +`$script:Hashes` + `Assert-Hash` aggiunti a [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1). +`Assert-Hash` viene chiamato dopo ogni download (dotnet-install.ps1, python installer, vs_buildtools.exe). +Se l'hash è `''` viene emesso un warning ma l'esecuzione continua (non-breaking). -Pattern minimale: -```powershell -$expected = '6F25A7DF...' # SHA256 pinnato per la versione esatta -$actual = (Get-FileHash $pyInstallerPath -Algorithm SHA256).Hash.ToLower() -if ($actual -ne $expected.ToLower()) { throw "Hash mismatch: $actual" } -``` -Aggiungere un blocco hash per ogni download e bloccarli in costanti `$script:Hashes`. -Aggiornare al cambio versione (è raro). +**Azione residua — OBBLIGATORIA prima del prossimo refresh snapshot**: +- [ ] Scaricare i tre file, calcolare hash, riempire `$script:Hashes` in Setup-WinBuild2025.ps1: + ```powershell + # Inside or outside VM — qualsiasi PowerShell: + (Get-FileHash 'python-3.13.3-amd64.exe' -Algorithm SHA256).Hash # → $script:Hashes['Python'] + (Get-FileHash 'dotnet-install.ps1' -Algorithm SHA256).Hash # → $script:Hashes['DotNetInstallScript'] + (Get-FileHash 'vs_buildtools.exe' -Algorithm SHA256).Hash # → $script:Hashes['VSBuildToolsBootstrapper'] + ``` +- [ ] Verificare che `Assert-Hash` throwi correttamente su hash errato (test con hash fake). -### 1.4 [P1] Validazione IP per-ottetto -File: [scripts/Invoke-CIJob.ps1:106](scripts/Invoke-CIJob.ps1), [scripts/Invoke-RemoteBuild.ps1:52](scripts/Invoke-RemoteBuild.ps1), [scripts/Get-BuildArtifacts.ps1:42](scripts/Get-BuildArtifacts.ps1), [scripts/Wait-VMReady.ps1:43](scripts/Wait-VMReady.ps1). +### 1.4 [P1] Validazione IP per-ottetto — COMPLETATO 2026-05-10 -**Motivazione**: `'^(\d{1,3}\.){3}\d{1,3}$'` accetta `999.999.999.999`. Sostituire con regex -per-ottetto: -```powershell -$ipv4 = '^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$' -[ValidatePattern($ipv4)] -``` -Estrarre la costante in un modulo condiviso `scripts/_Common.psm1` per evitare drift fra i 4 file (vedi §5.2). +Regex per-ottetto sostituita in tutti e 4 i file: +`'^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$'` + +Estrazione in `scripts/_Common.psm1` — vedi §5.2 (P2, backlog). ### 1.5 [P1] PAT mai persistito quando si abilita `-UseGitClone` Requisiti di sicurezza per l'implementazione del clone in-VM. La sequenza operativa diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ae4ee48..1e4d7fd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -53,7 +53,7 @@ The runner host **never executes build tools directly**. Its only role is orches │ │ ├── Clone_Job_002.vmx │ │ │ │ └── Clone_Job_003.vmx │ │ │ └───────────────────────────────────────────────────────────────────┘ │ -│ │ WinRM :5985 (192.168.79.0/24 NAT) │ +│ │ WinRM :5986 HTTPS (192.168.79.0/24 NAT) │ └──────────────────────────────────┼──────────────────────────────────────┘ │ ┌────────────────────────┼──────────────────────────┐ @@ -105,8 +105,8 @@ The runner host **never executes build tools directly**. Its only role is orches ### WinRM / PowerShell Remoting - Default transport for remote build execution inside VMs -- Port: **5985** (HTTP, in-lab use); migrate to 5986/HTTPS for hardened setups -- Authentication: **Basic** (unencrypted — acceptable for isolated lab) +- Port: **5986** (HTTPS, self-signed cert, `AllowUnencrypted=false`) +- Authentication: **Basic over HTTPS** (`-SkipCACheck` for lab self-signed cert) - Used for: - Transferring source: `Compress-Archive` on host → WinRM copy → `Expand-Archive` in guest - Running build commands (`Invoke-Command`) @@ -133,7 +133,7 @@ Build VMs: ... Clone_Job_N → 192.168.79.1xx -WinRM port 5985 (HTTP) on each VM IP. +WinRM port 5986 (HTTPS, self-signed) on each VM IP. VMs have internet access via NAT — required for pip/nuget during build. ``` diff --git a/docs/BEST-PRACTICES.md b/docs/BEST-PRACTICES.md index 456d809..734dca9 100644 --- a/docs/BEST-PRACTICES.md +++ b/docs/BEST-PRACTICES.md @@ -30,47 +30,26 @@ $cred = Get-StoredCredential -Target 'BuildVMGuest' --- -## 2. WinRM Security +## 2. WinRM Security — HTTPS/5986 (implementato 2026-05-10) -### Current setup (HTTP / port 5985) +### Setup attuale (HTTPS / port 5986) -Acceptable for an **isolated lab network** where: -- Build VMs sono su VMnet8 NAT (192.168.79.0/24) — raggiungibili dall'host, internet via NAT -- No external traffic reaches port 5985 -- Credentials are managed via Windows Credential Manager +`Deploy-WinBuild2025.ps1` post-install.ps1 crea un certificato self-signed e configura +il listener HTTPS/5986 **prima** dello snapshot `BaseClean`. `AllowUnencrypted=false`. -### Recommended upgrade: WinRM over HTTPS (port 5986) +- Build VMs su VMnet8 NAT (192.168.79.0/24) — accesso solo dall'host +- Port 5986 firewall rule ristretta a `RemoteAddress '192.168.79.0/24'` +- Credentials via Windows Credential Manager (target `BuildVMGuest`) +Tutti gli script host usano: ```powershell -# Inside the template VM (before taking BaseClean snapshot): - -# Create self-signed certificate -$cert = New-SelfSignedCertificate ` - -Subject "CN=$(hostname)" ` - -CertStoreLocation Cert:\LocalMachine\My ` - -KeyUsage DigitalSignature, KeyEncipherment ` - -KeyAlgorithm RSA ` - -KeyLength 2048 - -# Create HTTPS WinRM listener -New-WSManInstance WinRM/Config/Listener ` - -SelectorSet @{ Transport = 'HTTPS'; Address = '*' } ` - -ValueSet @{ Hostname = $(hostname); CertificateThumbprint = $cert.Thumbprint } - -# Allow port 5986 -New-NetFirewallRule -Name 'WinRM-HTTPS-CI' -DisplayName 'WinRM HTTPS CI' ` - -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -``` - -```powershell -# On the HOST (in scripts), use HTTPS session options: $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -$session = New-PSSession -ComputerName $ip -Port 5986 -UseSSL ` +$session = New-PSSession -ComputerName $ip -Port 5986 -UseSSL -Authentication Basic ` -Credential $cred -SessionOption $sessionOptions ``` -> `-SkipCACheck` is acceptable for a self-signed cert in an isolated lab. Do NOT -> use this against externally accessible machines. +> `-SkipCACheck`/`-SkipCNCheck` sono accettabili per un cert self-signed in lab isolato. +> Non usare contro macchine accessibili dall'esterno — usare una CA trusted in quel caso. --- @@ -251,7 +230,7 @@ Invoke-Command -Session $session -ScriptBlock { ``` Build VMs can reach: -- The host via VMnet8 gateway (WinRM on port 5985) +- The host via VMnet8 gateway (WinRM HTTPS on port 5986) - Internet via VMware NAT (for pip, nuget, npm at build time) - Gitea server if on LAN reachable via NAT gateway diff --git a/docs/HOST-SETUP.md b/docs/HOST-SETUP.md index 971ffee..bfc615f 100644 --- a/docs/HOST-SETUP.md +++ b/docs/HOST-SETUP.md @@ -120,10 +120,10 @@ Allo `Setup-Host.ps1` exit 0: 3. **Configura host WinRM client** (necessario per Prepare-WinBuild2025.ps1): ```powershell - Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force + # AllowUnencrypted non serve — Prepare usa HTTPS/5986 (nessun testo in chiaro) Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate ``` - *Note*: Prepare-WinBuild2025.ps1 imposta queste in modo transitorio e le ripristina nel `finally`. + *Note*: Prepare-WinBuild2025.ps1 aggiunge l'IP specifico della VM in modo transitorio e lo ripristina nel `finally`. 4. **Provisiona il template VM** — vedi [WINDOWS-TEMPLATE-SETUP.md](WINDOWS-TEMPLATE-SETUP.md) diff --git a/docs/LINUX-TEMPLATE-SETUP.md b/docs/LINUX-TEMPLATE-SETUP.md index c6a95c9..93eab1f 100644 --- a/docs/LINUX-TEMPLATE-SETUP.md +++ b/docs/LINUX-TEMPLATE-SETUP.md @@ -314,7 +314,7 @@ Equivalente di `Prepare-WinBuild2025.ps1` per Linux. Usa SSH invece di WinRM. | Aspetto | Windows | Linux | | ----------------------- | -------------------------------------- | ------------------------------------------- | -| Transport | WinRM HTTP/5985 + Basic | SSH/22 + ed25519 key | +| Transport | WinRM HTTPS/5986 + Basic (-SkipCACheck) | SSH/22 + ed25519 key | | Auth fluid | `ci_build` admin + UAC off + LATFP=1 | `ci_build` + sudoers NOPASSWD | | Trasferimento file | `Copy-Item -ToSession` | `scp` o `rsync` | | Esecuzione remota | `Invoke-Command -Session` | `ssh user@host 'cmd'` | diff --git a/docs/WINDOWS-TEMPLATE-SETUP.md b/docs/WINDOWS-TEMPLATE-SETUP.md index 360d76d..14fb432 100644 --- a/docs/WINDOWS-TEMPLATE-SETUP.md +++ b/docs/WINDOWS-TEMPLATE-SETUP.md @@ -65,7 +65,8 @@ winrm quickconfig -q ```powershell Enable-PSRemoting -Force -SkipNetworkProfileCheck Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force -Set-Item WSMan:\localhost\Service\AllowUnencrypted $true -Force +# AllowUnencrypted left false — Deploy post-install.ps1 creates HTTPS/5986 listener +# Basic auth is secure over HTTPS; host connects on port 5986 with -SkipCACheck Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force ``` @@ -110,12 +111,12 @@ cd N:\Code\Workspace\Local-CI-CD-System\template Ogni step ha validazione `Assert-Step` (throw on failure, `[OK]` su pass): -1. **Pre-flight**: script dir presente, `Setup-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5985 reachable -2. **Configura host WinRM client**: `AllowUnencrypted=true`, aggiunge `$VMIPAddress` ai `TrustedHosts` - - Salva valori precedenti, ripristinati nel `finally` (host posture invariata) -3. **Validazione host WinRM**: `AllowUnencrypted=true`, `TrustedHosts` include IP -4. **Test connettività**: `Test-WSMan -Authentication Basic` con credenziali admin -5. **Apre PSSession** verso la VM +1. **Pre-flight**: script dir presente, `Setup-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5986 reachable +2. **Configura host WinRM client**: aggiunge `$VMIPAddress` ai `TrustedHosts` (no `AllowUnencrypted` — HTTPS non lo richiede) + - Salva valore precedente, ripristinato nel `finally` (host posture invariata) +3. **Validazione host WinRM**: `TrustedHosts` include IP +4. **Test connettività**: `Test-WSMan -Port 5986 -UseSSL -Authentication Basic` con credenziali admin +5. **Apre PSSession** `-UseSSL -Port 5986` verso la VM 6. **Crea `C:\CI`** in guest, valida creazione 7. **Copia `Setup-WinBuild2025.ps1`** in `C:\CI\Setup-WinBuild2025.ps1`, valida size match 8. **Esegue `Setup-WinBuild2025.ps1`** dentro la VM (vedi Fase C) @@ -193,7 +194,7 @@ Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover | ---- | ---------- | ------------------------------------------------------------------------------ | | 1 | Assert | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` | | 2 | Assert | GPO DisableAntiSpyware=1 (set da Deploy) | -| 3 | Assert | WinRM Running+Automatic, AllowUnencrypted=true, Auth/Basic=true, MaxMem≥2048 | +| 3 | Assert | WinRM Running+Automatic, AllowUnencrypted=false (HTTPS-only), Auth/Basic=true, MaxMem≥2048 | | 4 | Operativo | User exists, enabled, PasswordNeverExpires, member of Administrators | | 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 | | 5 | Operativo | Ogni dir Test-Path -PathType Container | diff --git a/scripts/Get-BuildArtifacts.ps1 b/scripts/Get-BuildArtifacts.ps1 index 73da801..928ac82 100644 --- a/scripts/Get-BuildArtifacts.ps1 +++ b/scripts/Get-BuildArtifacts.ps1 @@ -39,7 +39,7 @@ [CmdletBinding()] param( [Parameter(Mandatory)] - [ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] + [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')] [string] $IPAddress, [Parameter(Mandatory)] @@ -67,10 +67,13 @@ $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationC Write-Host "[Get-BuildArtifacts] Connecting to VM at $IPAddress..." $session = New-PSSession ` - -ComputerName $IPAddress ` - -Credential $Credential ` - -SessionOption $sessionOptions ` - -ErrorAction Stop + -ComputerName $IPAddress ` + -Credential $Credential ` + -SessionOption $sessionOptions ` + -UseSSL ` + -Port 5986 ` + -Authentication Basic ` + -ErrorAction Stop try { # ── Verify artifact exists in guest ────────────────────────────────── diff --git a/scripts/Invoke-CIJob.ps1 b/scripts/Invoke-CIJob.ps1 index 73c1e45..b68884e 100644 --- a/scripts/Invoke-CIJob.ps1 +++ b/scripts/Invoke-CIJob.ps1 @@ -103,7 +103,7 @@ param( [string] $ArtifactBaseDir = 'F:\CI\Artifacts', - [ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] + [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')] [string] $VMIPAddress = '', # if empty, auto-detected via vmrun getGuestIPAddress [string] $GuestCredentialTarget = 'BuildVMGuest', diff --git a/scripts/Invoke-RemoteBuild.ps1 b/scripts/Invoke-RemoteBuild.ps1 index 509a244..66438d1 100644 --- a/scripts/Invoke-RemoteBuild.ps1 +++ b/scripts/Invoke-RemoteBuild.ps1 @@ -49,7 +49,7 @@ [CmdletBinding()] param( [Parameter(Mandatory)] - [ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] + [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')] [string] $IPAddress, [Parameter(Mandatory)] @@ -85,10 +85,13 @@ $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationC Write-Host "[Invoke-RemoteBuild] Connecting to VM at $IPAddress..." $session = New-PSSession ` - -ComputerName $IPAddress ` - -Credential $Credential ` - -SessionOption $sessionOptions ` - -ErrorAction Stop + -ComputerName $IPAddress ` + -Credential $Credential ` + -SessionOption $sessionOptions ` + -UseSSL ` + -Port 5986 ` + -Authentication Basic ` + -ErrorAction Stop try { # ── Ensure working directories exist on guest ──────────────────────── diff --git a/scripts/Wait-VMReady.ps1 b/scripts/Wait-VMReady.ps1 index 48ce63d..3e602bc 100644 --- a/scripts/Wait-VMReady.ps1 +++ b/scripts/Wait-VMReady.ps1 @@ -40,7 +40,7 @@ param( [string] $VMPath, [Parameter(Mandatory)] - [ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] + [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')] [string] $IPAddress, [ValidateRange(30, 600)] @@ -52,7 +52,7 @@ param( [string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe', # Skip ICMP ping phase (phase 2). Useful when the guest firewall blocks ICMP - # but WinRM (TCP 5985) is open. Phase 1 (vmrun state) and phase 3 (WinRM) + # but WinRM (TCP 5986) is open. Phase 1 (vmrun state) and phase 3 (WinRM) # are still checked. [switch] $SkipPing ) @@ -68,6 +68,7 @@ $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $attempt = 0 $phase = 'vmrun-state' # tracks current phase for informative output $lastPhase = '' +$wsmOpt = New-WSManSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck Write-Host "[Wait-VMReady] Waiting for VM ready (timeout: ${TimeoutSeconds}s, IP: $IPAddress)..." @@ -109,7 +110,7 @@ while ((Get-Date) -lt $deadline) { # ── Phase 3: WinRM responsive ──────────────────────────────────────── try { - Test-WSMan -ComputerName $IPAddress -ErrorAction Stop | Out-Null + Test-WSMan -ComputerName $IPAddress -Port 5986 -UseSSL -SessionOption $wsmOpt -ErrorAction Stop | Out-Null # Success — VM is ready Write-Host "[Wait-VMReady] VM ready after ~$([int]($attempt * $PollIntervalSeconds))s (attempt $attempt)." return @@ -117,7 +118,7 @@ while ((Get-Date) -lt $deadline) { catch { $phase = 'winrm' if ($lastPhase -ne $phase) { - Write-Host "[Wait-VMReady] Phase 3/3: ping OK, waiting for WinRM on port 5985..." + Write-Host "[Wait-VMReady] Phase 3/3: ping OK, waiting for WinRM on port 5986 (HTTPS)..." $lastPhase = $phase } Start-Sleep -Seconds $PollIntervalSeconds diff --git a/template/Deploy-WinBuild2025.ps1 b/template/Deploy-WinBuild2025.ps1 index 46df1bc..8002369 100644 --- a/template/Deploy-WinBuild2025.ps1 +++ b/template/Deploy-WinBuild2025.ps1 @@ -43,8 +43,8 @@ them after applying updates (§7.2 Strategy B) - Windows Firewall all profiles disabled (VMnet8 NAT lab; Setup Step 1 validates) - RDP enabled (no NLA, lab only) + firewall rules for RDP/ICMP/WinRM-HTTPS - - WinRM HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth, - AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25 + - WinRM HTTPS 5986 only (self-signed cert), Basic auth, AllowUnencrypted=false, + TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25 - ICMPv4 inbound allowed - Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive), DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed; @@ -617,17 +617,17 @@ Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyConti New-NetFirewallRule -DisplayName 'ICMPv4-In' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -ErrorAction SilentlyContinue | Out-Null L 'RDP + ICMP allowed' -# ── WinRM HTTP + HTTPS, Basic+Unencrypted, TrustedHosts=* (lab only) ───── +# ── WinRM HTTPS-only/5986, Basic auth over HTTPS, TrustedHosts=* (lab only) ─ +# AllowUnencrypted intentionally left false — all WinRM traffic uses HTTPS/5986. Enable-PSRemoting -Force -SkipNetworkProfileCheck | Out-Null -winrm set winrm/config/service '@{AllowUnencrypted="true"}' | Out-Null winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null winrm set winrm/config/client/auth '@{Basic="true"}' | Out-Null winrm set winrm/config/client '@{TrustedHosts="*"}' | Out-Null try { `$cert = New-SelfSignedCertificate -DnsName `$env:COMPUTERNAME -CertStoreLocation Cert:\LocalMachine\My New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint `$cert.Thumbprint -Force | Out-Null - New-NetFirewallRule -DisplayName 'WinRM-HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Any | Out-Null - L 'WinRM HTTPS listener configured' + New-NetFirewallRule -DisplayName 'WinRM-HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Any -RemoteAddress '192.168.79.0/24' | Out-Null + L 'WinRM HTTPS listener configured (port 5986, restricted to 192.168.79.0/24)' } catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" } winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>`$null @@ -1107,8 +1107,7 @@ Write-Host " Admin user : $AdminUser" Write-Host " Snapshot : $SnapshotName" Write-Host "" Write-Host " RDP : mstsc /v:$guestIP" -Write-Host " WinRM HTTP : 5985" -Write-Host " WinRM HTTPS : 5986 (self-signed)" +Write-Host " WinRM HTTPS : 5986 (self-signed, AllowUnencrypted=false)" Write-Host "" Write-Host " Next steps (from template\ dir):" Write-Host " .\Validate-DeployState.ps1 -VMIPAddress $guestIP" diff --git a/template/Prepare-WinBuild2025.ps1 b/template/Prepare-WinBuild2025.ps1 index daf635a..815998f 100644 --- a/template/Prepare-WinBuild2025.ps1 +++ b/template/Prepare-WinBuild2025.ps1 @@ -5,10 +5,10 @@ .DESCRIPTION Automates the template VM provisioning from the host machine: - 1. Pre-flight validation: script presence, IP octet range, TCP/5985 reachable - 2. Configures host WinRM client (AllowUnencrypted, TrustedHosts) — restored on exit - 3. Validates host WinRM client settings applied correctly - 4. Tests WinRM connectivity (Test-WSMan) — fails fast with actionable message + 1. Pre-flight validation: script presence, IP octet range, TCP/5986 reachable + 2. Configures host WinRM client (TrustedHosts only, HTTPS needs no AllowUnencrypted) — restored on exit + 3. Validates host WinRM TrustedHosts applied correctly + 4. Tests WinRM HTTPS connectivity (Test-WSMan -UseSSL -Port 5986) — fails fast with actionable message 5. Opens PSSession to the VM 6. Ensures C:\CI exists on guest; validates creation 7. Copies Setup-WinBuild2025.ps1 into the VM; validates file present + size match @@ -17,7 +17,7 @@ 9. Handles the reboot-required case (exit 3010) and optionally re-runs 10. Post-setup remote validation: 9 guest-state checks via a single Invoke-Command (WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs) - 11. Restores host WinRM client settings in finally block + 11. Restores host TrustedHosts in finally block Every validation step uses Assert-Step: prints [OK] on success, throws on failure. The snapshot should only be taken after this script exits 0. @@ -227,18 +227,18 @@ Assert-Step 'PreFlight' "VMIPAddress octets in 0..255" { # WinRM readiness — retry loop (longer timeout if VM was just powered on by this script) $winrmTimeoutSec = if ($vmWasPoweredOn) { 180 } else { 20 } -Write-Host "[Prepare] Waiting for WinRM on $VMIPAddress`:5985 (timeout ${winrmTimeoutSec}s)..." +Write-Host "[Prepare] Waiting for WinRM on $VMIPAddress`:5986 (timeout ${winrmTimeoutSec}s)..." $winrmDeadline = [datetime]::UtcNow.AddSeconds($winrmTimeoutSec) $winrmReady = $false while ([datetime]::UtcNow -lt $winrmDeadline) { - if (Test-NetConnection -ComputerName $VMIPAddress -Port 5985 ` + if (Test-NetConnection -ComputerName $VMIPAddress -Port 5986 ` -InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) { $winrmReady = $true; break } Start-Sleep -Seconds 5 Write-Host " ... waiting for WinRM..." } -Assert-Step 'PreFlight' "VMIPAddress $VMIPAddress reachable on TCP/5985" { $winrmReady } +Assert-Step 'PreFlight' "VMIPAddress $VMIPAddress reachable on TCP/5986" { $winrmReady } # Prompt for BuildPassword if not supplied — never use a hardcoded default. if ($BuildPassword -eq '') { @@ -260,60 +260,50 @@ else { } $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck +$wsmOpt = New-WSManSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -# ── Configure HOST-side WinRM client (required for Basic auth over HTTP) ────── -# AllowUnencrypted is needed for HTTP/5985 Basic auth. We save the prior value -# and restore it in the finally block so this script doesn't permanently change -# the host security posture. -Write-Host "[Prepare] Configuring host WinRM client for unencrypted Basic auth (will be restored on exit)..." -$prevAllowUnencrypted = $null -$prevTrustedHosts = $null +# ── Configure HOST-side WinRM client (TrustedHosts for HTTPS Basic auth) ────── +# HTTPS/5986 does not require AllowUnencrypted on the host side. Only TrustedHosts +# needs appending so PS accepts the non-domain VM. Restored in the finally block. +$prevTrustedHosts = $null try { - $prevAllowUnencrypted = (Get-Item WSMan:\localhost\Client\AllowUnencrypted -ErrorAction Stop).Value - $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value - Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force -ErrorAction Stop + $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value # Add VM IP to TrustedHosts if not already present if ($prevTrustedHosts -ne '*' -and $prevTrustedHosts -notlike "*$VMIPAddress*") { $newTrusted = if ($prevTrustedHosts -eq '') { $VMIPAddress } else { "$prevTrustedHosts,$VMIPAddress" } Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newTrusted -Force -ErrorAction Stop } - Write-Host "[Prepare] Host WinRM client configured." - # Validation - Assert-Step 'HostWinRM' 'Client/AllowUnencrypted=true' { - (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value -eq 'true' - } + Write-Host "[Prepare] Host TrustedHosts configured." Assert-Step 'HostWinRM' "TrustedHosts includes $VMIPAddress (or '*')" { $th = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $th -eq '*' -or $th -like "*$VMIPAddress*" } } catch { - Write-Warning "[Prepare] Could not configure host WinRM client settings (need elevation?)." + Write-Warning "[Prepare] Could not configure host WinRM TrustedHosts (need elevation?)." Write-Warning "Run once in an elevated PowerShell on the HOST:" - Write-Warning " Set-Item WSMan:\localhost\Client\AllowUnencrypted `$true -Force" Write-Warning " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force" Write-Warning "Then re-run this script." exit 1 } -Write-Host "[Prepare] Testing WinRM connectivity to $VMIPAddress..." +Write-Host "[Prepare] Testing WinRM HTTPS connectivity to $VMIPAddress`:5986..." try { - Test-WSMan -ComputerName $VMIPAddress -Credential $credential ` + Test-WSMan -ComputerName $VMIPAddress -Port 5986 -UseSSL ` + -SessionOption $wsmOpt -Credential $credential ` -Authentication Basic -ErrorAction Stop | Out-Null - Write-Host "[Prepare] WinRM reachable." + Write-Host "[Prepare] WinRM HTTPS reachable." } catch { Write-Error @" -[Prepare] Cannot reach $VMIPAddress via WinRM. +[Prepare] Cannot reach $VMIPAddress via WinRM HTTPS (port 5986). Ensure: - The VM is running and on VMnet8 (NAT) with internet access - - WinRM is enabled inside the VM (run as Administrator in VM): - winrm quickconfig -q - Enable-PSRemoting -Force -SkipNetworkProfileCheck - Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force - - Windows Firewall allows port 5985 inbound + - WinRM HTTPS is enabled inside the VM — Deploy-WinBuild2025.ps1 post-install.ps1 + must have run successfully (creates self-signed cert + HTTPS listener on 5986) + - Windows Firewall allows port 5986 inbound (or firewall is disabled on the guest) - The IP is correct (check ipconfig inside the VM) Error: $_ @@ -324,11 +314,13 @@ Error: $_ # ── Open session ────────────────────────────────────────────────────────────── Write-Host "[Prepare] Opening PSSession..." $session = New-PSSession ` - -ComputerName $VMIPAddress ` - -Credential $credential ` - -SessionOption $sessionOptions ` + -ComputerName $VMIPAddress ` + -Credential $credential ` + -SessionOption $sessionOptions ` + -UseSSL ` + -Port 5986 ` -Authentication Basic ` - -ErrorAction Stop + -ErrorAction Stop try { # ── Copy Setup-WinBuild2025.ps1 into the VM ─────────────────────────────── @@ -423,7 +415,9 @@ try { while ((Get-Date) -lt $deadline) { Start-Sleep -Seconds 10 try { - $r = Test-WSMan -ComputerName $IP -Credential $Cred ` + $wsm = New-WSManSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck + $r = Test-WSMan -ComputerName $IP -Port 5986 -UseSSL ` + -SessionOption $wsm -Credential $Cred ` -Authentication Basic -ErrorAction Stop if ($r) { return $true } } catch { } @@ -481,7 +475,8 @@ try { } Write-Host "[Prepare] Guest WinRM ready, reopening session..." $session = New-PSSession -ComputerName $VMIPAddress -Credential $credential ` - -SessionOption $sessionOptions -Authentication Basic -ErrorAction Stop + -SessionOption $sessionOptions -UseSSL -Port 5986 ` + -Authentication Basic -ErrorAction Stop } # ── Post-setup remote validation ────────────────────────────────────────── @@ -670,12 +665,9 @@ try { finally { Remove-PSSession $session -ErrorAction SilentlyContinue - # Restore host WinRM client settings to their original values - if ($null -ne $prevAllowUnencrypted) { - Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnencrypted -Force -ErrorAction SilentlyContinue - } + # Restore host TrustedHosts to its original value if ($null -ne $prevTrustedHosts) { Set-Item WSMan:\localhost\Client\TrustedHosts -Value $prevTrustedHosts -Force -ErrorAction SilentlyContinue } - Write-Host "[Prepare] Host WinRM client settings restored." + Write-Host "[Prepare] Host TrustedHosts restored." } diff --git a/template/Setup-WinBuild2025.ps1 b/template/Setup-WinBuild2025.ps1 index 80e5ea3..8a980dc 100644 --- a/template/Setup-WinBuild2025.ps1 +++ b/template/Setup-WinBuild2025.ps1 @@ -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 -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 '' -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--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. diff --git a/template/Validate-DeployState.ps1 b/template/Validate-DeployState.ps1 index 8f835fc..b04fa3b 100644 --- a/template/Validate-DeployState.ps1 +++ b/template/Validate-DeployState.ps1 @@ -42,11 +42,9 @@ $cred = New-Object System.Management.Automation.PSCredential( ) $so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value try { - Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force $cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) { $newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress } @@ -56,7 +54,7 @@ try { Write-Host "`nValidating Deploy state on $VMIPAddress..." -ForegroundColor Cyan $checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` - -Authentication Basic -SessionOption $so -ScriptBlock { + -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock { $results = [System.Collections.Generic.List[PSCustomObject]]::new() @@ -82,8 +80,8 @@ try { # ── WinRM ───────────────────────────────────────────────────────────── Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' } Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' } - Chk 'WinRM AllowUnencrypted=true' { - (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' + Chk 'WinRM AllowUnencrypted=false (HTTPS-only)' { + (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'false' } Chk 'WinRM Auth/Basic=true' { (Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true' @@ -182,6 +180,5 @@ try { } } finally { - Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue - Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue + Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue } diff --git a/template/Validate-SetupState.ps1 b/template/Validate-SetupState.ps1 index e097d73..6b5458d 100644 --- a/template/Validate-SetupState.ps1 +++ b/template/Validate-SetupState.ps1 @@ -63,11 +63,9 @@ $cred = New-Object System.Management.Automation.PSCredential( ) $so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value try { - Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force $cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) { $newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress } @@ -77,7 +75,7 @@ try { Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan $checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` - -Authentication Basic -SessionOption $so -ScriptBlock { + -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock { param($BuildUsername, $DotNetChannel, $PythonVersion) @@ -109,8 +107,8 @@ try { # WinRM Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' } Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' } - Chk 'WinRM AllowUnencrypted=true' { - (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' + Chk 'WinRM AllowUnencrypted=false (HTTPS-only)' { + (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'false' } Chk 'WinRM Auth/Basic=true' { (Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true' @@ -270,7 +268,7 @@ try { Write-Host "Applying remediation for $($failedNames.Count) failed check(s)..." -ForegroundColor Yellow Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` - -Authentication Basic -SessionOption $so -ScriptBlock { + -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock { param([string[]] $FailedNames, [string] $AdminPassword) @@ -296,7 +294,7 @@ try { # ── Re-run checks ───────────────────────────────────────────────────────── Write-Host "`nRe-validating after remediation..." -ForegroundColor Cyan $checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` - -Authentication Basic -SessionOption $so -ScriptBlock { + -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock { param($BuildUsername, $DotNetChannel, $PythonVersion) $results = [System.Collections.Generic.List[PSCustomObject]]::new() @@ -310,7 +308,7 @@ try { Chk 'Defender GPO DisableAntiSpyware=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' -Name DisableAntiSpyware -EA Stop).DisableAntiSpyware -eq 1 } Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' } Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' } - Chk 'WinRM AllowUnencrypted=true' { (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' } + Chk 'WinRM AllowUnencrypted=false (HTTPS-only)' { (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'false' } Chk 'WinRM Auth/Basic=true' { (Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true' } Chk 'WinRM MaxMemoryPerShellMB >= 2048' { [int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048 } $polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' @@ -369,6 +367,5 @@ try { } } finally { - Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue - Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue + Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue }