Adds security hardening to Windows Hyper-V script
Security improvements: - SHA256 checksum verification for Ubuntu image downloads - Strict input validation for all user inputs (git name/email, passwords) - Blocks shell metacharacters to prevent injection attacks - Config file created with restricted ACL from the start - VNC password minimum increased to 8 characters - Security reminder to remove cloud-init ISO after first boot Reliability improvements: - ARM64 architecture detection for Windows on ARM - Log file creation for troubleshooting - Automatic cleanup on failure (VM, disk, ISO) - Hosts file backup before modification Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
65790ee3e2
commit
ca12925111
2 changed files with 401 additions and 75 deletions
24
CHANGELOG.md
24
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/),
|
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).
|
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_<timestamp>.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
|
## [0.13.0] - 2025-01-25
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,42 @@ $ErrorActionPreference = "Stop"
|
||||||
$ProgressPreference = "SilentlyContinue" # Speed up downloads
|
$ProgressPreference = "SilentlyContinue" # Speed up downloads
|
||||||
|
|
||||||
$UBUNTU_VERSION = "24.04"
|
$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"
|
$VM_STORAGE_PATH = "$env:ProgramData\HyperV-DevSandbox"
|
||||||
$IMAGES_PATH = "$VM_STORAGE_PATH\Images"
|
$IMAGES_PATH = "$VM_STORAGE_PATH\Images"
|
||||||
$VMS_PATH = "$VM_STORAGE_PATH\VMs"
|
$VMS_PATH = "$VM_STORAGE_PATH\VMs"
|
||||||
$CONFIG_FILE = "$PSScriptRoot\config.env"
|
$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
|
# 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 {
|
function Write-Status {
|
||||||
param([string]$Message, [string]$Type = "Info")
|
param([string]$Message, [string]$Type = "Info")
|
||||||
$colors = @{
|
$colors = @{
|
||||||
|
|
@ -105,6 +131,7 @@ function Write-Status {
|
||||||
"Error" = "[X]"
|
"Error" = "[X]"
|
||||||
}
|
}
|
||||||
Write-Host "$($prefix[$Type]) $Message" -ForegroundColor $colors[$Type]
|
Write-Host "$($prefix[$Type]) $Message" -ForegroundColor $colors[$Type]
|
||||||
|
Write-Log $Message $Type.ToUpper()
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test-Administrator {
|
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: <sha256> <filename> or <sha256> *<filename>
|
||||||
|
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 {
|
function ConvertTo-Base64 {
|
||||||
param([string]$Text)
|
param([string]$Text)
|
||||||
return [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Text))
|
return [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Text))
|
||||||
|
|
@ -273,11 +437,22 @@ function New-ISOFile {
|
||||||
# Pre-flight Checks
|
# 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 ""
|
||||||
Write-Host "============================================================================" -ForegroundColor Green
|
Write-Host "============================================================================" -ForegroundColor Green
|
||||||
Write-Host " Hyper-V Development Sandbox - Maximum Security Mode" -ForegroundColor Green
|
Write-Host " Hyper-V Development Sandbox - Maximum Security Mode" -ForegroundColor Green
|
||||||
Write-Host "============================================================================" -ForegroundColor Green
|
Write-Host "============================================================================" -ForegroundColor Green
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
Write-Host " Log file: $LOG_FILE" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
# Check admin privileges
|
# Check admin privileges
|
||||||
if (-not (Test-Administrator)) {
|
if (-not (Test-Administrator)) {
|
||||||
|
|
@ -351,19 +526,21 @@ if (Test-Path $CONFIG_FILE) {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
$GitName = Get-SecureInput "Git commit author name" -AsPlainText
|
$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 {
|
do {
|
||||||
$GitEmail = Get-SecureInput "Git commit author email" -AsPlainText
|
$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
|
# Create config file with restricted permissions from the start
|
||||||
@"
|
# First create empty file with restricted ACL, then write content
|
||||||
GIT_NAME="$GitName"
|
$null = New-Item -Path $CONFIG_FILE -ItemType File -Force
|
||||||
GIT_EMAIL="$GitEmail"
|
|
||||||
"@ | Out-File -FilePath $CONFIG_FILE -Encoding UTF8
|
|
||||||
|
|
||||||
# Restrict permissions (Windows equivalent of chmod 600)
|
|
||||||
$acl = Get-Acl $CONFIG_FILE
|
$acl = Get-Acl $CONFIG_FILE
|
||||||
$acl.SetAccessRuleProtection($true, $false)
|
$acl.SetAccessRuleProtection($true, $false)
|
||||||
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||||
|
|
@ -374,38 +551,93 @@ GIT_EMAIL="$GitEmail"
|
||||||
$acl.SetAccessRule($rule)
|
$acl.SetAccessRule($rule)
|
||||||
Set-Acl -Path $CONFIG_FILE -AclObject $acl
|
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-Host ""
|
||||||
Write-Status "Config saved to: $CONFIG_FILE" "Success"
|
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)
|
# Prompt for VM password (not stored)
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
do {
|
do {
|
||||||
$VMPassword = Get-SecureInput "VM user password (min 8 chars, for 'dev' user)"
|
$VMPassword = Get-SecureInput "VM user password (min 8 chars, no quotes/backslashes)"
|
||||||
} while ($VMPassword.Length -lt 8)
|
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
|
# Prompt for VNC password if VNC is enabled
|
||||||
$VNCPassword = ""
|
$VNCPassword = ""
|
||||||
if (-not $SkipVNC) {
|
if (-not $SkipVNC) {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
do {
|
do {
|
||||||
$VNCPassword = Get-SecureInput "VNC password (min 6 chars, for remote desktop)"
|
$VNCPassword = Get-SecureInput "VNC password (exactly 8 chars, no special shell chars)"
|
||||||
} while ($VNCPassword.Length -lt 6)
|
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 $IMAGES_PATH -Force | Out-Null
|
||||||
New-Item -ItemType Directory -Path "$VMS_PATH\$VMName" -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)) {
|
# Get expected checksum
|
||||||
Write-Status "Downloading Ubuntu $UBUNTU_VERSION cloud image (~700MB)..." "Info"
|
$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 " URL: $UBUNTU_IMAGE_URL" -ForegroundColor Gray
|
||||||
Write-Host " This may take a few minutes..." -ForegroundColor Gray
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Use BITS for better download handling
|
# Use BITS for better download handling
|
||||||
|
|
@ -422,19 +654,37 @@ if (-not (Test-Path $imagePath)) {
|
||||||
}
|
}
|
||||||
Complete-BitsTransfer -BitsJob $bitsJob
|
Complete-BitsTransfer -BitsJob $bitsJob
|
||||||
Write-Status "Download complete" "Success"
|
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 {
|
} catch {
|
||||||
# Fallback to Invoke-WebRequest
|
# Fallback to Invoke-WebRequest
|
||||||
Write-Status "BITS transfer failed, trying direct download..." "Warning"
|
Write-Status "BITS transfer failed, trying direct download..." "Warning"
|
||||||
try {
|
try {
|
||||||
Invoke-WebRequest -Uri $UBUNTU_IMAGE_URL -OutFile $imagePath -UseBasicParsing
|
Invoke-WebRequest -Uri $UBUNTU_IMAGE_URL -OutFile $imagePath -UseBasicParsing
|
||||||
Write-Status "Download complete" "Success"
|
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 {
|
} catch {
|
||||||
Write-Status "Failed to download Ubuntu image: $_" "Error"
|
Write-Status "Failed to download Ubuntu image: $_" "Error"
|
||||||
exit 1
|
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"
|
$vmDiskPath = "$VMS_PATH\$VMName\disk.vhdx"
|
||||||
|
|
||||||
Write-Status "Creating VM disk ($DiskGB GB)..." "Info"
|
Write-Status "Creating VM disk ($DiskGB GB)..." "Info"
|
||||||
Copy-Item -Path $imagePath -Destination $vmDiskPath -Force
|
try {
|
||||||
Resize-VHD -Path $vmDiskPath -SizeBytes ($DiskGB * 1GB)
|
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
|
# Generate SSH Key
|
||||||
|
|
@ -580,8 +837,9 @@ instance-id: $VMName-$(Get-Date -Format 'yyyyMMddHHmmss')
|
||||||
local-hostname: $VMName
|
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"
|
$cloudInitISO = "$VMS_PATH\$VMName\cloud-init.iso"
|
||||||
|
$script:CleanupResources.ISOPath = $cloudInitISO
|
||||||
|
|
||||||
Write-Status "Creating cloud-init ISO..." "Info"
|
Write-Status "Creating cloud-init ISO..." "Info"
|
||||||
try {
|
try {
|
||||||
|
|
@ -590,6 +848,7 @@ try {
|
||||||
throw "ISO file was not created"
|
throw "ISO file was not created"
|
||||||
}
|
}
|
||||||
Write-Status "Cloud-init ISO created" "Success"
|
Write-Status "Cloud-init ISO created" "Success"
|
||||||
|
Write-Log "Cloud-init ISO contains passwords - will be removed after first boot"
|
||||||
} catch {
|
} catch {
|
||||||
Write-Status "Failed to create cloud-init ISO: $_" "Error"
|
Write-Status "Failed to create cloud-init ISO: $_" "Error"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
@ -601,6 +860,7 @@ try {
|
||||||
Write-Host "2. Install genisoimage in WSL:" -ForegroundColor Cyan
|
Write-Host "2. Install genisoimage in WSL:" -ForegroundColor Cyan
|
||||||
Write-Host " wsl sudo apt-get install genisoimage" -ForegroundColor White
|
Write-Host " wsl sudo apt-get install genisoimage" -ForegroundColor White
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
Invoke-Cleanup -OnError
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -614,53 +874,64 @@ Write-Host " Memory: $MemoryGB GB (dynamic)" -ForegroundColor Gray
|
||||||
Write-Host " Disk: $DiskGB GB" -ForegroundColor Gray
|
Write-Host " Disk: $DiskGB GB" -ForegroundColor Gray
|
||||||
Write-Host " Switch: $($vmSwitch.Name)" -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 {
|
try {
|
||||||
Set-VMProcessor -VM $vm -ExposeVirtualizationExtensions $true
|
# Create VM (Generation 2 for UEFI)
|
||||||
Write-Host " Nested virtualization: enabled" -ForegroundColor Gray
|
$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 {
|
} 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
|
# Start VM and Wait for Provisioning
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -695,18 +966,37 @@ Write-Host ""
|
||||||
if ($vmIP) {
|
if ($vmIP) {
|
||||||
Write-Status "VM IP: $vmIP" "Success"
|
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"
|
$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"
|
$hostsEntry = "$vmIP`t$VMName.local"
|
||||||
|
|
||||||
# Remove old entry if exists
|
try {
|
||||||
$hostsContent = Get-Content $hostsFile -ErrorAction SilentlyContinue
|
# Create backup before modification
|
||||||
$hostsContent = $hostsContent | Where-Object { $_ -notmatch "\s+$VMName\.local\s*$" }
|
if (Test-Path $hostsFile) {
|
||||||
|
Copy-Item -Path $hostsFile -Destination $hostsBackup -Force
|
||||||
|
Write-Log "Created hosts file backup: $hostsBackup"
|
||||||
|
}
|
||||||
|
|
||||||
# Add new entry
|
# Remove old entry if exists
|
||||||
$hostsContent += $hostsEntry
|
$hostsContent = Get-Content $hostsFile -ErrorAction SilentlyContinue
|
||||||
$hostsContent | Set-Content $hostsFile -Force
|
$hostsContent = $hostsContent | Where-Object { $_ -notmatch "\s+$VMName\.local\s*$" }
|
||||||
Write-Status "Updated hosts file: $VMName.local -> $vmIP" "Info"
|
|
||||||
|
# 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
|
# Wait a moment for SSH to be ready
|
||||||
Write-Status "Waiting for SSH to become available..." "Info"
|
Write-Status "Waiting for SSH to become available..." "Info"
|
||||||
|
|
@ -768,7 +1058,19 @@ if ($vmIP) {
|
||||||
Write-Host ""
|
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 "============================================================================" -ForegroundColor Green
|
||||||
Write-Host " Security: Full Hyper-V isolation (separate kernel, network, filesystem)" -ForegroundColor Green
|
Write-Host " Security: Full Hyper-V isolation (separate kernel, network, filesystem)" -ForegroundColor Green
|
||||||
Write-Host "============================================================================" -ForegroundColor Green
|
Write-Host "============================================================================" -ForegroundColor Green
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Log "Setup completed successfully for VM: $VMName"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue