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
+1 -1
View File
@@ -16,7 +16,7 @@
- Gitea self-hosted — `http://10.10.20.11:3100` - Gitea self-hosted — `http://10.10.20.11:3100`
- act_runner v1.0.2 — Windows service su host, label `windows-build` - act_runner v1.0.2 — Windows service su host, label `windows-build`
- Build VMs: subnet VMnet8 NAT — `192.168.79.0/24` - 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 ### Architettura implementata
1. Gitea Actions enqueue job → act_runner lo prende 1. Gitea Actions enqueue job → act_runner lo prende
+5 -4
View File
@@ -54,7 +54,7 @@ Ogni build gira in una VM Windows Server 2025 **usa e getta**, ripristinata dall
| .NET SDK | 10.0.203 | | .NET SDK | 10.0.203 |
| Python | 3.13.3 | | Python | 3.13.3 |
| Rete | VMnet8 NAT — `192.168.79.0/24` | | 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 ## Note di sicurezza
WinRM in modalità HTTP/Basic è **accettabile solo in ambiente lab isolato**. WinRM usa **HTTPS/5986** con certificato self-signed e Basic auth.
Per ambienti di produzione o condivisi, migrare a HTTPS/5986 con certificato self-signed `-SkipCACheck`/`-SkipCNCheck` sono accettabili in lab isolato (il cert non è di una CA trusted).
e rimuovere `AllowUnencrypted=true`. Vedi [docs/BEST-PRACTICES.md](docs/BEST-PRACTICES.md). Per ambienti condivisi o di produzione, usare un certificato CA valida e rimuovere `-SkipCACheck`.
Vedi [docs/BEST-PRACTICES.md](docs/BEST-PRACTICES.md).
+30 -37
View File
@@ -65,7 +65,7 @@
```powershell ```powershell
Enable-PSRemoting -Force -SkipNetworkProfileCheck Enable-PSRemoting -Force -SkipNetworkProfileCheck
Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force 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 Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
``` ```
- [x] **Attiva Windows Server 2025** (prima dello snapshot — i linked clone ereditano l'attivazione): - [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. 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`): 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 - Host WinRM: AllowUnencrypted, TrustedHosts
- Guest prep: `C:\CI` esiste, file copiato, size match - Guest prep: `C:\CI` esiste, file copiato, size match
- Post-setup remoto (9 check): WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs - Post-setup remoto (9 check): WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs
@@ -148,25 +148,22 @@
## 1. Sicurezza & hardening ## 1. Sicurezza & hardening
### 1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986 ### 1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986 — COMPLETATO 2026-05-10
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.
**Azioni**: **Azioni**:
- [ ] Generare cert self-signed nel template *prima* dello snapshot. - [x] Generare cert self-signed nel template *prima* dello snapshot (già in Deploy post-install.ps1).
- [ ] Sostituire `New-PSSession -ComputerName $ip` con `-UseSSL -Port 5986` in `Invoke-RemoteBuild.ps1` e `Get-BuildArtifacts.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`.
- [ ] Rimuovere `AllowUnencrypted=true` dal `Setup-WinBuild2025.ps1`. - [x] Rimuovere `AllowUnencrypted=true` da Deploy post-install.ps1 e Setup-WinBuild2025.ps1 (assertion → false).
- [ ] Tenere `-SkipCACheck` lato host: per cert lab self-signed è accettabile, ma documentare la motivazione inline. - [x] `-SkipCACheck`/`-SkipCNCheck`/`-SkipRevocationCheck` mantenuti nei `New-PSSessionOption` e `New-WSManSessionOption` (cert lab self-signed — documentato inline).
- [ ] Regole firewall — limitare WinRM alla subnet build VM (`192.168.79.0/24` — VMnet8 NAT). - [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) ### 1.2 [P0] Restringere `TrustedHosts` lato host (audit-only — già coperto)
File: nessuno script imposta `*` sull'host. Stato corrente: File: nessuno script imposta `*` sull'host. Stato corrente:
- `Setup-Host.ps1` non tocca TrustedHosts (verificato 2026-05-10) - `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 - [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 **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 Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate
``` ```
### 1.3 [P0] Pinning hash SHA256 degli installer ### 1.3 [P0] Pinning hash SHA256 degli installer — STRUTTURA AGGIUNTA 2026-05-10
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).
**Motivazione**: Python, `dotnet-install.ps1`, `vs_buildtools.exe` vengono scaricati senza `$script:Hashes` + `Assert-Hash` aggiunti a [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
verifica integrità. Un MITM nella rete dell'host (anche temporaneo) può iniettare binari `Assert-Hash` viene chiamato dopo ogni download (dotnet-install.ps1, python installer, vs_buildtools.exe).
compromessi nel template, che poi viene snapshottato e usato da *ogni* build. Se l'hash è `''` viene emesso un warning ma l'esecuzione continua (non-breaking).
Pattern minimale: **Azione residua — OBBLIGATORIA prima del prossimo refresh snapshot**:
```powershell - [ ] Scaricare i tre file, calcolare hash, riempire `$script:Hashes` in Setup-WinBuild2025.ps1:
$expected = '6F25A7DF...' # SHA256 pinnato per la versione esatta ```powershell
$actual = (Get-FileHash $pyInstallerPath -Algorithm SHA256).Hash.ToLower() # Inside or outside VM — qualsiasi PowerShell:
if ($actual -ne $expected.ToLower()) { throw "Hash mismatch: $actual" } (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']
Aggiungere un blocco hash per ogni download e bloccarli in costanti `$script:Hashes`. (Get-FileHash 'vs_buildtools.exe' -Algorithm SHA256).Hash # → $script:Hashes['VSBuildToolsBootstrapper']
Aggiornare al cambio versione (è raro). ```
- [ ] Verificare che `Assert-Hash` throwi correttamente su hash errato (test con hash fake).
### 1.4 [P1] Validazione IP per-ottetto ### 1.4 [P1] Validazione IP per-ottetto — COMPLETATO 2026-05-10
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).
**Motivazione**: `'^(\d{1,3}\.){3}\d{1,3}$'` accetta `999.999.999.999`. Sostituire con regex Regex per-ottetto sostituita in tutti e 4 i file:
per-ottetto: `'^((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)$'`
```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)$' Estrazione in `scripts/_Common.psm1` — vedi §5.2 (P2, backlog).
[ValidatePattern($ipv4)]
```
Estrarre la costante in un modulo condiviso `scripts/_Common.psm1` per evitare drift fra i 4 file (vedi §5.2).
### 1.5 [P1] PAT mai persistito quando si abilita `-UseGitClone` ### 1.5 [P1] PAT mai persistito quando si abilita `-UseGitClone`
Requisiti di sicurezza per l'implementazione del clone in-VM. La sequenza operativa Requisiti di sicurezza per l'implementazione del clone in-VM. La sequenza operativa
+4 -4
View File
@@ -53,7 +53,7 @@ The runner host **never executes build tools directly**. Its only role is orches
│ │ ├── Clone_Job_002.vmx │ │ │ │ ├── Clone_Job_002.vmx │ │
│ │ └── Clone_Job_003.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 ### WinRM / PowerShell Remoting
- Default transport for remote build execution inside VMs - Default transport for remote build execution inside VMs
- Port: **5985** (HTTP, in-lab use); migrate to 5986/HTTPS for hardened setups - Port: **5986** (HTTPS, self-signed cert, `AllowUnencrypted=false`)
- Authentication: **Basic** (unencrypted — acceptable for isolated lab) - Authentication: **Basic over HTTPS** (`-SkipCACheck` for lab self-signed cert)
- Used for: - Used for:
- Transferring source: `Compress-Archive` on host → WinRM copy → `Expand-Archive` in guest - Transferring source: `Compress-Archive` on host → WinRM copy → `Expand-Archive` in guest
- Running build commands (`Invoke-Command`) - Running build commands (`Invoke-Command`)
@@ -133,7 +133,7 @@ Build VMs:
... ...
Clone_Job_N → 192.168.79.1xx 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. VMs have internet access via NAT — required for pip/nuget during build.
``` ```
+12 -33
View File
@@ -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: `Deploy-WinBuild2025.ps1` post-install.ps1 crea un certificato self-signed e configura
- Build VMs sono su VMnet8 NAT (192.168.79.0/24) — raggiungibili dall'host, internet via NAT il listener HTTPS/5986 **prima** dello snapshot `BaseClean`. `AllowUnencrypted=false`.
- No external traffic reaches port 5985
- Credentials are managed via Windows Credential Manager
### 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 ```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 $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 -Credential $cred -SessionOption $sessionOptions
``` ```
> `-SkipCACheck` is acceptable for a self-signed cert in an isolated lab. Do NOT > `-SkipCACheck`/`-SkipCNCheck` sono accettabili per un cert self-signed in lab isolato.
> use this against externally accessible machines. > 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: 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) - Internet via VMware NAT (for pip, nuget, npm at build time)
- Gitea server if on LAN reachable via NAT gateway - Gitea server if on LAN reachable via NAT gateway
+2 -2
View File
@@ -120,10 +120,10 @@ Allo `Setup-Host.ps1` exit 0:
3. **Configura host WinRM client** (necessario per Prepare-WinBuild2025.ps1): 3. **Configura host WinRM client** (necessario per Prepare-WinBuild2025.ps1):
```powershell ```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 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) 4. **Provisiona il template VM** — vedi [WINDOWS-TEMPLATE-SETUP.md](WINDOWS-TEMPLATE-SETUP.md)
+1 -1
View File
@@ -314,7 +314,7 @@ Equivalente di `Prepare-WinBuild2025.ps1` per Linux. Usa SSH invece di WinRM.
| Aspetto | Windows | Linux | | 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 | | Auth fluid | `ci_build` admin + UAC off + LATFP=1 | `ci_build` + sudoers NOPASSWD |
| Trasferimento file | `Copy-Item -ToSession` | `scp` o `rsync` | | Trasferimento file | `Copy-Item -ToSession` | `scp` o `rsync` |
| Esecuzione remota | `Invoke-Command -Session` | `ssh user@host 'cmd'` | | Esecuzione remota | `Invoke-Command -Session` | `ssh user@host 'cmd'` |
+9 -8
View File
@@ -65,7 +65,8 @@ winrm quickconfig -q
```powershell ```powershell
Enable-PSRemoting -Force -SkipNetworkProfileCheck Enable-PSRemoting -Force -SkipNetworkProfileCheck
Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force 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 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): 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 1. **Pre-flight**: script dir presente, `Setup-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5986 reachable
2. **Configura host WinRM client**: `AllowUnencrypted=true`, aggiunge `$VMIPAddress` ai `TrustedHosts` 2. **Configura host WinRM client**: aggiunge `$VMIPAddress` ai `TrustedHosts` (no `AllowUnencrypted` — HTTPS non lo richiede)
- Salva valori precedenti, ripristinati nel `finally` (host posture invariata) - Salva valore precedente, ripristinato nel `finally` (host posture invariata)
3. **Validazione host WinRM**: `AllowUnencrypted=true`, `TrustedHosts` include IP 3. **Validazione host WinRM**: `TrustedHosts` include IP
4. **Test connettività**: `Test-WSMan -Authentication Basic` con credenziali admin 4. **Test connettività**: `Test-WSMan -Port 5986 -UseSSL -Authentication Basic` con credenziali admin
5. **Apre PSSession** verso la VM 5. **Apre PSSession** `-UseSSL -Port 5986` verso la VM
6. **Crea `C:\CI`** in guest, valida creazione 6. **Crea `C:\CI`** in guest, valida creazione
7. **Copia `Setup-WinBuild2025.ps1`** in `C:\CI\Setup-WinBuild2025.ps1`, valida size match 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) 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` | | 1 | Assert | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` |
| 2 | Assert | GPO DisableAntiSpyware=1 (set da Deploy) | | 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 | | 4 | Operativo | User exists, enabled, PasswordNeverExpires, member of Administrators |
| 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 | | 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| 5 | Operativo | Ogni dir Test-Path -PathType Container | | 5 | Operativo | Ogni dir Test-Path -PathType Container |
+8 -5
View File
@@ -39,7 +39,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory)] [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, [string] $IPAddress,
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -67,10 +67,13 @@ $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationC
Write-Host "[Get-BuildArtifacts] Connecting to VM at $IPAddress..." Write-Host "[Get-BuildArtifacts] Connecting to VM at $IPAddress..."
$session = New-PSSession ` $session = New-PSSession `
-ComputerName $IPAddress ` -ComputerName $IPAddress `
-Credential $Credential ` -Credential $Credential `
-SessionOption $sessionOptions ` -SessionOption $sessionOptions `
-ErrorAction Stop -UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
try { try {
# ── Verify artifact exists in guest ────────────────────────────────── # ── Verify artifact exists in guest ──────────────────────────────────
+1 -1
View File
@@ -103,7 +103,7 @@ param(
[string] $ArtifactBaseDir = 'F:\CI\Artifacts', [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] $VMIPAddress = '', # if empty, auto-detected via vmrun getGuestIPAddress
[string] $GuestCredentialTarget = 'BuildVMGuest', [string] $GuestCredentialTarget = 'BuildVMGuest',
+8 -5
View File
@@ -49,7 +49,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory)] [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, [string] $IPAddress,
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -85,10 +85,13 @@ $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationC
Write-Host "[Invoke-RemoteBuild] Connecting to VM at $IPAddress..." Write-Host "[Invoke-RemoteBuild] Connecting to VM at $IPAddress..."
$session = New-PSSession ` $session = New-PSSession `
-ComputerName $IPAddress ` -ComputerName $IPAddress `
-Credential $Credential ` -Credential $Credential `
-SessionOption $sessionOptions ` -SessionOption $sessionOptions `
-ErrorAction Stop -UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
try { try {
# ── Ensure working directories exist on guest ──────────────────────── # ── Ensure working directories exist on guest ────────────────────────
+5 -4
View File
@@ -40,7 +40,7 @@ param(
[string] $VMPath, [string] $VMPath,
[Parameter(Mandatory)] [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, [string] $IPAddress,
[ValidateRange(30, 600)] [ValidateRange(30, 600)]
@@ -52,7 +52,7 @@ param(
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe', [string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
# Skip ICMP ping phase (phase 2). Useful when the guest firewall blocks ICMP # 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. # are still checked.
[switch] $SkipPing [switch] $SkipPing
) )
@@ -68,6 +68,7 @@ $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$attempt = 0 $attempt = 0
$phase = 'vmrun-state' # tracks current phase for informative output $phase = 'vmrun-state' # tracks current phase for informative output
$lastPhase = '' $lastPhase = ''
$wsmOpt = New-WSManSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
Write-Host "[Wait-VMReady] Waiting for VM ready (timeout: ${TimeoutSeconds}s, IP: $IPAddress)..." 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 ──────────────────────────────────────── # ── Phase 3: WinRM responsive ────────────────────────────────────────
try { 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 # Success — VM is ready
Write-Host "[Wait-VMReady] VM ready after ~$([int]($attempt * $PollIntervalSeconds))s (attempt $attempt)." Write-Host "[Wait-VMReady] VM ready after ~$([int]($attempt * $PollIntervalSeconds))s (attempt $attempt)."
return return
@@ -117,7 +118,7 @@ while ((Get-Date) -lt $deadline) {
catch { catch {
$phase = 'winrm' $phase = 'winrm'
if ($lastPhase -ne $phase) { 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 $lastPhase = $phase
} }
Start-Sleep -Seconds $PollIntervalSeconds Start-Sleep -Seconds $PollIntervalSeconds
+7 -8
View File
@@ -43,8 +43,8 @@
them after applying updates (§7.2 Strategy B) them after applying updates (§7.2 Strategy B)
- Windows Firewall all profiles disabled (VMnet8 NAT lab; Setup Step 1 validates) - 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 - RDP enabled (no NLA, lab only) + firewall rules for RDP/ICMP/WinRM-HTTPS
- WinRM HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth, - WinRM HTTPS 5986 only (self-signed cert), Basic auth, AllowUnencrypted=false,
AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25 TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25
- ICMPv4 inbound allowed - ICMPv4 inbound allowed
- Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive), - Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive),
DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed; 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 New-NetFirewallRule -DisplayName 'ICMPv4-In' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -ErrorAction SilentlyContinue | Out-Null
L 'RDP + ICMP allowed' 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 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/service/auth '@{Basic="true"}' | Out-Null
winrm set winrm/config/client/auth '@{Basic="true"}' | Out-Null winrm set winrm/config/client/auth '@{Basic="true"}' | Out-Null
winrm set winrm/config/client '@{TrustedHosts="*"}' | Out-Null winrm set winrm/config/client '@{TrustedHosts="*"}' | Out-Null
try { try {
`$cert = New-SelfSignedCertificate -DnsName `$env:COMPUTERNAME -CertStoreLocation Cert:\LocalMachine\My `$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-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 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' L 'WinRM HTTPS listener configured (port 5986, restricted to 192.168.79.0/24)'
} catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" } } catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" }
winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>`$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 " Snapshot : $SnapshotName"
Write-Host "" Write-Host ""
Write-Host " RDP : mstsc /v:$guestIP" Write-Host " RDP : mstsc /v:$guestIP"
Write-Host " WinRM HTTP : 5985" Write-Host " WinRM HTTPS : 5986 (self-signed, AllowUnencrypted=false)"
Write-Host " WinRM HTTPS : 5986 (self-signed)"
Write-Host "" Write-Host ""
Write-Host " Next steps (from template\ dir):" Write-Host " Next steps (from template\ dir):"
Write-Host " .\Validate-DeployState.ps1 -VMIPAddress $guestIP" Write-Host " .\Validate-DeployState.ps1 -VMIPAddress $guestIP"
+37 -45
View File
@@ -5,10 +5,10 @@
.DESCRIPTION .DESCRIPTION
Automates the template VM provisioning from the host machine: Automates the template VM provisioning from the host machine:
1. Pre-flight validation: script presence, IP octet range, TCP/5985 reachable 1. Pre-flight validation: script presence, IP octet range, TCP/5986 reachable
2. Configures host WinRM client (AllowUnencrypted, TrustedHosts) restored on exit 2. Configures host WinRM client (TrustedHosts only, HTTPS needs no AllowUnencrypted) restored on exit
3. Validates host WinRM client settings applied correctly 3. Validates host WinRM TrustedHosts applied correctly
4. Tests WinRM connectivity (Test-WSMan) fails fast with actionable message 4. Tests WinRM HTTPS connectivity (Test-WSMan -UseSSL -Port 5986) fails fast with actionable message
5. Opens PSSession to the VM 5. Opens PSSession to the VM
6. Ensures C:\CI exists on guest; validates creation 6. Ensures C:\CI exists on guest; validates creation
7. Copies Setup-WinBuild2025.ps1 into the VM; validates file present + size match 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 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 10. Post-setup remote validation: 9 guest-state checks via a single Invoke-Command
(WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs) (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. Every validation step uses Assert-Step: prints [OK] on success, throws on failure.
The snapshot should only be taken after this script exits 0. 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) # WinRM readiness — retry loop (longer timeout if VM was just powered on by this script)
$winrmTimeoutSec = if ($vmWasPoweredOn) { 180 } else { 20 } $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) $winrmDeadline = [datetime]::UtcNow.AddSeconds($winrmTimeoutSec)
$winrmReady = $false $winrmReady = $false
while ([datetime]::UtcNow -lt $winrmDeadline) { 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) { -InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) {
$winrmReady = $true; break $winrmReady = $true; break
} }
Start-Sleep -Seconds 5 Start-Sleep -Seconds 5
Write-Host " ... waiting for WinRM..." 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. # Prompt for BuildPassword if not supplied — never use a hardcoded default.
if ($BuildPassword -eq '') { if ($BuildPassword -eq '') {
@@ -260,60 +260,50 @@ else {
} }
$sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$wsmOpt = New-WSManSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
# ── Configure HOST-side WinRM client (required for Basic auth over HTTP) ────── # ── Configure HOST-side WinRM client (TrustedHosts for HTTPS Basic auth) ──────
# AllowUnencrypted is needed for HTTP/5985 Basic auth. We save the prior value # HTTPS/5986 does not require AllowUnencrypted on the host side. Only TrustedHosts
# and restore it in the finally block so this script doesn't permanently change # needs appending so PS accepts the non-domain VM. Restored in the finally block.
# the host security posture. $prevTrustedHosts = $null
Write-Host "[Prepare] Configuring host WinRM client for unencrypted Basic auth (will be restored on exit)..."
$prevAllowUnencrypted = $null
$prevTrustedHosts = $null
try { try {
$prevAllowUnencrypted = (Get-Item WSMan:\localhost\Client\AllowUnencrypted -ErrorAction Stop).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force -ErrorAction Stop
# Add VM IP to TrustedHosts if not already present # Add VM IP to TrustedHosts if not already present
if ($prevTrustedHosts -ne '*' -and $prevTrustedHosts -notlike "*$VMIPAddress*") { if ($prevTrustedHosts -ne '*' -and $prevTrustedHosts -notlike "*$VMIPAddress*") {
$newTrusted = if ($prevTrustedHosts -eq '') { $VMIPAddress } else { "$prevTrustedHosts,$VMIPAddress" } $newTrusted = if ($prevTrustedHosts -eq '') { $VMIPAddress } else { "$prevTrustedHosts,$VMIPAddress" }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newTrusted -Force -ErrorAction Stop Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newTrusted -Force -ErrorAction Stop
} }
Write-Host "[Prepare] Host WinRM client configured." Write-Host "[Prepare] Host TrustedHosts configured."
# Validation
Assert-Step 'HostWinRM' 'Client/AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value -eq 'true'
}
Assert-Step 'HostWinRM' "TrustedHosts includes $VMIPAddress (or '*')" { Assert-Step 'HostWinRM' "TrustedHosts includes $VMIPAddress (or '*')" {
$th = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $th = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
$th -eq '*' -or $th -like "*$VMIPAddress*" $th -eq '*' -or $th -like "*$VMIPAddress*"
} }
} }
catch { 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 "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 " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force"
Write-Warning "Then re-run this script." Write-Warning "Then re-run this script."
exit 1 exit 1
} }
Write-Host "[Prepare] Testing WinRM connectivity to $VMIPAddress..." Write-Host "[Prepare] Testing WinRM HTTPS connectivity to $VMIPAddress`:5986..."
try { try {
Test-WSMan -ComputerName $VMIPAddress -Credential $credential ` Test-WSMan -ComputerName $VMIPAddress -Port 5986 -UseSSL `
-SessionOption $wsmOpt -Credential $credential `
-Authentication Basic -ErrorAction Stop | Out-Null -Authentication Basic -ErrorAction Stop | Out-Null
Write-Host "[Prepare] WinRM reachable." Write-Host "[Prepare] WinRM HTTPS reachable."
} }
catch { catch {
Write-Error @" Write-Error @"
[Prepare] Cannot reach $VMIPAddress via WinRM. [Prepare] Cannot reach $VMIPAddress via WinRM HTTPS (port 5986).
Ensure: Ensure:
- The VM is running and on VMnet8 (NAT) with internet access - The VM is running and on VMnet8 (NAT) with internet access
- WinRM is enabled inside the VM (run as Administrator in VM): - WinRM HTTPS is enabled inside the VM Deploy-WinBuild2025.ps1 post-install.ps1
winrm quickconfig -q must have run successfully (creates self-signed cert + HTTPS listener on 5986)
Enable-PSRemoting -Force -SkipNetworkProfileCheck - Windows Firewall allows port 5986 inbound (or firewall is disabled on the guest)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
- Windows Firewall allows port 5985 inbound
- The IP is correct (check ipconfig inside the VM) - The IP is correct (check ipconfig inside the VM)
Error: $_ Error: $_
@@ -324,11 +314,13 @@ Error: $_
# ── Open session ────────────────────────────────────────────────────────────── # ── Open session ──────────────────────────────────────────────────────────────
Write-Host "[Prepare] Opening PSSession..." Write-Host "[Prepare] Opening PSSession..."
$session = New-PSSession ` $session = New-PSSession `
-ComputerName $VMIPAddress ` -ComputerName $VMIPAddress `
-Credential $credential ` -Credential $credential `
-SessionOption $sessionOptions ` -SessionOption $sessionOptions `
-UseSSL `
-Port 5986 `
-Authentication Basic ` -Authentication Basic `
-ErrorAction Stop -ErrorAction Stop
try { try {
# ── Copy Setup-WinBuild2025.ps1 into the VM ─────────────────────────────── # ── Copy Setup-WinBuild2025.ps1 into the VM ───────────────────────────────
@@ -423,7 +415,9 @@ try {
while ((Get-Date) -lt $deadline) { while ((Get-Date) -lt $deadline) {
Start-Sleep -Seconds 10 Start-Sleep -Seconds 10
try { 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 -Authentication Basic -ErrorAction Stop
if ($r) { return $true } if ($r) { return $true }
} catch { } } catch { }
@@ -481,7 +475,8 @@ try {
} }
Write-Host "[Prepare] Guest WinRM ready, reopening session..." Write-Host "[Prepare] Guest WinRM ready, reopening session..."
$session = New-PSSession -ComputerName $VMIPAddress -Credential $credential ` $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 ────────────────────────────────────────── # ── Post-setup remote validation ──────────────────────────────────────────
@@ -670,12 +665,9 @@ try {
finally { finally {
Remove-PSSession $session -ErrorAction SilentlyContinue Remove-PSSession $session -ErrorAction SilentlyContinue
# Restore host WinRM client settings to their original values # Restore host TrustedHosts to its original value
if ($null -ne $prevAllowUnencrypted) {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnencrypted -Force -ErrorAction SilentlyContinue
}
if ($null -ne $prevTrustedHosts) { if ($null -ne $prevTrustedHosts) {
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $prevTrustedHosts -Force -ErrorAction SilentlyContinue 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."
} }
+52 -8
View File
@@ -18,9 +18,9 @@
Validation: Domain/Private/Public all Enabled=False Validation: Domain/Private/Public all Enabled=False
Step 2 Defender: validation only Deploy set DisableAntiSpyware=1 GPO + RTP off. Step 2 Defender: validation only Deploy set DisableAntiSpyware=1 GPO + RTP off.
Validation: GPO DisableAntiSpyware=1 Validation: GPO DisableAntiSpyware=1
Step 3 WinRM: validation only Deploy ran Enable-PSRemoting, Basic auth, Step 3 WinRM: validation only Deploy ran Enable-PSRemoting, Basic auth over HTTPS,
AllowUnencrypted, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25. AllowUnencrypted=false (HTTPS-only), MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25.
Validation: service Running+Automatic, AllowUnencrypted, Auth/Basic, memory Validation: service Running+Automatic, AllowUnencrypted=false, Auth/Basic, memory
Step 4 Local build user account ($BuildUsername, PasswordNeverExpires, Administrators) Step 4 Local build user account ($BuildUsername, PasswordNeverExpires, Administrators)
Validation: user exists, enabled, PasswordNeverExpires, admin membership Validation: user exists, enabled, PasswordNeverExpires, admin membership
Step 4b UAC: validation only Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1. Step 4b UAC: validation only Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1.
@@ -161,6 +161,44 @@ function Assert-Step {
Write-Host " [OK] $Description" -ForegroundColor Green 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 ─────────────── # ── Pre-flight: remove temp files from any previous partial run ───────────────
# These files are normally deleted at the end of each step that creates them. # 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. # 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) ─────── # ── Step 3: WinRM (validation only — configured by Deploy-WinBuild2025) ───────
Write-Step "WinRM state (validation only — configured at deploy time)" Write-Step "WinRM state (validation only — configured at deploy time)"
# Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth, # Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth over HTTPS,
# AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25, # AllowUnencrypted=false (HTTPS/5986 only), TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h,
# and StartupType=Automatic. Validated here before the build user and toolchain steps. # MaxProcesses=25, StartupType=Automatic. Validated here before the build user and toolchain steps.
Assert-Step 'WinRM' 'service Running' { Assert-Step 'WinRM' 'service Running' {
(Get-Service WinRM -ErrorAction Stop).Status -eq 'Running' (Get-Service WinRM -ErrorAction Stop).Status -eq 'Running'
@@ -213,8 +251,8 @@ Assert-Step 'WinRM' 'service Running' {
Assert-Step 'WinRM' 'service StartType Automatic' { Assert-Step 'WinRM' 'service StartType Automatic' {
(Get-Service WinRM -ErrorAction Stop).StartType -eq 'Automatic' (Get-Service WinRM -ErrorAction Stop).StartType -eq 'Automatic'
} }
Assert-Step 'WinRM' 'AllowUnencrypted=true' { Assert-Step 'WinRM' 'AllowUnencrypted=false (HTTPS-only)' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted).Value -eq 'true' (Get-Item WSMan:\localhost\Service\AllowUnencrypted).Value -eq 'false'
} }
Assert-Step 'WinRM' 'Auth/Basic=true' { Assert-Step 'WinRM' 'Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic).Value -eq 'true' (Get-Item WSMan:\localhost\Service\Auth\Basic).Value -eq 'true'
@@ -589,6 +627,8 @@ else {
$dotnetInstallScript = "$env:TEMP\dotnet-install.ps1" $dotnetInstallScript = "$env:TEMP\dotnet-install.ps1"
Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' ` Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' `
-OutFile $dotnetInstallScript -UseBasicParsing -OutFile $dotnetInstallScript -UseBasicParsing
Assert-Hash -FilePath $dotnetInstallScript -Label 'dotnet-install.ps1' `
-Expected $script:Hashes['DotNetInstallScript']
Write-Host "Installing .NET SDK $DotNetSdkVersion (channel)..." Write-Host "Installing .NET SDK $DotNetSdkVersion (channel)..."
& $dotnetInstallScript ` & $dotnetInstallScript `
@@ -635,6 +675,8 @@ else {
$pyInstallerPath = 'C:\CI\python_installer.exe' $pyInstallerPath = 'C:\CI\python_installer.exe'
Write-Host "Downloading Python $PythonVersion installer..." Write-Host "Downloading Python $PythonVersion installer..."
Invoke-WebRequest -Uri $pyInstallerUrl -OutFile $pyInstallerPath -UseBasicParsing 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)..." Write-Host "Installing Python $PythonVersion (all users, prepend PATH)..."
$pyProc = Start-Process -FilePath $pyInstallerPath -ArgumentList @( $pyProc = Start-Process -FilePath $pyInstallerPath -ArgumentList @(
@@ -689,6 +731,8 @@ else {
Write-Host "Downloading VS Build Tools installer..." Write-Host "Downloading VS Build Tools installer..."
Remove-Item $vsInstallerPath -ErrorAction SilentlyContinue # remove any stale/corrupt copy Remove-Item $vsInstallerPath -ErrorAction SilentlyContinue # remove any stale/corrupt copy
Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath -UseBasicParsing 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). # Remove Zone.Identifier ADS — files downloaded from internet are marked Zone 3 (Internet).
# SYSTEM account cannot execute them without this unblock step. # SYSTEM account cannot execute them without this unblock step.
+4 -7
View File
@@ -42,11 +42,9 @@ $cred = New-Object System.Management.Automation.PSCredential(
) )
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck $so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try { try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) { if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress } $newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
@@ -56,7 +54,7 @@ try {
Write-Host "`nValidating Deploy state on $VMIPAddress..." -ForegroundColor Cyan Write-Host "`nValidating Deploy state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` $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() $results = [System.Collections.Generic.List[PSCustomObject]]::new()
@@ -82,8 +80,8 @@ try {
# ── WinRM ───────────────────────────────────────────────────────────── # ── WinRM ─────────────────────────────────────────────────────────────
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' } 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 service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' { Chk 'WinRM AllowUnencrypted=false (HTTPS-only)' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'false'
} }
Chk 'WinRM Auth/Basic=true' { Chk 'WinRM Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true' (Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true'
@@ -182,6 +180,5 @@ try {
} }
} finally { } 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
} }
+7 -10
View File
@@ -63,11 +63,9 @@ $cred = New-Object System.Management.Automation.PSCredential(
) )
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck $so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try { try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) { if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress } $newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
@@ -77,7 +75,7 @@ try {
Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` $checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock { -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion) param($BuildUsername, $DotNetChannel, $PythonVersion)
@@ -109,8 +107,8 @@ try {
# WinRM # WinRM
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' } 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 service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' { Chk 'WinRM AllowUnencrypted=false (HTTPS-only)' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'false'
} }
Chk 'WinRM Auth/Basic=true' { Chk 'WinRM Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq '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 Write-Host "Applying remediation for $($failedNames.Count) failed check(s)..." -ForegroundColor Yellow
Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock { -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param([string[]] $FailedNames, [string] $AdminPassword) param([string[]] $FailedNames, [string] $AdminPassword)
@@ -296,7 +294,7 @@ try {
# ── Re-run checks ───────────────────────────────────────────────────────── # ── Re-run checks ─────────────────────────────────────────────────────────
Write-Host "`nRe-validating after remediation..." -ForegroundColor Cyan Write-Host "`nRe-validating after remediation..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` $checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock { -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion) param($BuildUsername, $DotNetChannel, $PythonVersion)
$results = [System.Collections.Generic.List[PSCustomObject]]::new() $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 '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 Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' } 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 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 } Chk 'WinRM MaxMemoryPerShellMB >= 2048' { [int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048 }
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' $polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
@@ -369,6 +367,5 @@ try {
} }
} finally { } 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
} }