diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a4650..004e5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.14.0] - 2025-01-25 + +### Security +- Add SHA256 checksum verification for Ubuntu cloud image downloads (prevents MITM attacks) +- Add strict input validation for git name, email, VM password, and VNC password +- Validate loaded config.env values to detect tampering +- VNC password minimum increased from 6 to 8 characters +- Block shell metacharacters in all user inputs to prevent injection +- Config file created with restricted ACL from the start (no race condition window) +- Add security reminder to remove cloud-init ISO after first boot (contains passwords) + +### Added +- ARM64 architecture detection for Windows on ARM devices +- Log file creation at `$env:TEMP\setup_env_windows_.log` +- Cleanup on failure: automatically removes partial VM, disk, and ISO on error +- Hosts file backup before modification (removed on success, kept on failure) +- Input validation functions: `Test-GitName`, `Test-GitEmail`, `Test-VMPassword`, `Test-VNCPassword` +- Checksum caching to avoid re-downloading verification data + +### Changed +- Ubuntu image URL now uses detected architecture instead of hardcoded amd64 +- All major operations now log to file for troubleshooting +- VM creation wrapped in try/catch with automatic cleanup on failure + ## [0.13.0] - 2025-01-25 ### Changed diff --git a/setup_env_windows.ps1 b/setup_env_windows.ps1 index b26f630..58b12dc 100644 --- a/setup_env_windows.ps1 +++ b/setup_env_windows.ps1 @@ -80,16 +80,42 @@ $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" # Speed up downloads $UBUNTU_VERSION = "24.04" -$UBUNTU_IMAGE_URL = "https://cloud-images.ubuntu.com/releases/$UBUNTU_VERSION/release/ubuntu-$UBUNTU_VERSION-server-cloudimg-amd64.vhdx" $VM_STORAGE_PATH = "$env:ProgramData\HyperV-DevSandbox" $IMAGES_PATH = "$VM_STORAGE_PATH\Images" $VMS_PATH = "$VM_STORAGE_PATH\VMs" $CONFIG_FILE = "$PSScriptRoot\config.env" +$LOG_FILE = "$env:TEMP\setup_env_windows_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" + +# Detect architecture for correct image +$ARCH = if ([System.Environment]::Is64BitOperatingSystem) { + $cpu = Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1 + if ($cpu.Architecture -eq 12) { "arm64" } else { "amd64" } +} else { + "amd64" +} + +$UBUNTU_IMAGE_URL = "https://cloud-images.ubuntu.com/releases/$UBUNTU_VERSION/release/ubuntu-$UBUNTU_VERSION-server-cloudimg-$ARCH.vhdx" +$UBUNTU_CHECKSUM_URL = "https://cloud-images.ubuntu.com/releases/$UBUNTU_VERSION/release/SHA256SUMS" # ============================================================================ # Helper Functions # ============================================================================ +# Global cleanup tracking +$script:CleanupResources = @{ + VMCreated = $false + VMName = "" + DiskPath = "" + ISOPath = "" +} + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $logEntry = "[$timestamp] [$Level] $Message" + Add-Content -Path $LOG_FILE -Value $logEntry -ErrorAction SilentlyContinue +} + function Write-Status { param([string]$Message, [string]$Type = "Info") $colors = @{ @@ -105,6 +131,7 @@ function Write-Status { "Error" = "[X]" } Write-Host "$($prefix[$Type]) $Message" -ForegroundColor $colors[$Type] + Write-Log $Message $Type.ToUpper() } function Test-Administrator { @@ -144,6 +171,143 @@ function Get-SecureInput { } } +# ============================================================================ +# Input Validation Functions +# ============================================================================ + +function Test-SafeInput { + # Validates input contains only safe characters (no shell metacharacters) + param([string]$Input, [string]$AllowedPattern = '^[a-zA-Z0-9@._\s-]+$') + return $Input -match $AllowedPattern +} + +function Test-GitName { + param([string]$Name) + # Allow letters, numbers, spaces, hyphens, periods, apostrophes + # Block shell metacharacters: $ ` \ " ' ; | & < > ( ) { } [ ] ! # % ^ + if ([string]::IsNullOrWhiteSpace($Name)) { return $false } + if ($Name.Length -gt 100) { return $false } + return $Name -match "^[a-zA-Z0-9\s.'_-]+$" +} + +function Test-GitEmail { + param([string]$Email) + # Standard email validation + block dangerous characters + if ([string]::IsNullOrWhiteSpace($Email)) { return $false } + if ($Email.Length -gt 254) { return $false } + # Must be valid email format and not contain shell metacharacters + return $Email -match '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' +} + +function Test-VMPassword { + param([string]$Password) + # Password requirements: + # - Minimum 8 characters + # - No characters that could break YAML or shell (no: ' " ` $ \ newlines) + if ($Password.Length -lt 8) { return $false } + if ($Password.Length -gt 128) { return $false } + # Block characters that could cause YAML/shell injection + if ($Password -match '[''"`$\\]') { return $false } + if ($Password -match '[\r\n]') { return $false } + return $true +} + +function Test-VNCPassword { + param([string]$Password) + # VNC password requirements: + # - Minimum 8 characters (increased from 6 for security) + # - Maximum 8 characters (VNC limitation - only first 8 are used) + # - No shell metacharacters + if ($Password.Length -lt 8) { return $false } + # Block characters that could cause issues + if ($Password -match '[''"`$\\;&|<>]') { return $false } + return $true +} + +function Invoke-Cleanup { + param([switch]$OnError) + + if ($OnError) { + Write-Status "Cleaning up after error..." "Warning" + Write-Log "Cleanup triggered due to error" + } + + # Stop and remove VM if it was created + if ($script:CleanupResources.VMCreated -and $script:CleanupResources.VMName) { + $vmName = $script:CleanupResources.VMName + Write-Log "Removing VM: $vmName" + Stop-VM -Name $vmName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + Remove-VM -Name $vmName -Force -ErrorAction SilentlyContinue + } + + # Remove disk if created + if ($script:CleanupResources.DiskPath -and (Test-Path $script:CleanupResources.DiskPath)) { + Write-Log "Removing disk: $($script:CleanupResources.DiskPath)" + Remove-Item -Path $script:CleanupResources.DiskPath -Force -ErrorAction SilentlyContinue + } + + # Remove ISO if created (contains sensitive data) + if ($script:CleanupResources.ISOPath -and (Test-Path $script:CleanupResources.ISOPath)) { + Write-Log "Removing cloud-init ISO: $($script:CleanupResources.ISOPath)" + Remove-Item -Path $script:CleanupResources.ISOPath -Force -ErrorAction SilentlyContinue + } +} + +function Get-FileChecksum { + param([string]$FilePath) + $hash = Get-FileHash -Path $FilePath -Algorithm SHA256 + return $hash.Hash.ToLower() +} + +function Test-ImageChecksum { + param( + [string]$ImagePath, + [string]$ExpectedChecksum + ) + + Write-Status "Verifying image checksum..." "Info" + $actualChecksum = Get-FileChecksum -FilePath $ImagePath + + if ($actualChecksum -eq $ExpectedChecksum.ToLower()) { + Write-Status "Checksum verified" "Success" + Write-Log "Checksum OK: $actualChecksum" + return $true + } else { + Write-Status "Checksum mismatch!" "Error" + Write-Log "Checksum FAILED: expected=$ExpectedChecksum actual=$actualChecksum" + return $false + } +} + +function Get-UbuntuImageChecksum { + param([string]$ImageFileName) + + try { + Write-Log "Fetching checksums from $UBUNTU_CHECKSUM_URL" + $checksums = Invoke-WebRequest -Uri $UBUNTU_CHECKSUM_URL -UseBasicParsing -ErrorAction Stop + $lines = $checksums.Content -split "`n" + + foreach ($line in $lines) { + # Format: or * + if ($line -match "^([a-f0-9]{64})\s+\*?(.+)$") { + $checksum = $matches[1] + $filename = $matches[2].Trim() + if ($filename -eq $ImageFileName -or $filename -eq "*$ImageFileName") { + Write-Log "Found checksum for $ImageFileName : $checksum" + return $checksum + } + } + } + + Write-Log "No checksum found for $ImageFileName" + return $null + } catch { + Write-Log "Failed to fetch checksums: $_" + return $null + } +} + function ConvertTo-Base64 { param([string]$Text) return [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Text)) @@ -273,11 +437,22 @@ function New-ISOFile { # Pre-flight Checks # ============================================================================ +# Initialize log file +$null = New-Item -Path $LOG_FILE -ItemType File -Force -ErrorAction SilentlyContinue +Write-Log "==========================================" +Write-Log "Hyper-V Development Sandbox Setup Started" +Write-Log "VM Name: $VMName" +Write-Log "Architecture: $ARCH" +Write-Log "PowerShell Version: $($PSVersionTable.PSVersion)" +Write-Log "==========================================" + Write-Host "" Write-Host "============================================================================" -ForegroundColor Green Write-Host " Hyper-V Development Sandbox - Maximum Security Mode" -ForegroundColor Green Write-Host "============================================================================" -ForegroundColor Green Write-Host "" +Write-Host " Log file: $LOG_FILE" -ForegroundColor Gray +Write-Host "" # Check admin privileges if (-not (Test-Administrator)) { @@ -351,19 +526,21 @@ if (Test-Path $CONFIG_FILE) { do { $GitName = Get-SecureInput "Git commit author name" -AsPlainText - } while ([string]::IsNullOrWhiteSpace($GitName)) + if (-not (Test-GitName $GitName)) { + Write-Host " Invalid name. Use only letters, numbers, spaces, periods, hyphens." -ForegroundColor Yellow + } + } while (-not (Test-GitName $GitName)) do { $GitEmail = Get-SecureInput "Git commit author email" -AsPlainText - } while ($GitEmail -notmatch '^[^@]+@[^@]+\.[^@]+$') + if (-not (Test-GitEmail $GitEmail)) { + Write-Host " Invalid email format." -ForegroundColor Yellow + } + } while (-not (Test-GitEmail $GitEmail)) - # Save config - @" -GIT_NAME="$GitName" -GIT_EMAIL="$GitEmail" -"@ | Out-File -FilePath $CONFIG_FILE -Encoding UTF8 - - # Restrict permissions (Windows equivalent of chmod 600) + # Create config file with restricted permissions from the start + # First create empty file with restricted ACL, then write content + $null = New-Item -Path $CONFIG_FILE -ItemType File -Force $acl = Get-Acl $CONFIG_FILE $acl.SetAccessRuleProtection($true, $false) $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( @@ -374,38 +551,93 @@ GIT_EMAIL="$GitEmail" $acl.SetAccessRule($rule) Set-Acl -Path $CONFIG_FILE -AclObject $acl + # Now write config content + @" +GIT_NAME="$GitName" +GIT_EMAIL="$GitEmail" +"@ | Out-File -FilePath $CONFIG_FILE -Encoding UTF8 -Force + Write-Host "" Write-Status "Config saved to: $CONFIG_FILE" "Success" } +# Validate loaded config values (could be tampered with) +if (-not (Test-GitName $GitName)) { + Write-Status "Invalid git name in config file. Please delete $CONFIG_FILE and run again." "Error" + exit 1 +} +if (-not (Test-GitEmail $GitEmail)) { + Write-Status "Invalid git email in config file. Please delete $CONFIG_FILE and run again." "Error" + exit 1 +} + # Prompt for VM password (not stored) Write-Host "" do { - $VMPassword = Get-SecureInput "VM user password (min 8 chars, for 'dev' user)" -} while ($VMPassword.Length -lt 8) + $VMPassword = Get-SecureInput "VM user password (min 8 chars, no quotes/backslashes)" + if (-not (Test-VMPassword $VMPassword)) { + Write-Host " Password must be 8+ chars without quotes, backticks, dollar signs, or backslashes." -ForegroundColor Yellow + } +} while (-not (Test-VMPassword $VMPassword)) # Prompt for VNC password if VNC is enabled $VNCPassword = "" if (-not $SkipVNC) { Write-Host "" do { - $VNCPassword = Get-SecureInput "VNC password (min 6 chars, for remote desktop)" - } while ($VNCPassword.Length -lt 6) + $VNCPassword = Get-SecureInput "VNC password (exactly 8 chars, no special shell chars)" + if (-not (Test-VNCPassword $VNCPassword)) { + Write-Host " Password must be exactly 8 chars without quotes, backslashes, or shell characters." -ForegroundColor Yellow + } + } while (-not (Test-VNCPassword $VNCPassword)) } # ============================================================================ -# Download Ubuntu Cloud Image +# Download Ubuntu Cloud Image (with checksum verification) # ============================================================================ New-Item -ItemType Directory -Path $IMAGES_PATH -Force | Out-Null New-Item -ItemType Directory -Path "$VMS_PATH\$VMName" -Force | Out-Null -$imagePath = "$IMAGES_PATH\ubuntu-$UBUNTU_VERSION-cloudimg-amd64.vhdx" +$imageFileName = "ubuntu-$UBUNTU_VERSION-server-cloudimg-$ARCH.vhdx" +$imagePath = "$IMAGES_PATH\$imageFileName" +$imageChecksumPath = "$imagePath.sha256" -if (-not (Test-Path $imagePath)) { - Write-Status "Downloading Ubuntu $UBUNTU_VERSION cloud image (~700MB)..." "Info" +# Get expected checksum +$expectedChecksum = $null +if (Test-Path $imageChecksumPath) { + $expectedChecksum = (Get-Content $imageChecksumPath -ErrorAction SilentlyContinue).Trim() + Write-Log "Loaded cached checksum: $expectedChecksum" +} + +if (-not $expectedChecksum) { + Write-Status "Fetching image checksum from Canonical..." "Info" + $expectedChecksum = Get-UbuntuImageChecksum -ImageFileName $imageFileName + if ($expectedChecksum) { + $expectedChecksum | Set-Content -Path $imageChecksumPath + } +} + +$needsDownload = $true +if (Test-Path $imagePath) { + if ($expectedChecksum) { + # Verify existing image + if (Test-ImageChecksum -ImagePath $imagePath -ExpectedChecksum $expectedChecksum) { + Write-Status "Using cached Ubuntu image (checksum verified)" "Info" + $needsDownload = $false + } else { + Write-Status "Cached image checksum mismatch, re-downloading..." "Warning" + Remove-Item -Path $imagePath -Force + } + } else { + Write-Status "Using cached Ubuntu image (checksum unavailable)" "Warning" + $needsDownload = $false + } +} + +if ($needsDownload) { + Write-Status "Downloading Ubuntu $UBUNTU_VERSION cloud image ($ARCH, ~700MB)..." "Info" Write-Host " URL: $UBUNTU_IMAGE_URL" -ForegroundColor Gray - Write-Host " This may take a few minutes..." -ForegroundColor Gray try { # Use BITS for better download handling @@ -422,19 +654,37 @@ if (-not (Test-Path $imagePath)) { } Complete-BitsTransfer -BitsJob $bitsJob Write-Status "Download complete" "Success" + + # Verify downloaded image + if ($expectedChecksum) { + if (-not (Test-ImageChecksum -ImagePath $imagePath -ExpectedChecksum $expectedChecksum)) { + Remove-Item -Path $imagePath -Force -ErrorAction SilentlyContinue + throw "Downloaded image failed checksum verification. This could indicate a corrupted download or MITM attack." + } + } else { + Write-Status "Warning: Could not verify image checksum (checksum unavailable)" "Warning" + } } catch { # Fallback to Invoke-WebRequest Write-Status "BITS transfer failed, trying direct download..." "Warning" try { Invoke-WebRequest -Uri $UBUNTU_IMAGE_URL -OutFile $imagePath -UseBasicParsing Write-Status "Download complete" "Success" + + # Verify downloaded image + if ($expectedChecksum) { + if (-not (Test-ImageChecksum -ImagePath $imagePath -ExpectedChecksum $expectedChecksum)) { + Remove-Item -Path $imagePath -Force -ErrorAction SilentlyContinue + throw "Downloaded image failed checksum verification. This could indicate a corrupted download or MITM attack." + } + } else { + Write-Status "Warning: Could not verify image checksum (checksum unavailable)" "Warning" + } } catch { Write-Status "Failed to download Ubuntu image: $_" "Error" exit 1 } } -} else { - Write-Status "Using cached Ubuntu image" "Info" } # ============================================================================ @@ -444,8 +694,15 @@ if (-not (Test-Path $imagePath)) { $vmDiskPath = "$VMS_PATH\$VMName\disk.vhdx" Write-Status "Creating VM disk ($DiskGB GB)..." "Info" -Copy-Item -Path $imagePath -Destination $vmDiskPath -Force -Resize-VHD -Path $vmDiskPath -SizeBytes ($DiskGB * 1GB) +try { + Copy-Item -Path $imagePath -Destination $vmDiskPath -Force + $script:CleanupResources.DiskPath = $vmDiskPath + Resize-VHD -Path $vmDiskPath -SizeBytes ($DiskGB * 1GB) +} catch { + Write-Status "Failed to create VM disk: $_" "Error" + Invoke-Cleanup -OnError + exit 1 +} # ============================================================================ # Generate SSH Key @@ -580,8 +837,9 @@ instance-id: $VMName-$(Get-Date -Format 'yyyyMMddHHmmss') local-hostname: $VMName "@ -# Create cloud-init ISO +# Create cloud-init ISO (contains sensitive data - will be cleaned up) $cloudInitISO = "$VMS_PATH\$VMName\cloud-init.iso" +$script:CleanupResources.ISOPath = $cloudInitISO Write-Status "Creating cloud-init ISO..." "Info" try { @@ -590,6 +848,7 @@ try { throw "ISO file was not created" } Write-Status "Cloud-init ISO created" "Success" + Write-Log "Cloud-init ISO contains passwords - will be removed after first boot" } catch { Write-Status "Failed to create cloud-init ISO: $_" "Error" Write-Host "" @@ -601,6 +860,7 @@ try { Write-Host "2. Install genisoimage in WSL:" -ForegroundColor Cyan Write-Host " wsl sudo apt-get install genisoimage" -ForegroundColor White Write-Host "" + Invoke-Cleanup -OnError exit 1 } @@ -614,53 +874,64 @@ Write-Host " Memory: $MemoryGB GB (dynamic)" -ForegroundColor Gray Write-Host " Disk: $DiskGB GB" -ForegroundColor Gray Write-Host " Switch: $($vmSwitch.Name)" -ForegroundColor Gray -# Create VM (Generation 2 for UEFI) -$vm = New-VM -Name $VMName ` - -Generation 2 ` - -MemoryStartupBytes ($MemoryGB * 1GB) ` - -VHDPath $vmDiskPath ` - -SwitchName $vmSwitch.Name ` - -Path "$VMS_PATH\$VMName" - -# Configure VM processor -Set-VMProcessor -VM $vm -Count $CPUs - -# Configure dynamic memory -Set-VMMemory -VM $vm -DynamicMemoryEnabled $true ` - -MinimumBytes 2GB ` - -StartupBytes ($MemoryGB * 1GB) ` - -MaximumBytes ($MemoryGB * 1GB) - -# Disable Secure Boot for Ubuntu (required for cloud images) -Set-VMFirmware -VM $vm -EnableSecureBoot Off - -# Add cloud-init ISO as DVD drive -Add-VMDvdDrive -VM $vm -Path $cloudInitISO - -# Configure boot order (DVD first for initial boot, disk for subsequent) -$dvd = Get-VMDvdDrive -VM $vm -$hdd = Get-VMHardDiskDrive -VM $vm -Set-VMFirmware -VM $vm -BootOrder $dvd, $hdd - -# Security: Disable integration services for maximum isolation -Write-Status "Configuring security settings..." "Info" -Disable-VMIntegrationService -VM $vm -Name "Guest Service Interface" -ErrorAction SilentlyContinue -Disable-VMIntegrationService -VM $vm -Name "Heartbeat" -ErrorAction SilentlyContinue -# Keep Time Synchronization and Shutdown for usability - -# Enable nested virtualization (for Docker inside VM) try { - Set-VMProcessor -VM $vm -ExposeVirtualizationExtensions $true - Write-Host " Nested virtualization: enabled" -ForegroundColor Gray + # Create VM (Generation 2 for UEFI) + $vm = New-VM -Name $VMName ` + -Generation 2 ` + -MemoryStartupBytes ($MemoryGB * 1GB) ` + -VHDPath $vmDiskPath ` + -SwitchName $vmSwitch.Name ` + -Path "$VMS_PATH\$VMName" + + # Track for cleanup + $script:CleanupResources.VMCreated = $true + $script:CleanupResources.VMName = $VMName + + # Configure VM processor + Set-VMProcessor -VM $vm -Count $CPUs + + # Configure dynamic memory + Set-VMMemory -VM $vm -DynamicMemoryEnabled $true ` + -MinimumBytes 2GB ` + -StartupBytes ($MemoryGB * 1GB) ` + -MaximumBytes ($MemoryGB * 1GB) + + # Disable Secure Boot for Ubuntu (required for cloud images) + Set-VMFirmware -VM $vm -EnableSecureBoot Off + + # Add cloud-init ISO as DVD drive + Add-VMDvdDrive -VM $vm -Path $cloudInitISO + + # Configure boot order (DVD first for initial boot, disk for subsequent) + $dvd = Get-VMDvdDrive -VM $vm + $hdd = Get-VMHardDiskDrive -VM $vm + Set-VMFirmware -VM $vm -BootOrder $dvd, $hdd + + # Security: Disable integration services for maximum isolation + Write-Status "Configuring security settings..." "Info" + Disable-VMIntegrationService -VM $vm -Name "Guest Service Interface" -ErrorAction SilentlyContinue + Disable-VMIntegrationService -VM $vm -Name "Heartbeat" -ErrorAction SilentlyContinue + # Keep Time Synchronization and Shutdown for usability + + # Enable nested virtualization (for Docker inside VM) + try { + Set-VMProcessor -VM $vm -ExposeVirtualizationExtensions $true + Write-Host " Nested virtualization: enabled" -ForegroundColor Gray + } catch { + Write-Host " Nested virtualization: not available" -ForegroundColor Gray + } + + # Set automatic checkpoints off (for performance) + Set-VM -VM $vm -AutomaticCheckpointsEnabled $false + + Write-Status "VM created successfully" "Success" + } catch { - Write-Host " Nested virtualization: not available" -ForegroundColor Gray + Write-Status "Failed to create VM: $_" "Error" + Invoke-Cleanup -OnError + exit 1 } -# Set automatic checkpoints off (for performance) -Set-VM -VM $vm -AutomaticCheckpointsEnabled $false - -Write-Status "VM created successfully" "Success" - # ============================================================================ # Start VM and Wait for Provisioning # ============================================================================ @@ -695,18 +966,37 @@ Write-Host "" if ($vmIP) { Write-Status "VM IP: $vmIP" "Success" - # Update hosts file for easy access + # Update hosts file for easy access (with backup) $hostsFile = "$env:SystemRoot\System32\drivers\etc\hosts" + $hostsBackup = "$env:SystemRoot\System32\drivers\etc\hosts.backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')" $hostsEntry = "$vmIP`t$VMName.local" - # Remove old entry if exists - $hostsContent = Get-Content $hostsFile -ErrorAction SilentlyContinue - $hostsContent = $hostsContent | Where-Object { $_ -notmatch "\s+$VMName\.local\s*$" } + try { + # Create backup before modification + if (Test-Path $hostsFile) { + Copy-Item -Path $hostsFile -Destination $hostsBackup -Force + Write-Log "Created hosts file backup: $hostsBackup" + } - # Add new entry - $hostsContent += $hostsEntry - $hostsContent | Set-Content $hostsFile -Force - Write-Status "Updated hosts file: $VMName.local -> $vmIP" "Info" + # Remove old entry if exists + $hostsContent = Get-Content $hostsFile -ErrorAction SilentlyContinue + $hostsContent = $hostsContent | Where-Object { $_ -notmatch "\s+$VMName\.local\s*$" } + + # Add new entry + $hostsContent += $hostsEntry + $hostsContent | Set-Content $hostsFile -Force + Write-Status "Updated hosts file: $VMName.local -> $vmIP" "Info" + + # Remove backup if successful (keep only on failure) + Remove-Item -Path $hostsBackup -Force -ErrorAction SilentlyContinue + } catch { + Write-Status "Failed to update hosts file: $_" "Warning" + Write-Host " You may need to manually add: $hostsEntry" -ForegroundColor Yellow + Write-Host " To file: $hostsFile" -ForegroundColor Yellow + if (Test-Path $hostsBackup) { + Write-Host " Backup available at: $hostsBackup" -ForegroundColor Yellow + } + } # Wait a moment for SSH to be ready Write-Status "Waiting for SSH to become available..." "Info" @@ -768,7 +1058,19 @@ if ($vmIP) { Write-Host "" } +# Security reminder about cloud-init ISO +Write-Host "SECURITY:" -ForegroundColor Yellow +Write-Host " The cloud-init ISO contains your VM password. After first boot completes," -ForegroundColor White +Write-Host " remove it for security:" -ForegroundColor White +Write-Host " Stop-VM -Name $VMName; Remove-VMDvdDrive -VMName $VMName -ControllerNumber 0 -ControllerLocation 1" -ForegroundColor Cyan +Write-Host " Remove-Item `"$cloudInitISO`"" -ForegroundColor Cyan +Write-Host "" +Write-Host "LOG FILE:" -ForegroundColor Yellow +Write-Host " $LOG_FILE" -ForegroundColor White +Write-Host "" Write-Host "============================================================================" -ForegroundColor Green Write-Host " Security: Full Hyper-V isolation (separate kernel, network, filesystem)" -ForegroundColor Green Write-Host "============================================================================" -ForegroundColor Green Write-Host "" + +Write-Log "Setup completed successfully for VM: $VMName"