├── .github └── CODEOWNERS ├── .gitignore ├── README.md └── powershell ├── .gitignore ├── DevolutionsLabs.psm1 ├── ad_init.ps1 ├── admf ├── Domain │ ├── GroupMemberships │ │ └── membership.json │ ├── Groups │ │ └── groups.json │ ├── Names │ │ └── variables.json │ ├── OrganizationalUnits │ │ └── ous.json │ └── Users │ │ └── users.json └── context.json ├── alpine.apkovl.tar.gz ├── build.ps1 ├── common.ps1 ├── dc_vm.ps1 ├── dvls_vm.ps1 ├── golden.ps1 ├── gw_vm.ps1 ├── host_init.ps1 ├── host_sync.ps1 ├── host_vm.ps1 ├── licensing.json ├── rdm_init.ps1 ├── rdm_vm.ps1 ├── rds_farm.ps1 ├── rtr_vm.ps1 ├── scripts ├── Fix-HyperVNetworkAdapters.ps1 └── New-CertificateTemplate.ps1 ├── test_vm.ps1 ├── unattend.sh └── unattend.xml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # File auto-generated and managed by Devops 2 | /.github/ @devolutions/devops 3 | /.github/dependabot.yml @devolutions/security-managers 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .vs/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Devolutions Labs 3 | 4 | ## Prerequisites 5 | 6 | This Hyper-V lab is designed to work properly on a Windows host with 32GB of RAM, alongside common development tools, and with minimal disk usage. 7 | 8 | If you have never set up PowerShell, use the following to change the default execution policy and update it from an elevated shell: 9 | 10 | ```powershell 11 | Set-ExecutionPolicy Unrestricted -Force 12 | Install-PackageProvider Nuget -Force 13 | Install-Module -Name PowerShellGet -Force 14 | ``` 15 | 16 | If you do not have a package manager already (winget, choco), use the following code snippet to install one: 17 | 18 | ```powershell 19 | if (-Not (Get-Command -Name winget -CommandType Application -ErrorAction SilentlyContinue)) { 20 | iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 21 | } 22 | ``` 23 | 24 | You can now install Hyper-V including the management tools (very important!). Manually reboot once this is done: 25 | 26 | ```powershell 27 | Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -NoRestart 28 | ``` 29 | 30 | On Windows Server, the command is slightly different: 31 | 32 | ```powershell 33 | Install-WindowsFeature -Name Hyper-V -IncludeManagementTools 34 | ``` 35 | 36 | In order to use Hyper-V from an unelevated shell, add yourself to the local Hyper-V Administrators group: 37 | 38 | ```powershell 39 | $CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name 40 | if (-Not (Get-LocalGroupMember -Group "Hyper-V Administrators" -Member $CurrentUser -ErrorAction SilentlyContinue)) { 41 | Add-LocalGroupMember -Group "Hyper-V Administrators" -Member @($CurrentUser) 42 | } 43 | ``` 44 | 45 | While optional, it is highly recommended to use Windows Terminal instead of the old Windows console host. Install it quickly using chocolatey: 46 | 47 | ```powershell 48 | choco install -y --no-progress microsoft-windows-terminal 49 | ``` 50 | 51 | Last but not least, install PowerShell 7 using an elevated Windows PowerShell terminal: 52 | 53 | ```powershell 54 | &([ScriptBlock]::Create((irm "https://aka.ms/install-powershell.ps1"))) -UseMSI -Quiet 55 | ``` 56 | 57 | At this point, it is advised to reboot. 58 | 59 | From this point forward, always use PowerShell 7, as Windows PowerShell compatibility is not guaranteed. 60 | 61 | ## Host Initialization 62 | 63 | Open an elevated PowerShell prompt, and move to the "powershell" directory of this repository containing all the scripts. 64 | 65 | Make sure the script files are unblocked for execution if they've been downloaded from a browser (Mark-of-the-Web): 66 | 67 | ```powershell 68 | Get-ChildItem . -Recurse | Unblock-File 69 | ``` 70 | 71 | Run the host_init.ps1 script to initialize the host environment: 72 | 73 | ```powershell 74 | .\host_init.ps1 75 | ``` 76 | 77 | You may need to reboot the host for the Hyper-V feature installation to complete. 78 | 79 | ## Golden Image 80 | 81 | Download the latest Windows Server .iso file (*_windows_server_2025_*.iso). This is the regular Windows Server ISO which is only available to those with the right Visual Studio (MSDN) subscription, not the evaluation ISO available publicly. Ask someone on your team for a download link (hint: the person maintaining these scripts). Copy the iso file to "C:\Hyper-V\ISOs", then create the golden virtual machine image: 82 | 83 | ```powershell 84 | .\golden.ps1 85 | ``` 86 | 87 | The process takes about an hour to complete, and creates a clean virtual hard disk image containing everything we need for all the virtual machines in the lab. 88 | 89 | ## Virtual Machines 90 | 91 | Launch the script to build the isolated lab of virtual machines: 92 | 93 | ```powershell 94 | .\build.ps1 95 | ``` 96 | 97 | All virtual machines are created in order using the golden image. The entire process takes about an hour to complete. 98 | 99 | ## Host Synchronization 100 | 101 | Last but not least, run the host synchronization script to make it possible to reach the virtual machines with the proper hostnames. The script also imports the root certificate authority from the lab, such that HTTPS will work properly inside the browser of the host. 102 | 103 | ```powershell 104 | .\host_sync.ps1 105 | ``` 106 | 107 | ## Remote Desktop Manager 108 | 109 | Launch Remote Desktop Manager, then run the rdm_init.ps1 script to create and initialize a new data source called "IT-HELP-LAB" 110 | 111 | ```powershell 112 | .\rdm_init.ps1 113 | ``` 114 | 115 | You will need to restart Remote Desktop Manager after running the script to see the new data source in the list. This part usually doesn't work well, so if "IT-HELP-LAB" is missing from the list, simply create an SQLite data source of the same name using "%LocalAppData%\Devolutions\RemoteDesktopManager\IT-HELP-LAB.db" as database file. 116 | -------------------------------------------------------------------------------- /powershell/.gitignore: -------------------------------------------------------------------------------- 1 | ADAccounts.json 2 | licensing.json 3 | -------------------------------------------------------------------------------- /powershell/DevolutionsLabs.psm1: -------------------------------------------------------------------------------- 1 | if (-Not (Test-Path 'variable:global:IsWindows')) { 2 | $script:IsWindows = $true; # Windows PowerShell 5.1 or earlier 3 | } 4 | 5 | if ($IsWindows) { 6 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; 7 | } 8 | 9 | function Write-DLabLog { 10 | param ( 11 | [Parameter(Mandatory = $true, Position = 0)] 12 | [string] $Message 13 | ) 14 | 15 | $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") 16 | Write-Host "[$timestamp] $Message" 17 | } 18 | 19 | function Get-DLabIpAddress 20 | { 21 | [CmdletBinding()] 22 | param( 23 | [Parameter(Mandatory=$true,Position=0)] 24 | [string] $NetworkBase, 25 | [Parameter(Mandatory=$true,Position=1)] 26 | [int] $HostNumber 27 | ) 28 | 29 | $([IPAddress] (([IPAddress] $NetworkBase).Address + ([IPAddress] "0.0.0.$HostNumber").Address)).ToString() 30 | } 31 | 32 | function Get-DLabPath 33 | { 34 | [CmdletBinding()] 35 | param( 36 | [Parameter(Mandatory=$true,Position=0)] 37 | [ValidateSet("ISOs","IMGs","VHDs","VFDs","ChildDisks","ParentDisks")] 38 | [string] $PathName 39 | ) 40 | 41 | $HyperVPath = if (Test-Path Env:DLAB_HOME) { $Env:DLAB_HOME } else { "C:\Hyper-V" } 42 | 43 | switch ($PathName) { 44 | "ISOs" { Join-Path $HyperVPath "ISOs" } 45 | "IMGs" { Join-Path $HyperVPath "IMGs" } 46 | "VHDs" { Join-Path $HyperVPath "VHDs" } 47 | "ChildDisks" { Join-Path $HyperVPath "VHDs" } 48 | "ParentDisks" { Join-Path $HyperVPath "IMGs" } 49 | } 50 | } 51 | 52 | function Get-DLabIsoFilePath 53 | { 54 | [CmdletBinding()] 55 | param( 56 | [Parameter(Mandatory=$true,Position=0)] 57 | [string] $Name 58 | ) 59 | 60 | $IsoPath = Get-DLabPath "ISOs" 61 | $(Get-ChildItem -Path $IsoPath "*$Name*.iso" | Sort-Object LastWriteTime -Descending)[0] 62 | } 63 | 64 | function Get-DLabParentDiskFilePath 65 | { 66 | [CmdletBinding()] 67 | param( 68 | [Parameter(Mandatory=$true,Position=0)] 69 | [string] $Name 70 | ) 71 | 72 | $ParentDisksPath = Get-DLabPath "IMGs" 73 | $(Get-ChildItem -Path $ParentDisksPath "*$Name*.vhdx" | Sort-Object LastWriteTime -Descending)[0] 74 | } 75 | 76 | function Get-DLabWindowsImageListFromIso 77 | { 78 | [CmdletBinding()] 79 | param( 80 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] 81 | [string] $IsoPath 82 | ) 83 | 84 | begin {} 85 | 86 | process { 87 | # Resolve and verify the path *before* trying to mount 88 | $resolvedIsoPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($IsoPath) 89 | if (-not (Test-Path $resolvedIsoPath)) { 90 | throw "ISO file not found: $resolvedIsoPath" 91 | } 92 | 93 | # Scope mount outside try/finally for dismount 94 | $mount = $null 95 | $resolvedWimPath = $null 96 | 97 | try { 98 | # Mount the ISO 99 | $mount = Mount-DiskImage -ImagePath $resolvedIsoPath -PassThru -ErrorAction Stop 100 | $volumes = Get-Volume -DiskImage $mount -ErrorAction SilentlyContinue 101 | if (-not $volumes) { 102 | throw "Failed to mount ISO. Ensure the file is valid and you have permission." 103 | } 104 | 105 | $driveLetter = $volumes.DriveLetter + ":" 106 | Write-Verbose "Mounted ISO at $driveLetter" 107 | 108 | # Locate WIM/ESD 109 | $resolvedWimPath = Join-Path $driveLetter "sources\install.wim" 110 | if (-not (Test-Path $resolvedWimPath)) { 111 | $resolvedWimPath = Join-Path $driveLetter "sources\install.esd" 112 | } 113 | 114 | if (-not (Test-Path $resolvedWimPath)) { 115 | throw "Neither install.wim nor install.esd found in $driveLetter\sources" 116 | } 117 | 118 | # Run DISM and check exit code 119 | $dismOutput = dism /Get-WimInfo /WimFile:$resolvedWimPath /English 120 | if ($LASTEXITCODE -eq 740) { 121 | throw "DISM failed with error 740: Elevated permissions are required. Please run this session as Administrator." 122 | } 123 | elseif ($LASTEXITCODE -ne 0) { 124 | throw "DISM failed with exit code $LASTEXITCODE. See output for details." 125 | } 126 | 127 | # Parse DISM output 128 | $imageInfo = @() 129 | $current = @{} 130 | foreach ($line in $dismOutput) { 131 | if ($line -match '^Index\s+:\s+(\d+)') { 132 | $current = @{ Index = [int]$matches[1] } 133 | } elseif ($line -match '^Name\s+:\s+(.+)$') { 134 | $current.Name = $matches[1].Trim() 135 | } elseif ($line -match '^Description\s+:\s+(.+)$') { 136 | $current.Description = $matches[1].Trim() 137 | $imageInfo += [PSCustomObject]$current 138 | $current = @{} 139 | } 140 | } 141 | 142 | return $imageInfo | Where-Object { $_.Index -and $_.Name } 143 | } 144 | finally { 145 | # Only attempt to dismount if it was successfully mounted 146 | if ($mount) { 147 | try { 148 | Dismount-DiskImage -ImagePath $resolvedIsoPath -ErrorAction SilentlyContinue | Out-Null 149 | Write-Verbose "ISO unmounted." 150 | } catch { 151 | Write-Warning "Failed to unmount ISO: $_" 152 | } 153 | } 154 | } 155 | } 156 | 157 | end {} 158 | } 159 | 160 | function New-DLabIsoFile 161 | { 162 | [CmdletBinding()] 163 | param( 164 | [Parameter(Mandatory=$true,Position=0)] 165 | [string] $Path, 166 | [Parameter(Mandatory=$true,Position=1)] 167 | [string] $Destination, 168 | [Parameter(Mandatory=$true)] 169 | [string] $VolumeName, 170 | [switch] $IncludeRoot, 171 | [switch] $Force 172 | ) 173 | 174 | # https://blog.apps.id.au/powershell-tools-create-an-iso/ 175 | # http://blogs.msdn.com/b/opticalstorage/archive/2010/08/13/writing-optical-discs-using-imapi-2-in-powershell.aspx 176 | # http://tools.start-automating.com/Install-ExportISOCommand/ 177 | # http://stackoverflow.com/a/9802807/223837 178 | 179 | # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364840.aspx 180 | $fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage 181 | $fsi.FileSystemsToCreate = 4 # FsiFileSystemUDF 182 | $fsi.FreeMediaBlocks = 0 183 | $fsi.VolumeName = $VolumeName 184 | $fsi.Root.AddTree($Path, $IncludeRoot) 185 | $istream = $fsi.CreateResultImage().ImageStream 186 | 187 | $Options = if ($PSEdition -eq 'Core') { 188 | @{ CompilerOptions = "/unsafe" } 189 | } else { 190 | $cp = New-Object CodeDom.Compiler.CompilerParameters 191 | $cp.CompilerOptions = "/unsafe" 192 | $cp.WarningLevel = 4 193 | $cp.TreatWarningsAsErrors = $true 194 | @{ CompilerParameters = $cp } 195 | } 196 | 197 | Add-Type @Options -TypeDefinition @" 198 | using System; 199 | using System.IO; 200 | using System.Runtime.InteropServices.ComTypes; 201 | 202 | namespace IsoHelper { 203 | public static class FileUtil { 204 | public static void WriteIStreamToFile(object i, string fileName) { 205 | IStream inputStream = i as IStream; 206 | FileStream outputFileStream = File.OpenWrite(fileName); 207 | int bytesRead = 0; 208 | int offset = 0; 209 | byte[] data; 210 | do { 211 | data = Read(inputStream, 2048, out bytesRead); 212 | outputFileStream.Write(data, 0, bytesRead); 213 | offset += bytesRead; 214 | } while (bytesRead == 2048); 215 | outputFileStream.Flush(); 216 | outputFileStream.Close(); 217 | } 218 | 219 | unsafe static private byte[] Read(IStream stream, int toRead, out int read) { 220 | byte[] buffer = new byte[toRead]; 221 | int bytesRead = 0; 222 | int* ptr = &bytesRead; 223 | stream.Read(buffer, toRead, (IntPtr)ptr); 224 | read = bytesRead; 225 | return buffer; 226 | } 227 | } 228 | } 229 | "@ 230 | 231 | [IsoHelper.FileUtil]::WriteIStreamToFile($istream, $Destination) 232 | } 233 | 234 | function New-DLabFormattedDisk 235 | { 236 | [CmdletBinding()] 237 | param( 238 | [Parameter(Mandatory=$true,Position=0)] 239 | [string] $DiskPath, 240 | [Parameter(Mandatory=$true)] 241 | [UInt64] $DiskSize, 242 | [ValidateSet("MBR","GPT")] 243 | [string] $PartitionStyle = "GPT", 244 | [ValidateSet("FAT32","NTFS")] 245 | [string] $FileSystem = "NTFS", 246 | [Parameter(Mandatory=$true)] 247 | [string] $FileSystemLabel, 248 | [switch] $MountDisk, 249 | [switch] $Force 250 | ) 251 | 252 | if (Test-Path $DiskPath -PathType 'Leaf') { 253 | if ($Force) { 254 | Remove-Item -Path $DiskPath 255 | } else { 256 | throw "`"$DiskPath`" already exists!" 257 | } 258 | } 259 | 260 | $VirtualDisk = New-VHD -Path $DiskPath -Dynamic -SizeBytes $DiskSize 261 | $NewDisk = Mount-VHD -Path $VirtualDisk.Path -PassThru 262 | 263 | $NewDisk | Initialize-Disk -PartitionStyle $PartitionStyle | Out-Null 264 | $Partition = $NewDisk | New-Partition -AssignDriveLetter -UseMaximumSize 265 | $Partition | Format-Volume -FileSystem $FileSystem -NewFileSystemLabel $FileSystemLabel | Out-Null 266 | 267 | if (-Not $MountDisk) { 268 | Dismount-VHD -Path $DiskPath | Out-Null 269 | } 270 | 271 | $NewDisk 272 | } 273 | 274 | function New-DLabParentDisk 275 | { 276 | [CmdletBinding()] 277 | param( 278 | [Parameter(Mandatory=$true,Position=0)] 279 | [string] $Name, 280 | [UInt64] $DiskSize, 281 | [switch] $Force 282 | ) 283 | 284 | $ParentDisksPath = Get-DLabPath "IMGs" 285 | $ParentDiskFileName = $Name, 'vhdx' -Join '.' 286 | $ParentDiskPath = Join-Path $ParentDisksPath $ParentDiskFileName 287 | 288 | if (Test-Path $ParentDiskPath -PathType 'Leaf') { 289 | if ($Force) { 290 | Remove-Item -Path $ParentDiskPath 291 | } else { 292 | throw "`"$ParentDiskPath`" already exists!" 293 | } 294 | } 295 | 296 | $Params = @{ 297 | Path = $ParentDiskPath; 298 | Dynamic = $true; 299 | } 300 | 301 | if ($PSBoundParameters.ContainsKey('DiskSize')) { 302 | $Params['SizeBytes'] = $DiskSize; 303 | } 304 | 305 | New-VHD @Params 306 | } 307 | 308 | function New-DLabChildDisk 309 | { 310 | [CmdletBinding()] 311 | param( 312 | [Parameter(Mandatory=$true,Position=0)] 313 | [string] $Name, 314 | [Parameter(Mandatory=$true,Position=1)] 315 | [string] $ParentDiskPath, 316 | [switch] $Force 317 | ) 318 | 319 | if (-Not (Test-Path $ParentDiskPath -PathType 'Leaf')) { 320 | throw "`"$ParentDiskPath`" cannot be found" 321 | } 322 | 323 | $ChildDisksPath = Get-DLabPath "VHDs" 324 | $ChildDiskFileName = $Name, 'vhdx' -Join '.' 325 | $ChildDiskPath = Join-Path $ChildDisksPath $ChildDiskFileName 326 | 327 | if (Test-Path $ChildDiskPath -PathType 'Leaf') { 328 | if ($Force) { 329 | Remove-Item -Path $ChildDiskPath 330 | } else { 331 | throw "`"$ChildDiskPath`" already exists!" 332 | } 333 | } 334 | 335 | New-VHD -Path $ChildDiskPath -ParentPath $ParentDiskPath -Differencing 336 | } 337 | 338 | function Test-DLabVM 339 | { 340 | [CmdletBinding()] 341 | param( 342 | [Parameter(Mandatory=$true,Position=0)] 343 | [string] $Name 344 | ) 345 | 346 | [bool]$(Get-VM $Name -ErrorAction SilentlyContinue) 347 | } 348 | 349 | function Find-7ZipExe { 350 | $7ZipExe = Get-Command -Name 7z -CommandType Application -ErrorAction SilentlyContinue | 351 | Select-Object -ExpandProperty Source -First 1 352 | if (-Not $7ZipExe) { 353 | $7ZipExe = @( 354 | "${Env:ProgramFiles}\7-Zip\7z.exe", 355 | "${Env:ProgramFiles(x86)}\7-Zip\7z.exe" 356 | ) | Where-Object { Test-Path $_ } | Select-Object -First 1 357 | } 358 | $7ZipExe 359 | } 360 | 361 | function Expand-AlpineOverlay 362 | { 363 | [CmdletBinding()] 364 | param( 365 | [Parameter(Mandatory=$true,Position=0)] 366 | [string] $InputFile, 367 | [Parameter(Mandatory=$true,Position=1)] 368 | [string] $Destination, 369 | [switch] $Force 370 | ) 371 | 372 | if (Test-Path $Destination) { 373 | if ($Force) { 374 | Remove-Item $Destination -Recurse -ErrorAction SilentlyContinue | Out-Null 375 | } else { 376 | throw "`"$Destination`" already exists!" 377 | } 378 | } 379 | 380 | $7ZipExe = Find-7ZipExe 381 | 382 | cmd.exe /c "`"$7ZipExe`" x $InputFile -so | `"$7ZipExe`" x -si -ttar -o`"$Destination`"" 383 | 384 | Push-Location 385 | Set-Location $Destination 386 | $RootPath = Get-Item . 387 | $ReparsePoints = Get-ChildItem . -Recurse | ` 388 | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint } 389 | $ReparsePoints | ForEach-Object { 390 | $Source = $_.FullName 391 | $Target = $_.Target.Replace('/','\') 392 | $Target = $Target.Substring($RootPath.FullName.Length) 393 | Push-Location 394 | Set-Location $_.Directory 395 | Remove-Item $Source | Out-Null 396 | & "cmd.exe" "/c" "mklink `"$Source`" `"$Target`"" 397 | #New-Item -ItemType SymbolicLink -Path $Source -Target $Target | Out-Null 398 | Pop-Location 399 | } 400 | Pop-Location 401 | } 402 | 403 | function Compress-AlpineOverlay 404 | { 405 | [CmdletBinding()] 406 | param( 407 | [Parameter(Mandatory=$true,Position=0)] 408 | [string] $InputPath, 409 | [Parameter(Mandatory=$true,Position=1)] 410 | [string] $Destination, 411 | [switch] $Force 412 | ) 413 | 414 | if (-Not (Test-Path $InputPath -PathType 'Container')) { 415 | throw "`"$InputPath`" does not exist or is not a directory" 416 | } 417 | 418 | if (-Not $Destination.EndsWith(".tar.gz")) { 419 | throw "`"$Destination`" does not end in .tar.gz" 420 | } 421 | 422 | if (-Not $Destination.EndsWith(".apkovl.tar.gz")) { 423 | Write-Warning -Message "`"$Destination`" does not end in .apkovl.tar.gz" 424 | } 425 | 426 | if (Test-Path $Destination) { 427 | if ($Force) { 428 | Remove-Item $Destination -ErrorAction SilentlyContinue | Out-Null 429 | } else { 430 | throw "VM `"$Destination`" already exists!" 431 | } 432 | } 433 | 434 | $TarFileName = $Destination.TrimEnd(".gz") | Split-Path -Leaf 435 | 436 | $7ZipExe = Find-7ZipExe 437 | 438 | cmd.exe /c "`"$7ZipExe`" a -ttar -snl -so $TarFileName `"$InputPath/*`" | `"$7ZipExe`" a -si $Destination" 439 | } 440 | 441 | function New-DLabRouterVM 442 | { 443 | [CmdletBinding()] 444 | param( 445 | [Parameter(Mandatory=$true,Position=0)] 446 | [string] $Name, 447 | [string] $Password, 448 | [Parameter(Mandatory=$true)] 449 | [string] $WanSwitchName, 450 | [Parameter(Mandatory=$true)] 451 | [string] $LanSwitchName, 452 | [string] $NetworkInterfaces, 453 | [string[]] $NameServers = @('1.1.1.1','1.0.0.1'), 454 | [string] $DnsMasqConf, 455 | [UInt64] $DiskSize = 1GB, 456 | [switch] $Force 457 | ) 458 | 459 | if (Test-DLabVM $Name) { 460 | if ($Force) { 461 | Stop-VM $Name -Force 462 | Remove-VM $Name -Force 463 | } else { 464 | throw "VM `"$Name`" already exists!" 465 | } 466 | } 467 | 468 | $IsoFilePath = $(Get-DLabIsoFilePath "alpine").FullName 469 | 470 | $ChildDisksPath = Get-DLabPath "VHDs" 471 | $DiskFileName = $Name, 'vhdx' -Join '.' 472 | $DiskPath = Join-Path $ChildDisksPath $DiskFileName 473 | 474 | $Params = @{ 475 | DiskPath = $DiskPath; 476 | DiskSize = $DiskSize; 477 | PartitionStyle = "MBR"; 478 | FileSystem = "FAT32"; 479 | FileSystemLabel = "APKOVL"; 480 | MountDisk = $true; 481 | } 482 | 483 | $AlpineDisk = New-DLabFormattedDisk @Params -Force:$Force 484 | 485 | $Volumes = $AlpineDisk | Get-Partition | Get-Volume | ` 486 | Sort-Object -Property Size -Descending 487 | $Volume = $Volumes[0] 488 | 489 | $MountPath = "$($Volume.DriveLetter)`:" 490 | $ApkOvlFileName = "alpine.apkovl.tar.gz" 491 | $OverlayFile = "$MountPath\$ApkOvlFileName" 492 | Copy-Item -Path "$PSScriptRoot\$ApkOvlFileName" -Destination $OverlayFile 493 | 494 | $TempPath = Join-Path $([System.IO.Path]::GetTempPath()) "apkovl-$Name" 495 | Remove-Item $TempPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null 496 | 497 | Expand-AlpineOverlay $OverlayFile -Destination $TempPath -Force 498 | 499 | $ResolvConf = $($NameServers | ForEach-Object { "nameserver $_" } | Out-String).Trim() 500 | Set-Content -Path $(Join-Path $TempPath "/etc/resolv.conf") -Value $ResolvConf -Encoding Utf8NoBOM 501 | 502 | if (-Not [string]::IsNullOrEmpty($NetworkInterfaces)) { 503 | Set-Content -Path $(Join-Path $TempPath "/etc/network/interfaces") -Value $NetworkInterfaces -Encoding Utf8NoBOM 504 | } 505 | 506 | if (-Not [string]::IsNullOrEmpty($DnsMasqConf)) { 507 | $EtcDnsMasqConf = $(Join-Path $TempPath "/etc/dnsmasq.conf") 508 | if (Test-Path $EtcDnsMasqConf) { 509 | Move-Item $EtcDnsMasqConf $(Join-Path $TempPath "/etc/dnsmasq.conf.bak") 510 | } 511 | Set-Content -Path $EtcDnsMasqConf -Value $DnsMasqConf -Encoding Utf8NoBOM 512 | } 513 | 514 | Compress-AlpineOverlay $TempPath -Destination $OverlayFile -Force 515 | Remove-Item $TempPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null 516 | 517 | $UnattendText = (Get-Content -Path "$PSScriptRoot\unattend.sh" -Raw) -Replace "`r`n", "`n" 518 | [IO.File]::WriteAllText($(Join-Path $MountPath "unattend.sh"), $UnattendText) 519 | 520 | Dismount-VHD -Path $AlpineDisk.Path 521 | 522 | $Params = @{ 523 | Name = $Name; 524 | VHDPath = $AlpineDisk.Path; 525 | MemoryStartupBytes = 1GB; 526 | } 527 | 528 | if ($WanSwitchName) { 529 | $Params['SwitchName'] = $WanSwitchName; 530 | } 531 | 532 | New-VM @Params 533 | 534 | Set-VMDvdDrive -VMName $Name -ControllerNumber 1 -Path $IsoFilePath 535 | 536 | $Params = @{ 537 | Name = $Name; 538 | ProcessorCount = 2; 539 | AutomaticStopAction = "Shutdown"; 540 | CheckpointType = "Disabled"; 541 | } 542 | 543 | Set-VM @Params 544 | 545 | Add-VMNetworkAdapter -VMName $Name -SwitchName $LanSwitchName 546 | } 547 | 548 | function New-DLabParentVM 549 | { 550 | [CmdletBinding()] 551 | param( 552 | [Parameter(Mandatory=$true,Position=0)] 553 | [string] $Name, 554 | [string] $Password, 555 | [string] $SwitchName, 556 | [Parameter(Mandatory=$true)] 557 | [string] $OSVersion, 558 | [string] $IsoFilePath, 559 | [UInt64] $DiskSize = 128GB, 560 | [switch] $Force 561 | ) 562 | 563 | if (Test-DLabVM $Name) { 564 | if ($Force) { 565 | Stop-VM $Name -Force 566 | Remove-VM $Name -Force 567 | } else { 568 | throw "VM `"$Name`" already exists!" 569 | } 570 | } 571 | 572 | $Generation = 1 573 | 574 | if ($OSVersion -eq '11') { 575 | $Generation = 2 576 | } 577 | 578 | if ([string]::IsNullOrEmpty($IsoFilePath)) { 579 | $IsoFilePath = $(Get-DLabIsoFilePath "windows_server_${OSVersion}").FullName 580 | } 581 | 582 | $ParentDisk = New-DLabParentDisk $Name -DiskSize $DiskSize -Force:$Force 583 | 584 | $Params = @{ 585 | Name = $Name; 586 | VHDPath = $ParentDisk.Path; 587 | MemoryStartupBytes = 4GB; 588 | Generation = $Generation; 589 | } 590 | 591 | if ($SwitchName) { 592 | $Params['SwitchName'] = $SwitchName; 593 | } 594 | 595 | New-VM @Params 596 | 597 | Set-VMDvdDrive -VMName $Name -ControllerNumber 1 -Path $IsoFilePath 598 | 599 | $Params = @{ 600 | Name = $Name; 601 | ProcessorCount = 4; 602 | AutomaticStopAction = "Shutdown"; 603 | CheckpointType = "Disabled"; 604 | } 605 | 606 | Set-VM @Params 607 | } 608 | 609 | function New-DLabVM 610 | { 611 | [CmdletBinding()] 612 | param( 613 | [Parameter(Mandatory=$true,Position=0)] 614 | [string] $Name, 615 | [string] $Password, 616 | [Int64] $MemoryBytes = 4GB, 617 | [Int64] $ProcessorCount = 4, 618 | [Parameter(Mandatory=$true)] 619 | [string] $OSVersion, 620 | [bool] $DynamicMemory = $true, 621 | [bool] $EnableVirtualization = $false, 622 | [switch] $Force 623 | ) 624 | 625 | if (Test-DLabVM $Name) { 626 | if ($Force) { 627 | Stop-VM $Name -Force 628 | Remove-VM $Name -Force 629 | } else { 630 | throw "VM `"$Name`" already exists!" 631 | } 632 | } 633 | 634 | $ParentDiskPath = Get-DLabParentDiskFilePath "Windows Server ${OSVersion} Standard" 635 | 636 | $ChildDisk = New-DLabChildDisk $Name -ParentDiskPath $ParentDiskPath -Force:$Force 637 | 638 | $MountedDisk = Mount-VHD -Path $ChildDisk.Path -PassThru 639 | 640 | $Volumes = $MountedDisk | Get-Partition | Get-Volume | ` 641 | Sort-Object -Property Size -Descending 642 | $Volume = $Volumes[0] 643 | 644 | $DriveLetter = $Volume.DriveLetter 645 | $PantherPath = "$DriveLetter`:\Windows\Panther" 646 | $AnswerFilePath = Join-Path $PantherPath "unattend.xml" 647 | 648 | $Params = @{ 649 | UserFullName = "devolutions"; 650 | UserOrganization = "IT-HELP"; 651 | ComputerName = $Name; 652 | AdministratorPassword = $Password; 653 | OSVersion = $OSVersion; 654 | UILanguage = "en-US"; 655 | UserLocale = "en-CA"; 656 | } 657 | 658 | New-DLabAnswerFile $AnswerFilePath @Params 659 | 660 | Dismount-VHD -Path $ChildDisk.Path 661 | 662 | $Params = @{ 663 | Name = $Name; 664 | VHDPath = $ChildDisk.Path; 665 | SwitchName = "LAN Switch"; 666 | } 667 | 668 | New-VM @Params 669 | 670 | if ($EnableVirtualization) { 671 | Set-VMProcessor -VMName $Name -ExposeVirtualizationExtensions $EnableVirtualization 672 | } 673 | 674 | if ($DynamicMemory) { 675 | $MemoryStartupBytes = ([math]::Floor(($MemoryBytes / 1MB * 0.8) / 2) * 2) * 1MB 676 | $MemoryMinimumBytes = $MemoryStartupBytes 677 | $MemoryMaximumBytes = $MemoryBytes 678 | } else { 679 | $MemoryStartupBytes = $MemoryBytes 680 | $MemoryMinimumBytes = $MemoryBytes 681 | $MemoryMaximumBytes = $MemoryBytes 682 | } 683 | 684 | $Params = @{ 685 | Name = $Name; 686 | ProcessorCount = $ProcessorCount; 687 | AutomaticStopAction = "Shutdown"; 688 | CheckpointType = "Disabled"; 689 | DynamicMemory = $DynamicMemory; 690 | MemoryStartupBytes = $MemoryStartupBytes; 691 | MemoryMinimumBytes = $MemoryMinimumBytes; 692 | MemoryMaximumBytes = $MemoryMaximumBytes; 693 | } 694 | 695 | Set-VM @Params 696 | } 697 | 698 | function New-DLabAnswerFile 699 | { 700 | [CmdletBinding()] 701 | param( 702 | [Parameter(Mandatory=$true,Position=0)] 703 | [string] $Path, 704 | [string] $ComputerName, 705 | [string] $UserFullName, 706 | [string] $UserOrganization, 707 | [string] $AdministratorPassword, 708 | [Parameter(Mandatory=$true)] 709 | [string] $OSVersion, 710 | [int] $ImageIndex, 711 | [string] $UILanguage = "en-US", 712 | [string] $UserLocale = "en-US", 713 | [string] $TimeZone = "Eastern Standard Time" 714 | ) 715 | 716 | $Path = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) 717 | 718 | $TemplateFile = Join-Path $PSScriptRoot "unattend.xml" 719 | $answer = [XML] $(Get-Content $TemplateFile) 720 | 721 | $windowsPE = $answer.unattend.settings | Where-Object { $_.pass -Like 'windowsPE' } 722 | 723 | $component = $windowsPE.component | Where-Object { $_.name -Like 'Microsoft-Windows-International-Core-WinPE' } 724 | 725 | $component.UILanguage = $UILanguage 726 | $component.UserLocale = $UserLocale 727 | 728 | $component = $windowsPE.component | Where-Object { $_.name -Like 'Microsoft-Windows-Setup' } 729 | 730 | if (-Not [string]::IsNullOrEmpty($UserFullName)) { 731 | $component.UserData.FullName = $UserFullName 732 | } 733 | 734 | if (-Not [string]::IsNullOrEmpty($UserOrganization)) { 735 | $component.UserData.Organization = $UserOrganization 736 | } 737 | 738 | if ($OSVersion -eq '11') { 739 | $OSImageName = "Windows $OSVersion Enterprise" 740 | } else { 741 | $OSImageName = "Windows Server $OSVersion SERVERSTANDARD" 742 | } 743 | 744 | if ($ImageIndex -gt 0) { 745 | $component.ImageInstall.OSImage.InstallFrom.MetaData.Key = "/IMAGE/INDEX" 746 | $component.ImageInstall.OSImage.InstallFrom.MetaData.Value = $ImageIndex 747 | } else { 748 | $component.ImageInstall.OSImage.InstallFrom.MetaData.Key = "/IMAGE/NAME" 749 | $component.ImageInstall.OSImage.InstallFrom.MetaData.Value = $OSImageName 750 | } 751 | 752 | $specialize = $answer.unattend.settings | Where-Object { $_.pass -Like 'specialize' } 753 | 754 | $component = $specialize.component | Where-Object { $_.name -Like 'Microsoft-Windows-International-Core' } 755 | 756 | $component.UILanguage = $UILanguage 757 | $component.UserLocale = $UserLocale 758 | 759 | $component = $specialize.component | Where-Object { $_.name -Like 'Microsoft-Windows-Shell-Setup' } 760 | 761 | if (-Not [string]::IsNullOrEmpty($ComputerName)) { 762 | $component.ComputerName = $ComputerName 763 | } 764 | 765 | $oobeSystem = $answer.unattend.settings | Where-Object { $_.pass -Like 'oobeSystem' } 766 | $component = $oobeSystem.component | Where-Object { $_.name -Like 'Microsoft-Windows-Shell-Setup' } 767 | 768 | if (-Not [string]::IsNullOrEmpty($AdministratorPassword)) { 769 | $component.UserAccounts.AdministratorPassword.Value = $AdministratorPassword 770 | } 771 | 772 | if (-Not [string]::IsNullOrEmpty($TimeZone)) { 773 | $component.TimeZone = $TimeZone 774 | } 775 | 776 | $answer.Save($Path) 777 | } 778 | 779 | function Start-DLabVM 780 | { 781 | [CmdletBinding()] 782 | param( 783 | [Parameter(Mandatory=$true,Position=0)] 784 | [string] $VMName, 785 | [string] $UserName, 786 | [string] $Password, 787 | [int] $Timeout = 60, 788 | [switch] $Force 789 | ) 790 | 791 | if (-Not $(Test-DLabVM $VMName)) { 792 | throw "VM `"$VMName`" does not exist" 793 | } 794 | 795 | Start-VM $VMName 796 | } 797 | 798 | function Get-DLabVMUptime 799 | { 800 | [CmdletBinding()] 801 | param( 802 | [Parameter(Mandatory=$true,Position=0)] 803 | [string] $VMName 804 | ) 805 | 806 | if (-Not $(Test-DLabVM $VMName)) { 807 | throw "VM `"$VMName`" does not exist" 808 | } 809 | 810 | $(Get-VM $VMName).Uptime 811 | } 812 | 813 | function Wait-DLabVM 814 | { 815 | [CmdletBinding()] 816 | param( 817 | [Parameter(Mandatory=$true,Position=0)] 818 | [string] $VMName, 819 | [Parameter(Mandatory=$true,Position=1)] 820 | [ValidateSet("Heartbeat","IPAddress","Shutdown","Reboot","MemoryOperations","PSDirect")] 821 | [string] $Condition, 822 | [TimeSpan] $OldUptime, 823 | [string] $UserName, 824 | [string] $Password, 825 | [int] $Timeout = 60, 826 | [switch] $Force 827 | ) 828 | 829 | if (-Not $(Test-DLabVM $VMName)) { 830 | throw "VM `"$VMName`" does not exist" 831 | } 832 | 833 | if ($Condition -eq 'PSDirect') { 834 | $Credential = Get-DLabCredential -UserName $UserName -Password $Password 835 | while ((Invoke-Command -VMName $VMName -Credential $Credential ` 836 | { "test" } -ErrorAction SilentlyContinue) -ne "test") { Start-Sleep 5 } 837 | } elseif ($Condition -eq 'Shutdown') { 838 | while ($(Get-VM $VMName).State -ne "Off") { Start-Sleep 5 } 839 | } elseif ($Condition -eq 'Reboot') { 840 | if (-Not $PSBoundParameters.ContainsKey('OldUptime')) { 841 | $OldUptime = $(Get-VM $VMName).Uptime 842 | } 843 | do { 844 | $NewUptime = $(Get-VM $VMName).Uptime 845 | Start-Sleep 5 846 | } 847 | while ($NewUptime -ge $OldUptime) 848 | } else { 849 | Wait-VM $VMName -For $Condition -Timeout $Timeout 850 | } 851 | } 852 | 853 | function Get-DLabCredential 854 | { 855 | [CmdletBinding()] 856 | param( 857 | [Parameter(Mandatory=$true,Position=0)] 858 | [string] $UserName = "Administrator", 859 | [string] $DomainName = ".\", 860 | [string] $Password 861 | ) 862 | 863 | if ([string]::IsNullOrEmpty($Password)) { 864 | $Credential = Get-Credential -UserName $UserName 865 | if ($PSEdition -eq 'Desktop') { 866 | $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) 867 | $Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) 868 | } else { 869 | $Password = ConvertFrom-SecureString -SecureString $SecureString -AsPlainText 870 | } 871 | } else { 872 | $SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force 873 | $Credential = New-Object System.Management.Automation.PSCredential @($UserName, $SecurePassword) 874 | } 875 | 876 | $Credential 877 | } 878 | 879 | function New-DLabVMSession 880 | { 881 | [CmdletBinding()] 882 | param( 883 | [Parameter(Mandatory=$true,Position=0)] 884 | [string] $VMName, 885 | [string] $UserName = "Administrator", 886 | [string] $DomainName = ".\", 887 | [string] $Password, 888 | [string] $ConfigurationName 889 | ) 890 | 891 | $Credential = Get-DLabCredential -UserName $UserName -DomainName $DomainName -Password $Password 892 | 893 | $Params = @{ 894 | VMName = $VMName 895 | Credential = $Credential 896 | } 897 | 898 | if (-Not [string]::IsNullOrEmpty($ConfigurationName)) { 899 | $Params.ConfigurationName = $ConfigurationName 900 | } 901 | 902 | New-PSSession @Params 903 | } 904 | 905 | function Set-DLabVMNetAdapter 906 | { 907 | [CmdletBinding()] 908 | param( 909 | [Parameter(Mandatory=$true,Position=0)] 910 | [string] $VMName, 911 | [Parameter(Mandatory=$true)] 912 | [System.Management.Automation.Runspaces.PSSession] $VMSession, 913 | [Parameter(Mandatory=$true)] 914 | [string] $SwitchName, 915 | [Parameter(Mandatory=$true)] 916 | [string] $NetAdapterName, 917 | [Parameter(Mandatory=$true)] 918 | [string] $IPAddress, 919 | [Parameter(Mandatory=$true)] 920 | [string] $DefaultGateway, 921 | [Parameter(Mandatory=$true)] 922 | [string] $DnsServerAddress, 923 | [bool] $RegisterAutomaticFix = $true 924 | ) 925 | 926 | $VMHostAdapters = Get-VMNetworkAdapter $VMName 927 | $Switch = $VMHostAdapters | Where-Object { $_.SwitchName -eq $SwitchName } 928 | $MacAddress = $Switch.MacAddress -Split '(.{2})' -Match '.' -Join '-' 929 | 930 | Invoke-Command -ScriptBlock { Param($MacAddress, $NetAdapterName, 931 | $IPAddress, $DefaultGateway, $DnsServerAddress, $RegisterAutomaticFix) 932 | $NetAdapter = Get-NetAdapter | Where-Object { $_.MacAddress -Like $MacAddress } 933 | Rename-NetAdapter -Name $NetAdapter.Name -NewName $NetAdapterName 934 | $Params = @{ 935 | IPAddress = $IPAddress; 936 | InterfaceAlias = $NetAdapterName; 937 | AddressFamily = "IPv4"; 938 | PrefixLength = 24; 939 | DefaultGateway = $DefaultGateway; 940 | } 941 | New-NetIPAddress @Params 942 | Set-DnsClientServerAddress -InterfaceAlias $NetAdapterName -ServerAddresses $DnsServerAddress 943 | Start-Sleep 5 944 | 945 | if ($RegisterAutomaticFix) { 946 | $IPAddressMatch = $IPAddress -replace '(\d+\.\d+\.\d+)\.\d+', '$1.*' 947 | $TaskName = "Fix-HyperVNetworkAdapters" 948 | $ScriptPath = "C:\tools\scripts\Fix-HyperVNetworkAdapters.ps1" 949 | $ArgString = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`" -IPAddressMatch `"$IPAddressMatch`"" 950 | $Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $ArgString 951 | $Trigger = New-ScheduledTaskTrigger -AtStartup 952 | $Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest 953 | Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Principal $Principal 954 | } 955 | } -Session $VMSession -ArgumentList @($MacAddress, $NetAdapterName, 956 | $IPAddress, $DefaultGateway, $DnsServerAddress, $RegisterAutomaticFix) 957 | } 958 | 959 | function Add-DLabVMToDomain 960 | { 961 | [CmdletBinding()] 962 | param( 963 | [Parameter(Mandatory=$true,Position=0)] 964 | [string] $VMName, 965 | [Parameter(Mandatory=$true)] 966 | [System.Management.Automation.Runspaces.PSSession] $VMSession, 967 | [Parameter(Mandatory=$true)] 968 | [string] $DomainName, 969 | [Parameter(Mandatory=$true)] 970 | [string] $DomainController, 971 | [Parameter(Mandatory=$true)] 972 | [string] $UserName, 973 | [Parameter(Mandatory=$true)] 974 | [string] $Password 975 | ) 976 | 977 | Invoke-Command -ScriptBlock { Param($DomainName, $DomainController, $UserName, $Password) 978 | $ConfirmPreference = "High" 979 | $SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force 980 | $Credential = New-Object System.Management.Automation.PSCredential @($UserName, $SecurePassword) 981 | while (-Not [bool](Resolve-DnsName -Name $DomainController -ErrorAction SilentlyContinue)) { 982 | Write-Host "Waiting for $DomainController..." 983 | Start-Sleep 5 984 | } 985 | Add-Computer -DomainName $DomainName -Credential $Credential -Restart 986 | } -Session $VMSession -ArgumentList @($DomainName, $DomainController, $UserName, $Password) 987 | } 988 | 989 | function Request-DLabCertificate 990 | { 991 | [CmdletBinding()] 992 | param( 993 | [Parameter(Mandatory=$true,Position=0)] 994 | [string] $VMName, 995 | [Parameter(Mandatory=$true)] 996 | [System.Management.Automation.Runspaces.PSSession] $VMSession, 997 | [Parameter(Mandatory=$true)] 998 | [string] $CommonName, 999 | [int] $KeyLength = 2048, 1000 | [string[]] $ExtendedKeyUsage = @('ServerAuthentication'), 1001 | [Parameter(Mandatory=$true)] 1002 | [string] $CACommonName, 1003 | [Parameter(Mandatory=$true)] 1004 | [string] $CAHostName, 1005 | [Parameter(Mandatory=$true)] 1006 | [string] $CertificateFile, 1007 | [Parameter(Mandatory=$true)] 1008 | [string] $Password 1009 | ) 1010 | 1011 | Invoke-Command -ScriptBlock { Param($CommonName, $KeyLength, $ExtendedKeyUsage, 1012 | $CAHostName, $CACommonName, $CertificateFile, $Password) 1013 | 1014 | $EkuNames = [ordered]@{ 1015 | CodeSigning="1.3.6.1.5.5.7.3.3"; 1016 | ServerAuthentication="1.3.6.1.5.5.7.3.1"; 1017 | ClientAuthentication="1.3.6.1.5.5.7.3.2"; 1018 | RemoteDesktopAuthentication="1.3.6.1.4.1.311.54.1.2"; 1019 | } 1020 | 1021 | $sb = [System.Text.StringBuilder]::new() 1022 | $ExtendedKeyUsage | ForEach-Object { 1023 | $EkuName = $_ 1024 | $EkuOid = $EkuNames[$EkuName] 1025 | $sb.AppendLine("OID = $EkuOid ; $EkuName") | Out-Null 1026 | } 1027 | $EnhancedKeyUsageExtension = $sb.ToString() 1028 | 1029 | $CertInf = @" 1030 | [NewRequest] 1031 | Subject = "CN=$CommonName" 1032 | Exportable = TRUE 1033 | KeyLength = $KeyLength 1034 | KeySpec = 1 1035 | KeyUsage = 0xA0 1036 | MachineKeySet = TRUE 1037 | 1038 | [RequestAttributes] 1039 | CertificateTemplate = "WebServer" 1040 | 1041 | [EnhancedKeyUsageExtension] 1042 | $EnhancedKeyUsageExtension 1043 | 1044 | [Extensions] 1045 | 2.5.29.17 = "{text}"; Subject Alternative Names (SANs) 1046 | _continue_ = "dns=$CommonName&" 1047 | "@ 1048 | 1049 | $TempPath = Join-Path $([System.IO.Path]::GetTempPath()) "certreq-$CommonName" 1050 | Remove-Item $TempPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null 1051 | New-Item -ItemType Directory -Path $TempPath -ErrorAction SilentlyContinue | Out-Null 1052 | 1053 | $TempInfFile = $(Join-Path $TempPath 'cert.inf') 1054 | $TempCsrFile = $(Join-Path $TempPath 'cert.csr') 1055 | $TempCerFile = $(Join-Path $TempPath 'cert.cer') 1056 | $TempRspFile = $(Join-Path $TempPath 'cert.rsp') 1057 | 1058 | Set-Content -Path $TempInfFile -Value $CertInf 1059 | 1060 | & 'certreq.exe' '-q' '-new' $TempInfFile $TempCsrFile 1061 | 1062 | $CAConfigName = "$CAHostName\$CACommonName" 1063 | & 'certreq.exe' '-q' '-submit' '-config' $CAConfigName $TempCsrFile $TempCerFile 1064 | 1065 | & 'certreq.exe' '-q' '-accept' $TempCerFile 1066 | 1067 | $Certificate = Get-ChildItem "cert:\LocalMachine\My" | 1068 | Where-Object { $_.Subject -eq "CN=$CommonName" } | Select-Object -First 1 1069 | 1070 | $SecurePassword = ConvertTo-SecureString -String $Password -Force -AsPlainText 1071 | 1072 | $Params = @{ 1073 | Cert = $Certificate; 1074 | ChainOption = "BuildChain"; 1075 | FilePath = $CertificateFile; 1076 | Password = $SecurePassword; 1077 | } 1078 | 1079 | Export-PfxCertificate @Params 1080 | 1081 | Get-ChildItem "cert:\LocalMachine\My" | 1082 | Where-Object { $_.Subject -eq "CN=$CommonName" } | 1083 | Remove-Item 1084 | 1085 | Remove-Item $TempPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null 1086 | 1087 | } -Session $VMSession -ArgumentList @($CommonName, $KeyLength, $ExtendedKeyUsage, 1088 | $CAHostName, $CACommonName, $CertificateFile, $Password) 1089 | } 1090 | 1091 | function Request-DLabRdpCertificate 1092 | { 1093 | [CmdletBinding()] 1094 | param( 1095 | [Parameter(Mandatory=$true,Position=0)] 1096 | [string] $VMName, 1097 | [Parameter(Mandatory=$true)] 1098 | [System.Management.Automation.Runspaces.PSSession] $VMSession, 1099 | [Parameter(Mandatory=$true)] 1100 | [string] $CACommonName, 1101 | [Parameter(Mandatory=$true)] 1102 | [string] $CAHostName, 1103 | [string] $CertificateFile = "~\Documents\rdp.pfx", 1104 | [string] $Password = "rdp123!" 1105 | ) 1106 | 1107 | $RdpServerName = Invoke-Command -ScriptBlock { 1108 | "$($Env:ComputerName).$($Env:UserDnsDomain.ToLower())" 1109 | } -Session $VMSession 1110 | 1111 | $RdpCertificateFile = $CertificateFile 1112 | $RdpCertificatePassword = $Password 1113 | 1114 | Request-DLabCertificate $VMName -VMSession $VMSession ` 1115 | -CommonName $RdpServerName ` 1116 | -ExtendedKeyUsage @('RemoteDesktopAuthentication') ` 1117 | -CAHostName $CAHostName -CACommonName $CACommonName ` 1118 | -CertificateFile $RdpCertificateFile -Password $RdpCertificatePassword 1119 | 1120 | Invoke-Command -ScriptBlock { Param($RdpServerName, $RdpCertificateFile, $RdpCertificatePassword) 1121 | $Params = @{ 1122 | FilePath = $RdpCertificateFile; 1123 | CertStoreLocation = "cert:\LocalMachine\My"; 1124 | Password = (ConvertTo-SecureString $RdpCertificatePassword -AsPlainText -Force); 1125 | Exportable = $true; 1126 | } 1127 | $RdpCertificate = Import-PfxCertificate @Params 1128 | $RdpCertificateThumbprint = $RdpCertificate.Thumbprint 1129 | 1130 | Get-CimInstance -ClassName Win32_TSGeneralSetting -Namespace ROOT\CIMV2\TerminalServices | 1131 | Set-CimInstance -Property @{ SSLCertificateSHA1Hash = $RdpCertificateThumbprint } 1132 | 1133 | } -Session $VMSession -ArgumentList @($RdpServerName, $RdpCertificateFile, $RdpCertificatePassword) 1134 | 1135 | Invoke-Command -ScriptBlock { 1136 | if (Test-Path Env:USERDOMAIN) { 1137 | $DomainUsers = "${Env:USERDOMAIN}\Domain Users" 1138 | Add-LocalGroupMember -Group "Remote Desktop Users" -Member $DomainUsers -ErrorAction SilentlyContinue 1139 | } 1140 | } -Session $VMSession 1141 | } 1142 | 1143 | function Initialize-DLabVncServer 1144 | { 1145 | [CmdletBinding()] 1146 | param( 1147 | [Parameter(Mandatory=$true,Position=0)] 1148 | [string] $VMName, 1149 | [Parameter(Mandatory=$true)] 1150 | [System.Management.Automation.Runspaces.PSSession] $VMSession 1151 | ) 1152 | 1153 | Invoke-Command -ScriptBlock { 1154 | $IniFile = "$Env:ProgramFiles\uvnc bvba\UltraVNC\ultravnc.ini" 1155 | $IniData = Get-Content $IniFile | Foreach-Object { 1156 | switch ($_) { 1157 | "MSLogonRequired=0" { "MSLogonRequired=1" } 1158 | "NewMSLogon=0" { "NewMSLogon=1" } 1159 | default { $_ } 1160 | } 1161 | } 1162 | $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False 1163 | [System.IO.File]::WriteAllLines($IniFile, $IniData, $Utf8NoBomEncoding) 1164 | 1165 | $AclFile = "$Env:ProgramFiles\uvnc bvba\UltraVNC\acl.txt" 1166 | $AclData = "allow`t0x00000003`t`"BUILTIN\Remote Desktop Users`"" 1167 | $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False 1168 | [System.IO.File]::WriteAllLines($AclFile, $AclData, $Utf8NoBomEncoding) 1169 | Start-Process -FilePath "$Env:ProgramFiles\uvnc bvba\UltraVNC\MSLogonACL.exe" -ArgumentList @('/i', '/o', $AclFile) -Wait -NoNewWindow 1170 | } -Session $VMSession 1171 | } 1172 | 1173 | function Initialize-DLabPSRemoting 1174 | { 1175 | [CmdletBinding()] 1176 | param( 1177 | [Parameter(Mandatory=$true,Position=0)] 1178 | [string] $VMName, 1179 | [Parameter(Mandatory=$true)] 1180 | [System.Management.Automation.Runspaces.PSSession] $VMSession 1181 | ) 1182 | 1183 | Invoke-Command -ScriptBlock { 1184 | $FullComputerName = [System.Net.DNS]::GetHostByName($Env:ComputerName).HostName 1185 | $Certificate = Get-ChildItem "cert:\LocalMachine\My" | Where-Object { 1186 | ($_.Subject -eq "CN=$FullComputerName") -and ($_.Issuer -ne $_.Subject) 1187 | } | Select-Object -First 1 1188 | $CertificateThumbprint = $Certificate.Thumbprint 1189 | 1190 | & winrm create winrm/config/Listener?Address=*+Transport=HTTPS "@{Hostname=`"$FullComputerName`";CertificateThumbprint=`"$CertificateThumbprint`"}" 1191 | & netsh advfirewall firewall add rule name="WinRM-HTTPS" dir=in localport=5986 protocol=TCP action=allow 1192 | } -Session $VMSession 1193 | } 1194 | 1195 | function Set-DLabVMAutologon 1196 | { 1197 | [CmdletBinding()] 1198 | param( 1199 | [Parameter(Mandatory=$true,Position=0)] 1200 | [string] $VMName, 1201 | [Parameter(Mandatory=$true)] 1202 | [string] $UserName, 1203 | [string] $DomainName = ".\", 1204 | [Parameter(Mandatory=$true)] 1205 | [string] $Password, 1206 | [switch] $Restart 1207 | ) 1208 | 1209 | if ([string]::IsNullOrEmpty($Password)) { 1210 | $Credential = Get-Credential -UserName $UserName 1211 | if ($PSEdition -eq 'Desktop') { 1212 | $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) 1213 | $Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) 1214 | } else { 1215 | $Password = ConvertFrom-SecureString -SecureString $SecureString -AsPlainText 1216 | } 1217 | } else { 1218 | $SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force 1219 | $Credential = New-Object System.Management.Automation.PSCredential @($UserName, $SecurePassword) 1220 | } 1221 | 1222 | $VMSession = New-PSSession -VMName $VMName -Credential $Credential 1223 | 1224 | Invoke-Command -ScriptBlock { Param($UserName, $DomainName, $Password, [bool] $Restart) 1225 | $WinlogonRegPath = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" 1226 | New-ItemProperty -Path $WinlogonRegPath -Name AutoAdminLogon -Value 1 -PropertyType DWORD -Force | Out-Null 1227 | New-ItemProperty -Path $WinlogonRegPath -Name ForceAutoLogon -Value 0 -PropertyType DWORD -Force | Out-Null 1228 | New-ItemProperty -Path $WinlogonRegPath -Name DefaultUserName -Value $Username -PropertyType String -Force | Out-Null 1229 | New-ItemProperty -Path $WinlogonRegPath -Name DefaultPassword -Value $Password -PropertyType String -Force | Out-Null 1230 | if ($Restart) { 1231 | Restart-Computer -Force 1232 | } 1233 | } -Session $VMSession -ArgumentList @($UserName, $DomainName, $Password, $Restart) 1234 | } 1235 | -------------------------------------------------------------------------------- /powershell/ad_init.ps1: -------------------------------------------------------------------------------- 1 | . .\common.ps1 2 | 3 | $VMAlias = "DC" 4 | $VMName = $LabPrefix, $VMAlias -Join "-" 5 | 6 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 7 | 8 | $AdmfContextStoreName = "Lab" 9 | $AdmfContextStorePath = "~\Documents\ADMF" 10 | 11 | Invoke-Command -ScriptBlock { Param($AdmfContextStoreName, $AdmfContextStorePath) 12 | Install-Module -Name ADMF -Scope AllUsers -Force 13 | New-Item -Path $AdmfContextStorePath -ItemType 'Directory' -ErrorAction SilentlyContinue | Out-Null 14 | New-AdmfContextStore -Name $AdmfContextStoreName -Path $AdmfContextStorePath -Scope SystemDefault 15 | } -Session $VMSession -ArgumentList @($AdmfContextStoreName, $AdmfContextStorePath) 16 | 17 | $ContextPath = Join-Path $PSScriptRoot "admf" 18 | 19 | $Context = Get-Content "$ContextPath\context.json" | ConvertFrom-Json 20 | $Membership = Get-Content "$ContextPath\Domain\GroupMemberships\membership.json" | ConvertFrom-Json 21 | $Groups = Get-Content "$ContextPath\Domain\Groups\groups.json" | ConvertFrom-Json 22 | $Variables = Get-Content "$ContextPath\Domain\Names\variables.json" | ConvertFrom-Json 23 | $OUs = Get-Content "$ContextPath\Domain\OrganizationalUnits\ous.json" | ConvertFrom-Json 24 | $Users = Get-Content "$ContextPath\Domain\Users\users.json" | ConvertFrom-Json 25 | 26 | Invoke-Command -ScriptBlock { Param($AdmfContextStorePath, $Context, 27 | $Membership, $Groups, $Variables, $OUs, $Users) 28 | $ContextVersion = "1.0.0" 29 | $AdmfContextStorePath = Resolve-Path $AdmfContextStorePath 30 | $ContextPath = Join-Path $AdmfContextStorePath "Basic\$ContextVersion" 31 | $ContextDomainPath = Join-Path $ContextPath "Domain" 32 | New-Item -Path $ContextPath -ItemType 'Directory' -ErrorAction SilentlyContinue | Out-Null 33 | New-Item -Path $ContextDomainPath -ItemType 'Directory' -ErrorAction SilentlyContinue | Out-Null 34 | @('GroupMemberships', 'Groups', 'Names', 'OrganizationalUnits', 'Users') | ForEach-Object { 35 | New-Item -Path $(Join-Path $ContextDomainPath $_) -ItemType 'Directory' -ErrorAction SilentlyContinue | Out-Null 36 | } 37 | $Context | ConvertTo-Json | Set-Content "$ContextPath\context.json" 38 | $Membership | ConvertTo-Json | Set-Content "$ContextPath\Domain\GroupMemberships\membership.json" 39 | $Groups | ConvertTo-Json | Set-Content "$ContextPath\Domain\Groups\groups.json" 40 | $Variables | ConvertTo-Json | Set-Content "$ContextPath\Domain\Names\variables.json" 41 | $OUs | ConvertTo-Json | Set-Content "$ContextPath\Domain\OrganizationalUnits\ous.json" 42 | $Users | ConvertTo-Json | Set-Content "$ContextPath\Domain\Users\users.json" 43 | } -Session $VMSession -ArgumentList @($AdmfContextStorePath, $Context, 44 | $Membership, $Groups, $Variables, $OUs, $Users) 45 | 46 | Invoke-Command -ScriptBlock { Param() 47 | $UserDnsDomain = $Env:UserDnsDomain.ToLower() 48 | Set-AdmfContext -Server $UserDnsDomain -Context "Basic" 49 | Test-AdmfDomain -Server $UserDnsDomain 50 | Invoke-AdmfDomain -Server $UserDnsDomain 51 | } -Session $VMSession -ArgumentList @() 52 | 53 | function New-RandomPassword { 54 | param( 55 | [Parameter(Position = 0)] 56 | [int] $Length = 16 57 | ) 58 | 59 | $charsets = @("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789") 60 | $sb = [System.Text.StringBuilder]::new() 61 | $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() 62 | 63 | $bytes = New-Object Byte[] 4 64 | 0 .. ($Length - 1) | ForEach-Object { 65 | $charset = $charsets[$_ % $charsets.Count] 66 | $rng.GetBytes($bytes) 67 | $num = [System.BitConverter]::ToUInt32($bytes, 0) 68 | $sb.Append($charset[$num % $charset.Length]) | Out-Null 69 | } 70 | 71 | return $sb.ToString() 72 | } 73 | 74 | $ADUsers = $Membership | Where-Object { $_.ItemType -eq 'User' } | Select-Object -ExpandProperty 'Name' 75 | 76 | $ADAccounts = @() 77 | foreach ($ADUser in $ADUsers) { 78 | $ADAccounts += [PSCustomObject]@{ 79 | Identity = $ADUser 80 | Password = $(New-RandomPassword 16) 81 | } 82 | } 83 | 84 | Set-Content -Path "ADAccounts.json" -Value $($ADAccounts | ConvertTo-Json) -Force 85 | 86 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 87 | 88 | $ADAccounts = $(Get-Content -Path "ADAccounts.json") | ConvertFrom-Json 89 | 90 | Invoke-Command -ScriptBlock { Param($ADAccounts) 91 | $ADAccounts | ForEach-Object { 92 | $Identity = $_.Identity 93 | $Password = ConvertTo-SecureString $_.Password -AsPlainText -Force 94 | Write-Host "Setting password for $Identity" 95 | try { 96 | Set-ADAccountPassword -Identity $Identity -NewPassword $Password -Reset 97 | } catch [Exception] { 98 | Write-Output $_.Exception.GetType().FullName, $_.Exception.Message 99 | } 100 | } 101 | } -Session $VMSession -ArgumentList @(,$ADAccounts) 102 | -------------------------------------------------------------------------------- /powershell/admf/Domain/GroupMemberships/membership.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "dford", 4 | "Domain": "%DomainName%", 5 | "ItemType": "User", 6 | "Group": "Management" 7 | }, 8 | { 9 | "Name": "ryoung", 10 | "Domain": "%DomainName%", 11 | "ItemType": "User", 12 | "Group": "Management" 13 | }, 14 | { 15 | "Name": "bowens", 16 | "Domain": "%DomainName%", 17 | "ItemType": "User", 18 | "Group": "Management" 19 | }, 20 | { 21 | "Name": "Management", 22 | "Domain": "%DomainName%", 23 | "ItemType": "Group", 24 | "Group": "RDM Users" 25 | }, 26 | { 27 | "Name": "lgriffin", 28 | "Domain": "%DomainName%", 29 | "ItemType": "User", 30 | "Group": "Inside Sales" 31 | }, 32 | { 33 | "Name": "jevans", 34 | "Domain": "%DomainName%", 35 | "ItemType": "User", 36 | "Group": "Inside Sales" 37 | }, 38 | { 39 | "Name": "kclark", 40 | "Domain": "%DomainName%", 41 | "ItemType": "User", 42 | "Group": "Inside Sales" 43 | }, 44 | { 45 | "Name": "rwallace", 46 | "Domain": "%DomainName%", 47 | "ItemType": "User", 48 | "Group": "Inside Sales" 49 | }, 50 | { 51 | "Name": "jjohnson", 52 | "Domain": "%DomainName%", 53 | "ItemType": "User", 54 | "Group": "Inside Sales" 55 | }, 56 | { 57 | "Name": "Inside Sales", 58 | "Domain": "%DomainName%", 59 | "ItemType": "Group", 60 | "Group": "RDM Users" 61 | }, 62 | { 63 | "Name": "kfoster", 64 | "Domain": "%DomainName%", 65 | "ItemType": "User", 66 | "Group": "Customer Support" 67 | }, 68 | { 69 | "Name": "dcollins", 70 | "Domain": "%DomainName%", 71 | "ItemType": "User", 72 | "Group": "Customer Support" 73 | }, 74 | { 75 | "Name": "smiller", 76 | "Domain": "%DomainName%", 77 | "ItemType": "User", 78 | "Group": "Customer Support" 79 | }, 80 | { 81 | "Name": "mmorgan", 82 | "Domain": "%DomainName%", 83 | "ItemType": "User", 84 | "Group": "Customer Support" 85 | }, 86 | { 87 | "Name": "Customer Support", 88 | "Domain": "%DomainName%", 89 | "ItemType": "Group", 90 | "Group": "RDM Users" 91 | }, 92 | { 93 | "Name": "ldap-reader", 94 | "Domain": "%DomainName%", 95 | "ItemType": "User", 96 | "Group": "ldap-readers" 97 | } 98 | ] -------------------------------------------------------------------------------- /powershell/admf/Domain/Groups/groups.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "Management", 4 | "Path": "OU=Groups,OU=%RootOU%,%DomainDN%", 5 | "Description": "Management", 6 | "Scope": "DomainLocal" 7 | }, 8 | { 9 | "Name": "Inside Sales", 10 | "Path": "OU=Groups,OU=%RootOU%,%DomainDN%", 11 | "Description": "Inside Sales", 12 | "Scope": "DomainLocal" 13 | }, 14 | { 15 | "Name": "Customer Support", 16 | "Path": "OU=Groups,OU=%RootOU%,%DomainDN%", 17 | "Description": "Customer Support", 18 | "Scope": "DomainLocal" 19 | }, 20 | { 21 | "Name": "RDM Users", 22 | "Path": "OU=Groups,OU=%RootOU%,%DomainDN%", 23 | "Description": "RDM Users", 24 | "Scope": "DomainLocal" 25 | }, 26 | { 27 | "Name": "ldap-readers", 28 | "Path": "OU=Groups,OU=%RootOU%,%DomainDN%", 29 | "Description": "LDAP readers", 30 | "Scope": "DomainLocal" 31 | } 32 | ] -------------------------------------------------------------------------------- /powershell/admf/Domain/Names/variables.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "%RootOU%", 4 | "Value": "Organization" 5 | } 6 | ] -------------------------------------------------------------------------------- /powershell/admf/Domain/OrganizationalUnits/ous.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "%RootOU%", 4 | "Description": "Root container for company assets", 5 | "Path": "%DomainDN%" 6 | }, 7 | { 8 | "Name": "Users", 9 | "Description": "User Container", 10 | "Path": "OU=%RootOU%,%DomainDN%" 11 | }, 12 | { 13 | "Name": "Groups", 14 | "Description": "Group Container", 15 | "Path": "OU=%RootOU%,%DomainDN%" 16 | } 17 | ] -------------------------------------------------------------------------------- /powershell/admf/Domain/Users/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "SamAccountName": "dford", 4 | "GivenName": "David", 5 | "Surname": "Ford", 6 | "UserPrincipalName": "dford@%DomainFqdn%", 7 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 8 | "PasswordNeverExpires": true 9 | }, 10 | { 11 | "SamAccountName": "ryoung", 12 | "GivenName": "Robert", 13 | "Surname": "Young", 14 | "UserPrincipalName": "ryoung@%DomainFqdn%", 15 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 16 | "PasswordNeverExpires": true 17 | }, 18 | { 19 | "SamAccountName": "bowens", 20 | "GivenName": "Barbara", 21 | "Surname": "Owens", 22 | "UserPrincipalName": "bowens@%DomainFqdn%", 23 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 24 | "PasswordNeverExpires": true 25 | }, 26 | { 27 | "SamAccountName": "lgriffin", 28 | "GivenName": "Laura", 29 | "Surname": "Griffin", 30 | "UserPrincipalName": "lgriffin@%DomainFqdn%", 31 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 32 | "PasswordNeverExpires": true 33 | }, 34 | { 35 | "SamAccountName": "jevans", 36 | "GivenName": "John", 37 | "Surname": "Evans", 38 | "UserPrincipalName": "jevans@%DomainFqdn%", 39 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 40 | "PasswordNeverExpires": true 41 | }, 42 | { 43 | "SamAccountName": "kclark", 44 | "GivenName": "Kimberly", 45 | "Surname": "Clark", 46 | "UserPrincipalName": "kclark@%DomainFqdn%", 47 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 48 | "PasswordNeverExpires": true 49 | }, 50 | { 51 | "SamAccountName": "rwallace", 52 | "GivenName": "Richard", 53 | "Surname": "Wallace", 54 | "UserPrincipalName": "rwallace@%DomainFqdn%", 55 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 56 | "PasswordNeverExpires": true 57 | }, 58 | { 59 | "SamAccountName": "jjohnson", 60 | "GivenName": "Jessica", 61 | "Surname": "Johnson", 62 | "UserPrincipalName": "jjohnson@%DomainFqdn%", 63 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 64 | "PasswordNeverExpires": true 65 | }, 66 | { 67 | "SamAccountName": "kfoster", 68 | "GivenName": "Kimberly", 69 | "Surname": "Foster", 70 | "UserPrincipalName": "kfoster@%DomainFqdn%", 71 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 72 | "PasswordNeverExpires": true 73 | }, 74 | { 75 | "SamAccountName": "dcollins", 76 | "GivenName": "Donna", 77 | "Surname": "Collins", 78 | "UserPrincipalName": "dcollins@%DomainFqdn%", 79 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 80 | "PasswordNeverExpires": true 81 | }, 82 | { 83 | "SamAccountName": "smiller", 84 | "GivenName": "Sharon", 85 | "Surname": "Miller", 86 | "UserPrincipalName": "smiller@%DomainFqdn%", 87 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 88 | "PasswordNeverExpires": true 89 | }, 90 | { 91 | "SamAccountName": "mmorgan", 92 | "GivenName": "Michael", 93 | "Surname": "Morgan", 94 | "UserPrincipalName": "mmorgan@%DomainFqdn%", 95 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 96 | "PasswordNeverExpires": true 97 | }, 98 | { 99 | "SamAccountName": "ldap-reader", 100 | "GivenName": "LDAP", 101 | "Surname": "Reader", 102 | "UserPrincipalName": "ldap-reader@%DomainFqdn%", 103 | "Path": "OU=Users,OU=%RootOU%,%DomainDN%", 104 | "PasswordNeverExpires": true 105 | } 106 | ] -------------------------------------------------------------------------------- /powershell/admf/context.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0.0", 3 | "Weight": 50, 4 | "Description": "Devolutions Labs", 5 | "Author": "Devolutions", 6 | "Prerequisites": [], 7 | "MutuallyExclusive": [], 8 | "Group": "Default" 9 | } 10 | -------------------------------------------------------------------------------- /powershell/alpine.apkovl.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devolutions/devolutions-labs/4d844087aa6e2a91a94f33dd41e7964005d50c10/powershell/alpine.apkovl.tar.gz -------------------------------------------------------------------------------- /powershell/build.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | #Requires -PSEdition Core 3 | 4 | . .\common.ps1 5 | 6 | Write-Host "Creating $LabPrefix lab..." 7 | 8 | Write-Host "Creating RTR VM..." 9 | $TimeRTR = Measure-Command { .\rtr_vm.ps1 } 10 | Write-Host "RTR VM creation time: $TimeRTR" 11 | 12 | Write-Host "Creating DC VM..." 13 | $TimeDC = Measure-Command { .\dc_vm.ps1 } 14 | Write-Host "DC VM creation time: $TimeDC" 15 | 16 | Write-Host "Creating DVLS VM..." 17 | $TimeDVLS = Measure-Command { .\dvls_vm.ps1 } 18 | Write-Host "DVLS VM creation time: $TimeDVLS" 19 | 20 | Write-Host "Creating GW VM..." 21 | $TimeGW = Measure-Command { .\gw_vm.ps1 } 22 | Write-Host "GW VM creation time: $TimeGW" 23 | 24 | Write-Host "Creating RDM VM..." 25 | $TimeRDM = Measure-Command { .\rdm_vm.ps1 } 26 | Write-Host "RDM VM creation time: $TimeRDM" 27 | 28 | Write-Host "Initializing Active Directory..." 29 | .\ad_init.ps1 30 | 31 | $TimeLab = $TimeRTR + $TimeDC + $TimeDVLS + $TimeGW + $TimeRDM 32 | Write-Host "Total $LabPrefix lab creation time: $TimeLab" 33 | -------------------------------------------------------------------------------- /powershell/common.ps1: -------------------------------------------------------------------------------- 1 | # common variable definitions 2 | 3 | if (Test-Path .\DevolutionsLabs.psm1) { 4 | Import-Module .\DevolutionsLabs.psm1 -Force 5 | } 6 | 7 | $licensing = Get-Content -Path "$PSScriptRoot\licensing.json" -Raw | ConvertFrom-Json 8 | 9 | $OSVersion = "2025" 10 | $LabPrefix = "IT-HELP" 11 | $LabDnsTld = ".ninja" 12 | $LabCompanyName = "IT Help Ninja" 13 | $LabName = $LabPrefix.ToLower() + $LabDnsTld 14 | 15 | $LocalUserName = "Administrator" 16 | $LocalPassword = "Local123!" 17 | 18 | $ProtectedUserName = "ProtectedUser" 19 | $ProtectedUserPassword = "Protected123!" 20 | 21 | $WanSwitchName = "NAT Switch" 22 | $LanSwitchName = "LAN Switch" 23 | $SwitchName = $LanSwitchName 24 | $NetAdapterName = "vEthernet (LAN)" 25 | $LabNetworkBase = "10.10.0.0" 26 | 27 | $RTRVMNumber = 2 28 | $RTRIpAddress = Get-DLabIpAddress $LabNetworkBase $RTRVMNumber 29 | $DefaultGateway = $RTRIpAddress 30 | $DhcpRangeStart = Get-DLabIpAddress $LabNetworkBase 100 31 | $DhcpRangeEnd = Get-DLabIpAddress $LabNetworkBase 255 32 | 33 | $DomainName = "ad.$LabName" 34 | $DnsZoneName = $DomainName 35 | $DomainDnsName = $DomainName 36 | $DomainNetbiosName = $LabPrefix 37 | $SafeModeAdministratorPassword = "SafeMode123!" 38 | 39 | $DCVMNumber = 3 40 | $DCMachineName = $LabPrefix, "DC" -Join "-" 41 | $DomainController = $DCMachineName 42 | $DCHostName = "$DCMachineName.$DomainName" 43 | $DCIpAddress = Get-DLabIpAddress $LabNetworkBase $DCVMNumber 44 | 45 | $CAMachineName = $LabPrefix, "DC" -Join "-" 46 | $CACommonName = $CAMachineName 47 | $CAHostName = "$CAMachineName.$DomainName" 48 | 49 | $DomainUserName = "$DomainNetbiosName\Administrator" 50 | $DomainPassword = "DevoLabs123!" 51 | 52 | $DnsServerForwarder = "1.1.1.1" 53 | $DnsServerAddress = $DCIpAddress 54 | -------------------------------------------------------------------------------- /powershell/dc_vm.ps1: -------------------------------------------------------------------------------- 1 | . .\common.ps1 2 | 3 | $VMAlias = "DC" 4 | $VMNumber = $DCVMNumber 5 | $VMName = $LabPrefix, $VMAlias -Join "-" 6 | $IpAddress = Get-DLabIpAddress $LabNetworkBase $VMNumber 7 | 8 | New-DLabVM $VMName -Password $DomainPassword -MemoryBytes 2GB -ProcessorCount 2 -OSVersion $OSVersion -Force 9 | Start-DLabVM $VMName 10 | 11 | Wait-DLabVM $VMName 'PSDirect' -Timeout 600 -UserName $LocalUserName -Password $DomainPassword 12 | $VMSession = New-DLabVMSession $VMName -UserName $LocalUserName -Password $DomainPassword 13 | 14 | Set-DLabVMNetAdapter $VMName -VMSession $VMSession ` 15 | -SwitchName $SwitchName -NetAdapterName $NetAdapterName ` 16 | -IPAddress $IPAddress -DefaultGateway $DefaultGateway ` 17 | -DnsServerAddress $DnsServerForwarder 18 | 19 | Write-Host "Test Internet connectivity" 20 | 21 | $ConnectionTest = Invoke-Command -ScriptBlock { 22 | 1..10 | ForEach-Object { 23 | Start-Sleep -Seconds 1; 24 | Resolve-DnsName -Name "www.google.com" -Type 'A' -DnsOnly -QuickTimeout -ErrorAction SilentlyContinue 25 | } | Where-Object { $_ -ne $null } | Select-Object -First 1 26 | } -Session $VMSession 27 | 28 | if (-Not $ConnectionTest) { 29 | throw "virtual machine doesn't have Internet access - fail early" 30 | } 31 | 32 | Write-Host "Promote Windows Server to domain controller" 33 | 34 | Invoke-Command -ScriptBlock { Param($DomainName, $DomainNetbiosName, $SafeModeAdministratorPassword) 35 | Install-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools 36 | $SafeModeAdministratorPassword = ConvertTo-SecureString $SafeModeAdministratorPassword -AsPlainText -Force 37 | $Params = @{ 38 | DomainName = $DomainName; 39 | DomainNetbiosName = $DomainNetbiosName; 40 | SafeModeAdministratorPassword = $SafeModeAdministratorPassword; 41 | InstallDNS = $true; 42 | SkipPreChecks = $true; 43 | } 44 | Install-ADDSForest @Params -Force 45 | } -Session $VMSession -ArgumentList @($DomainName, $DomainNetbiosName, $SafeModeAdministratorPassword) 46 | 47 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 48 | $BootTime = Get-Date 49 | 50 | Wait-DLabVM $VMName 'PSDirect' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 51 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 52 | 53 | Write-Host "Wait for domain controller promotion to complete (about 10 minutes)" 54 | 55 | # wait a good 5-10 minutes for the domain controller promotion to complete after reboot 56 | 57 | Invoke-Command -ScriptBlock { Param($BootTime) 58 | while (-Not [bool]$(Get-EventLog -LogName "System" ` 59 | -Source "Microsoft-Windows-GroupPolicy" -InstanceId 1502 ` 60 | -After $BootTime -ErrorAction SilentlyContinue)) { 61 | Start-Sleep 10 62 | } 63 | } -Session $VMSession -ArgumentList @($BootTime) 64 | 65 | Write-Host "Create read-only network share" 66 | 67 | Invoke-Command -ScriptBlock { 68 | New-Item "C:\Shared" -ItemType "Directory" | Out-Null 69 | New-SmbShare -Name "Shared" -Path "C:\Shared" -FullAccess 'ANONYMOUS LOGON','Everyone' 70 | } -Session $VMSession 71 | 72 | Write-Host "Disable Active Directory default password expiration policy" 73 | 74 | Invoke-Command -ScriptBlock { 75 | Get-ADDefaultDomainPasswordPolicy -Current LoggedOnUser | Set-ADDefaultDomainPasswordPolicy -MaxPasswordAge 00.00:00:00 76 | } -Session $VMSession 77 | 78 | Write-Host "Create ProtectedUser test account in Protected Users group" 79 | 80 | Invoke-Command -ScriptBlock { Param($ProtectedUserName, $ProtectedUserPassword) 81 | $SafeProtectedUserPassword = ConvertTo-SecureString $ProtectedUserPassword -AsPlainText -Force 82 | $DomainDnsName = $Env:UserDnsDomain.ToLower() 83 | 84 | $Params = @{ 85 | Name = $ProtectedUserName; 86 | GivenName = "Protected"; 87 | Surname = "User"; 88 | SamAccountName = "ProtectedUser"; 89 | UserPrincipalName = "ProtectedUser@$DomainDnsName"; 90 | AccountPassword = $SafeProtectedUserPassword; 91 | PasswordNeverExpires = $true; 92 | Description = "User member of the Protected Users group"; 93 | Enabled = $true; 94 | } 95 | $ProtectedUser = New-ADUser @Params -PassThru 96 | 97 | Add-ADGroupMember -Identity "Protected Users" -Members @($ProtectedUser) 98 | Add-ADGroupMember -Identity "Domain Admins" -Members @($ProtectedUser) 99 | } -Session $VMSession -ArgumentList @($ProtectedUserName, $ProtectedUserPassword) 100 | 101 | Write-Host "Install Active Directory Certificate Services" 102 | 103 | Invoke-Command -ScriptBlock { Param($DomainName, $UserName, $Password, $CACommonName) 104 | $ConfirmPreference = "High" 105 | Install-WindowsFeature -Name AD-Certificate -IncludeManagementTools 106 | Install-WindowsFeature -Name ADCS-Online-Cert 107 | $Params = @{ 108 | CAType = "EnterpriseRootCa"; 109 | CryptoProviderName = "RSA#Microsoft Software Key Storage Provider"; 110 | HashAlgorithmName = "SHA256"; 111 | KeyLength = 2048; 112 | CACommonName = $CACommonName; 113 | } 114 | Install-AdcsCertificationAuthority @Params -Force 115 | } -Session $VMSession -ArgumentList @($DomainName, $DomainUserName, $DomainPassword, $CACommonName) 116 | 117 | # Install IIS + Publish CRL over HTTP 118 | 119 | Invoke-Command -ScriptBlock { Param($CAHostName, $CACommonName) 120 | Install-WindowsFeature -Name 'Web-Server' | Out-Null 121 | Remove-IISSite -Name "Default Web Site" -Confirm:$false 122 | $CertSrvPath = "${Env:WinDir}\System32\CertSrv" 123 | New-IISSite -Name 'CertSrv' -PhysicalPath $CertSrvPath -BindingInformation "*:80:" 124 | & "$Env:WinDir\system32\inetsrv\appcmd.exe" set config ` 125 | -section:system.webServer/security/requestFiltering -allowDoubleEscaping:True /commit:apphost 126 | Start-IISSite -Name 'CertSrv' 127 | $LdapCrlDP = Get-CACrlDistributionPoint | Where-Object { $_.Uri -Like "ldap://*" } 128 | Remove-CACrlDistributionPoint -Uri $LdapCrlDP.Uri -Force 129 | $HttpCrlDP = Get-CACrlDistributionPoint | Where-Object { $_.Uri -Like "http://*/CertEnroll/*" } 130 | Remove-CACrlDistributionPoint -Uri $HttpCrlDP.Uri -Force 131 | Add-CACrlDistributionPoint -Uri $HttpCrlDP.URI -AddToCertificateCdp -AddToFreshestCrl -Force 132 | $LdapAIA = Get-CAAuthorityInformationAccess | Where-Object { $_.Uri -Like "ldap://*" } 133 | Remove-CAAuthorityInformationAccess -Uri $LdapAIA.Uri -Force 134 | Restart-Service CertSvc 135 | Start-Sleep -Seconds 10 # Wait for CertSvc 136 | $CAConfigName = "$CAHostName\$CACommonName" 137 | $CertAdmin = New-Object -COM "CertificateAuthority.Admin" 138 | # PublishCRLs flags: RePublish = 0x10 (16), BaseCRL = 1, DeltaCRL = 2 139 | $CertAdmin.PublishCRLs($CAConfigName, $([DateTime]::UtcNow), 17) 140 | Get-ChildItem "$CertSrvPath\CertEnroll\*.crl" | ForEach-Object { certutil.exe -f -dspublish $_.FullName } 141 | } -Session $VMSession -ArgumentList @($CAHostName, $CACommonName) 142 | 143 | Write-Host "Force AD CS CRL renewal on system startup" 144 | 145 | Invoke-Command -ScriptBlock { 146 | $TaskAction = New-ScheduledTaskAction -Execute "certutil.exe" -Argument "-crl" 147 | $TaskTrigger = New-ScheduledTaskTrigger -AtStartup 148 | 149 | $Params = @{ 150 | Action = $TaskAction; 151 | Trigger = $TaskTrigger; 152 | User = "NT AUTHORITY\SYSTEM"; 153 | TaskName = "Force AD CS CRL renewal"; 154 | Description = "Force AD CS CRL renewal"; 155 | } 156 | Register-ScheduledTask @Params 157 | } -Session $VMSession 158 | 159 | Write-Host "Requesting RDP server certificate" 160 | 161 | Request-DLabRdpCertificate $VMName -VMSession $VMSession ` 162 | -CAHostName $CAHostName -CACommonName $CACommonName 163 | 164 | Write-Host "Initializing PSRemoting" 165 | 166 | Initialize-DLabPSRemoting $VMName -VMSession $VMSession 167 | 168 | Write-Host "Initializing VNC server" 169 | 170 | Initialize-DLabVncServer $VMName -VMSession $VMSession 171 | 172 | Write-Host "Create Smartcard Logon certificate template" 173 | 174 | Invoke-Command -ScriptBlock { 175 | $Params = @{ 176 | BaseCertTypeName = "SmartcardLogon" 177 | NewCertTypeName = "MySmartcardLogon" 178 | NewCertTypeFriendlyName = "My Smartcard Logon" 179 | EnrolleeSuppliesSubject = $true 180 | AllowExportableKey = $true 181 | EnableTemplate = $true 182 | } 183 | & "C:\tools\scripts\New-CertificateTemplate.ps1" @Params 184 | } -Session $VMSession 185 | 186 | Write-Host "Configuring LDAPS certificate" 187 | 188 | Invoke-Command -ScriptBlock { 189 | $FullComputerName = [System.Net.DNS]::GetHostByName($Env:ComputerName).HostName 190 | $Certificate = Get-ChildItem "cert:\LocalMachine\My" | Where-Object { 191 | ($_.Subject -eq "CN=$FullComputerName") -and ($_.Issuer -ne $_.Subject) 192 | } | Select-Object -First 1 193 | $CertificateThumbprint = $Certificate.Thumbprint 194 | 195 | $LocalCertStore = 'HKLM:/Software/Microsoft/SystemCertificates/My/Certificates' 196 | $NtdsCertStore = 'HKLM:/Software/Microsoft/Cryptography/Services/NTDS/SystemCertificates/My/Certificates' 197 | if (-Not (Test-Path $NtdsCertStore)) { 198 | New-Item $NtdsCertStore -Force 199 | } 200 | Copy-Item -Path "$LocalCertStore/$CertificateThumbprint" -Destination $NtdsCertStore 201 | 202 | $dse = [adsi]'LDAP://localhost/rootDSE' 203 | [void]$dse.Properties['renewServerCertificate'].Add(1) 204 | $dse.CommitChanges() 205 | } -Session $VMSession 206 | -------------------------------------------------------------------------------- /powershell/dvls_vm.ps1: -------------------------------------------------------------------------------- 1 | . .\common.ps1 2 | 3 | $VMAlias = "DVLS" 4 | $VMNumber = 6 5 | $VMName = $LabPrefix, $VMAlias -Join "-" 6 | $IpAddress = Get-DLabIpAddress $LabNetworkBase $VMNumber 7 | 8 | New-DLabVM $VMName -Password $LocalPassword -OSVersion $OSVersion -Force 9 | Start-DLabVM $VMName 10 | 11 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 120 -UserName $LocalUserName -Password $LocalPassword 12 | $VMSession = New-DLabVMSession $VMName -UserName $LocalUserName -Password $LocalPassword 13 | 14 | Set-DLabVMNetAdapter $VMName -VMSession $VMSession ` 15 | -SwitchName $SwitchName -NetAdapterName $NetAdapterName ` 16 | -IPAddress $IPAddress -DefaultGateway $DefaultGateway ` 17 | -DnsServerAddress $DnsServerAddress 18 | 19 | Write-Host "Joining domain" 20 | 21 | Add-DLabVMToDomain $VMName -VMSession $VMSession ` 22 | -DomainName $DomainName -DomainController $DomainController ` 23 | -UserName $DomainUserName -Password $DomainPassword 24 | 25 | # Wait for virtual machine to reboot after domain join operation 26 | 27 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 28 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 29 | 30 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 31 | 32 | Write-Host "Requesting RDP server certificate" 33 | 34 | Request-DLabRdpCertificate $VMName -VMSession $VMSession ` 35 | -CAHostName $CAHostName -CACommonName $CACommonName 36 | 37 | Write-Host "Initializing PSRemoting" 38 | 39 | Initialize-DLabPSRemoting $VMName -VMSession $VMSession 40 | 41 | Write-Host "Initializing VNC server" 42 | 43 | Initialize-DLabVncServer $VMName -VMSession $VMSession 44 | 45 | Write-Host "Installing IIS features" 46 | 47 | Invoke-Command -ScriptBlock { 48 | @('Web-Server', 49 | 'Web-Http-Errors', 50 | 'Web-Http-Logging', 51 | 'Web-Http-Tracing', 52 | 'Web-Static-Content', 53 | 'Web-Default-Doc', 54 | 'Web-Dir-Browsing', 55 | 'Web-AppInit', 56 | 'Web-Net-Ext45', 57 | 'Web-Asp-Net45', 58 | 'Web-ISAPI-Ext', 59 | 'Web-ISAPI-Filter', 60 | 'Web-Basic-Auth', 61 | 'Web-Digest-Auth', 62 | 'Web-Stat-Compression', 63 | 'Web-Windows-Auth', 64 | 'Web-Mgmt-Tools', 65 | 'Web-WebSockets' 66 | ) | Foreach-Object { Install-WindowsFeature -Name $_ | Out-Null } 67 | } -Session $VMSession 68 | 69 | Write-Host "Installing IIS ASP.NET Core Module (ANCM)" 70 | 71 | Invoke-Command -ScriptBlock { 72 | # https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer 73 | $DotNetHostingFileName = "dotnet-hosting-9.0.4-win.exe" 74 | $DotNetHostingFileUrl = "https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.4/$DotNetHostingFileName" 75 | $DotNetHostingFileSHA512 = 'e02d6e48361bc09f84aefef0653bd1eaa1324795d120758115818d77f1ba0bca751dcc7e7c143293c7831fd72ff566d7c2248d1cb795f8d251c04631bc4459ea' 76 | $ProgressPreference = 'SilentlyContinue' 77 | Invoke-WebRequest $DotNetHostingFileUrl -OutFile "${Env:TEMP}\$DotNetHostingFileName" 78 | $FileHash = (Get-FileHash -Algorithm SHA512 "${Env:TEMP}\$DotNetHostingFileName").Hash 79 | if ($DotNetHostingFileSHA512 -ine $FileHash) { throw "unexpected SHA512 file hash for $DotNetHostingFileName`: $DotNetHostingFileSHA512" } 80 | Start-Process -FilePath "${Env:TEMP}\$DotNetHostingFileName" -ArgumentList @('/install', '/quiet', '/norestart', 'OPT_NO_X86=1') -Wait -NoNewWindow 81 | Remove-Item "${Env:TEMP}\$DotNetHostingFileName" -Force | Out-Null 82 | } -Session $VMSession 83 | 84 | Write-Host "Installing IIS extensions" 85 | 86 | Invoke-Command -ScriptBlock { 87 | choco install -y --no-progress urlrewrite 88 | choco install -y --no-progress --ignore-checksums iis-arr 89 | } -Session $VMSession 90 | 91 | Write-Host "Increase http.sys UrlSegmentMaxLength" 92 | 93 | Invoke-Command -ScriptBlock { 94 | # https://learn.microsoft.com/en-US/troubleshoot/developer/webapps/iis/iisadmin-service-inetinfo/httpsys-registry-windows 95 | Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\HTTP\Parameters' -Name 'UrlSegmentMaxLength' -Value 8192 -Type DWORD 96 | } -Session $VMSession 97 | 98 | Write-Host "Changing IIS default rules" 99 | 100 | Invoke-Command -ScriptBlock { 101 | & "$Env:WinDir\system32\inetsrv\appcmd.exe" set config ` 102 | -section:system.webServer/proxy -enabled:true /commit:apphost 103 | 104 | & "$Env:WinDir\system32\inetsrv\appcmd.exe" set config ` 105 | -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost 106 | 107 | & "$Env:WinDir\system32\inetsrv\appcmd.exe" set config ` 108 | -section:system.webServer/rewrite/globalRules -useOriginalURLEncoding:false /commit:apphost 109 | } -Session $VMSession 110 | 111 | Write-Host "Installing WebView2 Runtime" 112 | 113 | Invoke-Command -ScriptBlock { 114 | $ProgressPreference = 'SilentlyContinue' 115 | Invoke-WebRequest "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile 'MicrosoftEdgeWebview2Setup.exe' 116 | Start-Process -FilePath '.\MicrosoftEdgeWebview2Setup.exe' -ArgumentList @('/silent', '/install') -Wait 117 | Remove-Item 'MicrosoftEdgeWebview2Setup.exe' -Force | Out-Null 118 | } -Session $VMSession 119 | 120 | Write-Host "Installing SQL Server Express" 121 | 122 | Invoke-Command -ScriptBlock { 123 | choco install -y --no-progress sql-server-express 124 | choco install -y --no-progress sql-server-management-studio 125 | Install-Module -Name SqlServer -Scope AllUsers -AllowClobber -Force 126 | } -Session $VMSession 127 | 128 | Write-Host "Creating SQL database for RDM" 129 | 130 | $SqlInstance = "localhost\SQLEXPRESS" 131 | $RdmDatabaseName = "rdm" 132 | $RdmSqlUsername = "rdm" 133 | $RdmSqlPassword = "sql123!" 134 | 135 | Invoke-Command -ScriptBlock { Param($DatabaseName, $SqlInstance) 136 | Import-Module SqlServer -Force 137 | $SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlInstance) 138 | $SqlServer.Settings.LoginMode = [Microsoft.SqlServer.Management.SMO.ServerLoginMode]::Mixed 139 | $SqlServer.Alter() 140 | $Database = New-Object Microsoft.SqlServer.Management.Smo.Database($SqlServer, $DatabaseName) 141 | $Database.Create() 142 | $Database.RecoveryModel = "simple" 143 | $Database.Alter() 144 | Get-Service -Name 'MSSQL$SQLEXPRESS' | Restart-Service 145 | Start-Sleep -Seconds 2 146 | } -Session $VMSession -ArgumentList @($RdmDatabaseName, $SqlInstance) 147 | 148 | Write-Host "Creating SQL user for RDM" 149 | 150 | Invoke-Command -ScriptBlock { Param($DatabaseName, $SqlInstance, $SqlUsername, $SqlPassword) 151 | Import-Module SqlServer -Force 152 | $SecurePassword = ConvertTo-SecureString $SqlPassword -AsPlainText -Force 153 | $SqlCredential = New-Object System.Management.Automation.PSCredential @($SqlUsername, $SecurePassword) 154 | $Params = @{ 155 | ServerInstance = $SqlInstance; 156 | LoginPSCredential = $SqlCredential; 157 | LoginType = "SqlLogin"; 158 | GrantConnectSql = $true; 159 | Enable = $true; 160 | } 161 | Add-SqlLogin @Params 162 | $SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlInstance) 163 | $Database = $SqlServer.Databases[$DatabaseName] 164 | $Database.SetOwner('sa') 165 | $Database.Alter() 166 | $User = New-Object Microsoft.SqlServer.Management.Smo.User($Database, $SqlUsername) 167 | $User.Login = $SqlUsername 168 | $User.Create() 169 | $Role = $Database.Roles['db_owner'] 170 | $Role.AddMember($SqlUsername) 171 | } -Session $VMSession -ArgumentList @($RdmDatabaseName, $SqlInstance, $RdmSqlUsername, $RdmSqlPassword) 172 | 173 | Write-Host "Creating SQL database for DVLS" 174 | 175 | $SqlInstance = "localhost\SQLEXPRESS" 176 | $DatabaseName = "dvls" 177 | $SqlUsername = "dvls" 178 | $SqlPassword = "sql123!" 179 | 180 | Invoke-Command -ScriptBlock { Param($DatabaseName, $SqlInstance) 181 | Import-Module SqlServer -Force 182 | $SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlInstance) 183 | $SqlServer.Settings.LoginMode = [Microsoft.SqlServer.Management.SMO.ServerLoginMode]::Mixed 184 | $SqlServer.Alter() 185 | $Database = New-Object Microsoft.SqlServer.Management.Smo.Database($SqlServer, $DatabaseName) 186 | $Database.Create() 187 | $Database.RecoveryModel = "simple" 188 | $Database.Alter() 189 | Get-Service -Name 'MSSQL$SQLEXPRESS' | Restart-Service 190 | Start-Sleep -Seconds 2 191 | } -Session $VMSession -ArgumentList @($DatabaseName, $SqlInstance) 192 | 193 | Write-Host "Creating SQL user for DVLS" 194 | 195 | Invoke-Command -ScriptBlock { Param($DatabaseName, $SqlInstance, $SqlUsername, $SqlPassword) 196 | Import-Module SqlServer -Force 197 | $SecurePassword = ConvertTo-SecureString $SqlPassword -AsPlainText -Force 198 | $SqlCredential = New-Object System.Management.Automation.PSCredential @($SqlUsername, $SecurePassword) 199 | $Params = @{ 200 | ServerInstance = $SqlInstance; 201 | LoginPSCredential = $SqlCredential; 202 | LoginType = "SqlLogin"; 203 | GrantConnectSql = $true; 204 | Enable = $true; 205 | } 206 | Add-SqlLogin @Params 207 | $SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlInstance) 208 | $Database = $SqlServer.Databases[$DatabaseName] 209 | $Database.SetOwner('sa') 210 | $Database.Alter() 211 | $User = New-Object Microsoft.SqlServer.Management.Smo.User($Database, $SqlUsername) 212 | $User.Login = $SqlUsername 213 | $User.Create() 214 | $Role = $Database.Roles['db_owner'] 215 | $Role.AddMember($SqlUsername) 216 | } -Session $VMSession -ArgumentList @($DatabaseName, $SqlInstance, $SqlUsername, $SqlPassword) 217 | 218 | $DvlsVersion = "" 219 | $GatewayVersion = "" 220 | 221 | $ProductsHtm = Invoke-RestMethod -Uri "https://devolutions.net/productinfo.htm" -Method 'GET' -ContentType 'text/plain' 222 | foreach ($line in $($ProductsHtm -split "`n")) { 223 | if ($line -match '^DPSbin\.Version=(.+)$') { 224 | $DvlsVersion = $matches[1].Trim() 225 | } elseif ($line -match '^Gatewaybin\.Version=(.+)$') { 226 | $GatewayVersion = $matches[1].Trim() 227 | } 228 | } 229 | 230 | if ([string]::IsNullOrEmpty($DvlsVersion)) { 231 | throw "failed to detect DVLS version" 232 | } 233 | Write-Host "DVLS Version: $DVLSVersion" 234 | 235 | if ([string]::IsNullOrEmpty($GatewayVersion)) { 236 | throw "failed to detect DVLS version" 237 | } 238 | Write-Host "Gateway Version: $GatewayVersion" 239 | 240 | $DvlsSiteName = "DVLS" 241 | $DvlsPath = "C:\inetpub\dvlsroot" 242 | $DvlsAdminUsername = "dvls-admin" 243 | $DvlsAdminPassword = "dvls-admin123!" 244 | $DvlsAdminEmail = "admin@ad.it-help.ninja" 245 | 246 | $DvlsHostName = "dvls.$DomainName" 247 | $DvlsAccessUri = "https://$DvlsHostName" 248 | $CertificateFile = "~\Documents\cert.pfx" 249 | $CertificatePassword = "cert123!" 250 | 251 | Write-Host "Creating new DNS record for Devolutions Server" 252 | 253 | Invoke-Command -ScriptBlock { Param($DnsName, $DnsZoneName, $IPAddress, $DnsServer) 254 | Install-WindowsFeature RSAT-DNS-Server 255 | $Params = @{ 256 | Name = $DnsName; 257 | ZoneName = $DnsZoneName; 258 | IPv4Address = $IPAddress; 259 | ComputerName = $DnsServer; 260 | AllowUpdateAny = $true; 261 | } 262 | Add-DnsServerResourceRecordA @Params 263 | } -Session $VMSession -ArgumentList @("dvls", $DomainName, $IPAddress, $DCHostName) 264 | 265 | Write-Host "Requesting new certificate for Devolutions Server" 266 | 267 | Request-DLabCertificate $VMName -VMSession $VMSession ` 268 | -CommonName $DvlsHostName ` 269 | -CAHostName $CAHostName -CACommonName $CACommonName ` 270 | -CertificateFile $CertificateFile -Password $CertificatePassword 271 | 272 | Write-Host "Enabling and configuring TLS in SQL Server" 273 | 274 | Invoke-Command -ScriptBlock { 275 | $SqlServerBaseKey = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.SQLEXPRESS\MSSQLServer" 276 | $SuperSocketLibKey = Join-Path $SqlServerBaseKey "SuperSocketNetLib" 277 | $SuperSocketLibTcpKey = Join-Path $SuperSocketLibKey "Tcp" 278 | $SuperSocketLibTcpIpAllKey = Join-Path $SuperSocketLibTcpKey "IPAll" 279 | 280 | Set-ItemProperty -Path $SuperSocketLibTcpKey -Name "Enabled" -Value 1 281 | Set-ItemProperty -Path $SuperSocketLibTcpIpAllKey -Name "TcpPort" -Value "1433" 282 | Set-ItemProperty -Path $SuperSocketLibTcpIpAllKey -Name "TcpDynamicPorts" -Value "" 283 | 284 | $cert = Get-ChildItem "cert:\LocalMachine\My" | 285 | Where-Object { $_.Subject -like "CN=$Env:COMPUTERNAME.*" } | 286 | Sort-Object NotBefore -Descending | Select-Object -First 1 287 | 288 | if (-Not $cert) { 289 | throw "TLS certificate not found for CN=$Env:COMPUTERNAME" 290 | } 291 | 292 | $thumbprint = $cert.Thumbprint 293 | $keyId = if ($PSEdition -eq 'Core') { 294 | $cert.PrivateKey.Key.UniqueName 295 | } else { 296 | $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName 297 | } 298 | $keyFile = Join-Path "$Env:ProgramData\Microsoft\Crypto\RSA\MachineKeys" $keyId 299 | 300 | $scOut = sc.exe showsid 'MSSQL$SQLEXPRESS' 301 | $sid = ($scOut | Select-String -Pattern '^SERVICE SID:').ToString().Split(':')[1].Trim() 302 | 303 | $acl = Get-Acl -Path $keyFile 304 | $sidObj = New-Object System.Security.Principal.SecurityIdentifier($sid) 305 | $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($sidObj, "Read", "Allow") 306 | $acl.AddAccessRule($rule) 307 | Set-Acl -Path $keyFile -AclObject $acl 308 | 309 | Set-ItemProperty -Path $SuperSocketLibKey -Name "Certificate" -Value $thumbprint 310 | Set-ItemProperty -Path $SuperSocketLibKey -Name "ForceEncryption" -Value 0 -Type DWORD 311 | 312 | New-NetFirewallRule -DisplayName "SQL Server TCP 1433" -Direction Inbound -Protocol TCP -LocalPort 1433 -Action Allow 313 | 314 | Restart-Service -Name 'MSSQL$SQLEXPRESS' -Force 315 | Start-Sleep -Seconds 2 316 | } -Session $VMSession 317 | 318 | Write-Host "Creating Devolutions Server IIS site" 319 | 320 | Invoke-Command -ScriptBlock { Param($DvlsHostName, $DvlsSiteName, $DvlsPath, $CertificateFile, $CertificatePassword) 321 | $DvlsPort = 443; 322 | New-Item -Path $DvlsPath -ItemType 'Directory' -ErrorAction SilentlyContinue | Out-Null 323 | $CertificatePassword = ConvertTo-SecureString $CertificatePassword -AsPlainText -Force 324 | $Params = @{ 325 | FilePath = $CertificateFile; 326 | CertStoreLocation = "cert:\LocalMachine\My"; 327 | Password = $CertificatePassword; 328 | Exportable = $true; 329 | } 330 | $Certificate = Import-PfxCertificate @Params 331 | $CertificateThumbprint = $Certificate.Thumbprint 332 | $BindingInformation = '*:' + $DvlsPort + ':' + $DvlsHostName 333 | $Params = @{ 334 | Name = $DvlsSiteName; 335 | Protocol = "https"; 336 | SslFlag = "Sni"; 337 | PhysicalPath = $DvlsPath; 338 | BindingInformation = $BindingInformation; 339 | CertStoreLocation = "cert:\LocalMachine\My"; 340 | CertificateThumbprint = $CertificateThumbprint; 341 | } 342 | New-IISSite @Params 343 | } -Session $VMSession -ArgumentList @($DvlsHostName, $DvlsSiteName, $DvlsPath, $CertificateFile, $CertificatePassword) 344 | 345 | Write-Host "Installing .NET Desktop Runtime" 346 | 347 | Invoke-Command -ScriptBlock { 348 | $MajorVersion = "9.0" 349 | $RuntimeType = "windowsdesktop" 350 | $Architecture = if ($Env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { "win-arm64" } else { "win-x64" } 351 | $ReleasesJsonUrl = "https://builds.dotnet.microsoft.com/dotnet/release-metadata/$MajorVersion/releases.json" 352 | $ReleasesData = Invoke-RestMethod -Uri $ReleasesJsonUrl 353 | 354 | $LatestReleaseWithDesktop = $ReleasesData.releases | 355 | Where-Object { $_.windowsdesktop -and $_.windowsdesktop.files } | 356 | Sort-Object -Property 'release-date' -Descending | Select-Object -First 1 357 | 358 | if (-not $LatestReleaseWithDesktop) { 359 | throw "Could not find any releases with $RuntimeType runtime." 360 | } 361 | 362 | $DesktopRuntimeVersion = $LatestReleaseWithDesktop.windowsdesktop.version 363 | $DesktopRuntimeFiles = $LatestReleaseWithDesktop.windowsdesktop.files 364 | $Installer = $DesktopRuntimeFiles | Where-Object { 365 | $_.rid -eq $Architecture -and $_.name -like "$RuntimeType-runtime-*-*.exe" 366 | } | Select-Object -First 1 367 | 368 | if (-not $Installer) { 369 | throw "Could not find $RuntimeType runtime installer for $Architecture" 370 | } 371 | 372 | $DownloadUrl = $Installer.url 373 | $ExpectedFileHash = $Installer.hash 374 | $InstallerFileName = Split-Path -Leaf $DownloadUrl 375 | $InstallerLocalPath = Join-Path $Env:TEMP $InstallerFileName 376 | $ProgressPreference = 'SilentlyContinue' 377 | Invoke-WebRequest $DownloadUrl -OutFile $InstallerLocalPath 378 | $ActualFileHash = (Get-FileHash -Algorithm SHA512 $InstallerLocalPath).Hash 379 | if ($ExpectedFileHash -ine $ActualFileHash) { throw "Unexpected SHA512 file hash for $InstallerFileName`: $ActualFileHash" } 380 | Start-Process -FilePath $InstallerLocalPath -ArgumentList @('/install', '/quiet', '/norestart', 'OPT_NO_X86=1') -Wait -NoNewWindow 381 | Remove-Item $InstallerLocalPath -Force | Out-Null 382 | 383 | Write-Host ".NET $RuntimeType runtime $DesktopRuntimeVersion installed successfully." 384 | } -Session $VMSession 385 | 386 | Write-Host "Installing Devolutions Console" 387 | 388 | Invoke-Command -ScriptBlock { Param($DvlsVersion) 389 | $ProgressPreference = 'SilentlyContinue' 390 | $DownloadBaseUrl = "https://cdn.devolutions.net/download" 391 | $DvlsConsoleExe = "$(Resolve-Path ~)\Documents\Setup.DVLS.Console.exe" 392 | Invoke-WebRequest "$DownloadBaseUrl/Setup.DVLS.Console.${DvlsVersion}.exe" -OutFile $DvlsConsoleExe 393 | Start-Process -FilePath $DvlsConsoleExe -ArgumentList @('/exenoui', '/quiet', '/norestart') -Wait 394 | } -Session $VMSession -ArgumentList @($DvlsVersion) 395 | 396 | Write-Host "Installing Devolutions Server" 397 | 398 | Invoke-Command -ScriptBlock { Param($DvlsVersion, $GatewayVersion, 399 | $DvlsPath, $DvlsSiteName, $DvlsAccessUri, $DatabaseName, 400 | $SqlInstance, $SqlUsername, $SqlPassword, 401 | $DvlsAdminUsername, $DvlsAdminPassword, 402 | $DvlsAdminEmail) 403 | 404 | $ProgressPreference = 'SilentlyContinue' 405 | $DownloadBaseUrl = "https://cdn.devolutions.net/download" 406 | 407 | Write-Host "Downloading Devolutions Server version $DvlsVersion" 408 | $DvlsWebAppZip = "$(Resolve-Path ~)\Documents\DVLS.${DvlsVersion}.zip" 409 | if (-Not $(Test-Path -Path $DvlsWebAppZip -PathType 'Leaf')) { 410 | Invoke-WebRequest "$DownloadBaseUrl/RDMS/DVLS.${DvlsVersion}.zip" -OutFile $DvlsWebAppZip 411 | } 412 | 413 | Write-Host "Downloading Devolutions Gateway version $GatewayVersion" 414 | $GatewayMsi = "$(Resolve-Path ~)\Documents\DevolutionsGateway.msi" 415 | if (-Not $(Test-Path -Path $GatewayMsi -PathType 'Leaf')) { 416 | Invoke-WebRequest "$DownloadBaseUrl/DevolutionsGateway-x86_64-${GatewayVersion}.msi" -OutFile $GatewayMsi 417 | } 418 | 419 | $BackupKeysPassword = "DvlsBackupKeys123!" 420 | $BackupKeysPath = "$(Resolve-Path ~)\Documents\DvlsBackupKeys" 421 | New-Item -Path $BackupKeysPath -ItemType 'Directory' -ErrorAction SilentlyContinue | Out-Null 422 | 423 | $PasswordFilePath = [Environment]::GetFolderPath('Desktop') 424 | New-Item -Path $PasswordFilePath -ItemType 'Directory' -ErrorAction SilentlyContinue | Out-Null 425 | 426 | $DvlsConsoleArgs = @( 427 | "server", "install", 428 | "-v", "--acceptEula", "-q", 429 | "--adminUsername=$DvlsAdminUsername", 430 | "--adminPassword=$DvlsAdminPassword", 431 | "--adminEmail=$DvlsAdminEmail", 432 | "--installZip=$DvlsWebAppZip", 433 | "--dps-path=$DvlsPath", 434 | "--dps-website-name=$DvlsSiteName", 435 | "--web-application-name=/", 436 | "--access-uri=$DvlsAccessUri", 437 | "--backupKeysPath=$BackupKeysPath", 438 | "--backupKeysPassword=$BackupKeysPassword", 439 | "--databaseHost=$SqlInstance", 440 | "--databaseName=$DatabaseName", 441 | "--db-username=$SqlUsername", 442 | "--db-password=$SqlPassword", 443 | "--pwd-file-path=$PasswordFilePath", 444 | "--install-devolutions-gateway", 445 | "--gateway-msi=$GatewayMsi", 446 | "--disableEncryptConfig", 447 | "--disablePassword") 448 | 449 | if ($DvlsAccessUri.StartsWith("http://")) { 450 | $DvlsConsoleArgs += @("--disable-https") 451 | } 452 | 453 | $DvlsConsoleCli = "${Env:ProgramFiles}\Devolutions\Devolutions Server Console\DPS.Console.CLI.exe" 454 | 455 | Write-Host "& '$DvlsConsoleCli' $($DvlsConsoleArgs -Join ' ')" 456 | 457 | & $DvlsConsoleCli @DvlsConsoleArgs 458 | 459 | } -Session $VMSession -ArgumentList @($DvlsVersion, $GatewayVersion, 460 | $DvlsPath, $DvlsSiteName, $DvlsAccessUri, $DatabaseName, 461 | $SqlInstance, $SqlUsername, $SqlPassword, 462 | $DvlsAdminUsername, $DvlsAdminPassword, 463 | $DvlsAdminEmail) 464 | -------------------------------------------------------------------------------- /powershell/golden.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | #Requires -PSEdition Core 3 | 4 | param( 5 | [string] $VMName = "IT-TEMPLATE", 6 | [string] $UserName = "Administrator", 7 | [string] $Password = "lab123!", 8 | [string] $OSVersion = $null, 9 | [string] $SwitchName = "NAT Switch", 10 | [string] $InputIsoPath = $null, 11 | [string] $OutputVhdxPath = $null, 12 | [bool] $InstallWindowsUpdates = $false, 13 | [bool] $DisableWindowsUpdates = $true, 14 | [bool] $InstallChocolateyPackages = $true, 15 | [bool] $InstallSkillableServices = $true 16 | ) 17 | 18 | $ErrorActionPreference = "Stop" 19 | 20 | $OSVersionParam = $OSVersion 21 | $SwitchNameParam = $SwitchName 22 | 23 | # load common variables 24 | . .\common.ps1 25 | 26 | if (-Not [string]::IsNullOrEmpty($OSVersionParam)) { 27 | $OSVersion = $OSVersionParam 28 | } 29 | 30 | if (-Not [string]::IsNullOrEmpty($SwitchNameParam)) { 31 | $SwitchName = $SwitchNameParam 32 | } 33 | 34 | Write-Host "Options:" 35 | Write-Host "`tOSVersion: $OSVersion" 36 | Write-Host "`tSwitchName: $SwitchName" 37 | Write-Host "`tVMName: $VMName" 38 | Write-Host "`tUserName: $UserName" 39 | Write-Host "`tPassword: $Password" 40 | Write-Host "`tInstallWindowsUpdates: $InstallWindowsUpdates" 41 | Write-Host "`tDisableWindowsUpdates: $DisableWindowsUpdates" 42 | Write-Host "`tInstallChocolateyPackages: $InstallChocolateyPackages" 43 | Write-Host "`tInstallSkillableServices: $InstallSkillableServices" 44 | Write-Host "" 45 | 46 | Write-DLabLog "Creating golden image" 47 | 48 | if (-Not [string]::IsNullOrEmpty($InputIsoPath)) { 49 | $IsoFilePath = $InputIsoPath 50 | } else { 51 | $IsoFilePath = $(Get-DLabIsoFilePath "windows_server_${OSVersion}").FullName 52 | } 53 | 54 | if (-Not (Test-Path $IsoFilePath)) { 55 | throw "ISO file not found: '$IsoFilePath'" 56 | } else { 57 | Write-DLabLog "Using '$IsoFilePath' ISO file" 58 | } 59 | 60 | Write-DLabLog "Scanning image list from Windows ISO" 61 | 62 | $ImageList = Get-DLabWindowsImageListFromIso $IsoFilePath | Select-Object -Property Index, Name 63 | $ImageList | Format-List 64 | 65 | if ($OSVersion -eq '11') { 66 | $Image = $ImageList | Where-Object { $_.Name -Like 'Windows * Enterprise' } | Select-Object -First 1 67 | } else { 68 | $Image = $ImageList | Where-Object { $_.Name -Like 'Windows Server * Standard (Desktop Experience)' } | Select-Object -First 1 69 | } 70 | 71 | if (-Not $Image) { 72 | throw "Could not automatically select image to install from the list" 73 | } 74 | 75 | $ImageIndex = $Image.Index 76 | Write-DLabLog "Using Windows image [$ImageIndex]: $($Image.Name)" 77 | 78 | Write-DLabLog "Creating Windows answer file" 79 | 80 | $AnswerTempPath = Join-Path $([System.IO.Path]::GetTempPath()) "unattend-$VMName" 81 | Remove-Item $AnswerTempPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null 82 | New-Item -ItemType Directory -Path $AnswerTempPath -ErrorAction SilentlyContinue | Out-Null 83 | $AnswerFilePath = Join-Path $AnswerTempPath "autounattend.xml" 84 | 85 | $Params = @{ 86 | UserFullName = "devolutions"; 87 | UserOrganization = "IT-HELP"; 88 | ComputerName = $Name; 89 | AdministratorPassword = $Password; 90 | OSVersion = $OSVersion; 91 | ImageIndex = $ImageIndex; 92 | UILanguage = "en-US"; 93 | UserLocale = "en-CA"; 94 | } 95 | 96 | New-DLabAnswerFile $AnswerFilePath @Params 97 | 98 | $AnswerIsoPath = Join-Path $([System.IO.Path]::GetTempPath()) "unattend-$VMName.iso" 99 | New-DLabIsoFile -Path $AnswerTempPath -Destination $AnswerIsoPath -VolumeName "unattend" 100 | 101 | New-DLabParentVM $VMName -OSVersion $OSVersion -IsoFilePath $IsoFilePath -Force 102 | 103 | Add-VMDvdDrive -VMName $VMName -ControllerNumber 1 -Path $AnswerIsoPath 104 | 105 | Write-DLabLog "Starting golden VM for Windows installation" 106 | 107 | Start-DLabVM $VMName 108 | Start-Sleep 5 109 | 110 | Write-DLabLog "Waiting for VM to reboot a first time during installation" 111 | 112 | Wait-DLabVM $VMName 'Reboot' -Timeout 600 113 | 114 | Get-VMDvdDrive $VMName | Where-Object { $_.DvdMediaType -Like 'ISO' } | 115 | Remove-VMDvdDrive -ErrorAction SilentlyContinue 116 | 117 | Remove-Item -Path $AnswerIsoPath -Force -ErrorAction SilentlyContinue | Out-Null 118 | Remove-Item -Path $AnswerTempPath -Recurse -Force -ErrorAction SilentlyContinue | Out-Null 119 | 120 | Write-DLabLog "Waiting for VM to become ready after Windows installation" 121 | 122 | Wait-DLabVM $VMName 'PSDirect' -Timeout (45 * 60) -UserName $UserName -Password $Password 123 | $VMSession = New-DLabVMSession $VMName -UserName $UserName -Password $Password 124 | 125 | Write-DLabLog "Shutting down VM post-installation" 126 | Stop-VM $VMName 127 | Wait-DLabVM $VMName 'Shutdown' -Timeout 120 128 | 129 | Get-VMNetworkAdapter -VMName $VMName | Remove-VMNetworkAdapter 130 | Add-VMNetworkAdapter -VMName $VMName -SwitchName $SwitchName 131 | 132 | Write-DLabLog "Starting VM to begin customization" 133 | 134 | Start-DLabVM $VMName 135 | Start-Sleep 5 136 | 137 | Wait-DLabVM $VMName 'PSDirect' -Timeout 360 -UserName $UserName -Password $Password 138 | $VMSession = New-DLabVMSession $VMName -UserName $UserName -Password $Password 139 | 140 | Write-DLabLog "Configuring VM network adapter" 141 | 142 | if ($SwitchName -eq "NAT Switch") { 143 | Set-DLabVMNetAdapter $VMName -VMSession $VMSession ` 144 | -SwitchName $SwitchName -NetAdapterName "vEthernet (LAN)" ` 145 | -IPAddress "10.9.0.249" -DefaultGateway "10.9.0.1" ` 146 | -DnsServerAddress "1.1.1.1" ` 147 | -RegisterAutomaticFix $false 148 | } 149 | 150 | Write-DLabLog "Testing Internet connectivity" 151 | 152 | $ConnectionTest = Invoke-Command -ScriptBlock { 153 | 1..10 | ForEach-Object { 154 | Start-Sleep -Seconds 1; 155 | Resolve-DnsName -Name "www.google.com" -Type 'A' -DnsOnly -QuickTimeout -ErrorAction SilentlyContinue 156 | } | Where-Object { $_ -ne $null } | Select-Object -First 1 157 | } -Session $VMSession 158 | 159 | if (-Not $ConnectionTest) { 160 | throw "virtual machine doesn't have Internet access - fail early" 161 | } 162 | 163 | Write-DLabLog "Increase WinRM default configuration values" 164 | 165 | Invoke-Command -ScriptBlock { 166 | & 'winrm' 'set' 'winrm/config' '@{MaxTimeoutms=\"1800000\"}' 167 | & 'winrm' 'set' 'winrm/config/winrs' '@{MaxMemoryPerShellMB=\"800\"}' 168 | } -Session $VMSession 169 | 170 | Write-DLabLog "Enabling TLS 1.2 for .NET Framework applications" 171 | 172 | Invoke-Command -ScriptBlock { 173 | Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWORD 174 | Set-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWORD 175 | } -Session $VMSession 176 | 177 | Write-DLabLog "Disabling Server Manager automatic launch and Windows Admin Center pop-up" 178 | 179 | Invoke-Command -ScriptBlock { 180 | $ServerManagerReg = "HKLM:\SOFTWARE\Microsoft\ServerManager" 181 | Set-ItemProperty -Path $ServerManagerReg -Name 'DoNotPopWACConsoleAtSMLaunch' -Value '1' -Type DWORD 182 | Set-ItemProperty -Path $ServerManagerReg -Name 'DoNotOpenServerManagerAtLogon' -Value '1' -Type DWORD 183 | } -Session $VMSession 184 | 185 | Write-DLabLog "Disabling 'Activate Windows' watermark on desktop" 186 | 187 | Invoke-Command -ScriptBlock { 188 | Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform\Activation' -Name 'Manual' -Value '1' -Type DWORD 189 | 190 | $TaskAction = New-ScheduledTaskAction -Execute 'powershell.exe' ` 191 | -Argument "-Command { Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform\Activation' -Name 'Manual' -Value '1' -Type DWORD }" 192 | 193 | $TaskTrigger = New-ScheduledTaskTrigger -AtStartup 194 | 195 | $Params = @{ 196 | Action = $TaskAction; 197 | Trigger = $TaskTrigger; 198 | User = "NT AUTHORITY\SYSTEM"; 199 | TaskName = "Activation Watermark"; 200 | Description = "Remove Windows Activation Watermark"; 201 | } 202 | Register-ScheduledTask @Params 203 | } -Session $VMSession 204 | 205 | $VMSession = New-DLabVMSession $VMName -UserName $UserName -Password $Password 206 | 207 | Write-DLabLog "Fix default borderless windows style" 208 | 209 | # https://www.deploymentresearch.com/fixing-borderless-windows-in-windows-server-2019-and-windows-server-2022/ 210 | Invoke-Command -ScriptBlock { 211 | $DefaultUserReg = "HKLM\TempDefault" 212 | $NtuserDatPath = "C:\Users\Default\NTUSER.DAT" 213 | reg load $DefaultUserReg $NtuserDatPath 214 | $HKDU = "Registry::$DefaultUserReg" 215 | $RegPath = "$HKDU\Control Panel\Desktop" 216 | $RegValue = ([byte[]](0x90,0x32,0x07,0x80,0x10,0x00,0x00,0x00)) 217 | New-ItemProperty -Path $RegPath -Name "UserPreferencesMask" -Value $RegValue -PropertyType "Binary" -Force | Out-Null 218 | [GC]::Collect() 219 | reg unload $DefaultUserReg 220 | } -Session $VMSession 221 | 222 | Write-DLabLog "Disabling Bing Search in Start Menu" 223 | 224 | Invoke-Command -ScriptBlock { 225 | $DefaultUserReg = "HKLM\TempDefault" 226 | $NtuserDatPath = "C:\Users\Default\NTUSER.DAT" 227 | reg load $DefaultUserReg $NtuserDatPath 228 | $HKDU = "Registry::$DefaultUserReg" 229 | $RegPath = "$HKDU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" 230 | New-Item -Path $RegPath -Force | Out-Null 231 | Set-ItemProperty -Path $RegPath -Name "BingSearchEnabled" -Value 1 -Type DWORD 232 | [GC]::Collect() 233 | [GC]::WaitForPendingFinalizers() 234 | reg unload $DefaultUserReg 235 | } -Session $VMSession 236 | 237 | Write-DLabLog "Hiding 'Learn more about this picture' desktop icon" 238 | 239 | Invoke-Command -ScriptBlock { 240 | $DefaultUserReg = "HKLM\TempDefault" 241 | $NtuserDatPath = "C:\Users\Default\NTUSER.DAT" 242 | reg load $DefaultUserReg $NtuserDatPath 243 | $HKDU = "Registry::$DefaultUserReg" 244 | $RegPath = "$HKDU\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" 245 | New-Item -Path $RegPath -Force | Out-Null 246 | Set-ItemProperty -Path $RegPath -Name '{2cc5ca98-6485-489a-920e-b3e88a6ccce3}' -Value 0 -Type DWORD 247 | [GC]::Collect() 248 | [GC]::WaitForPendingFinalizers() 249 | reg unload $DefaultUserReg 250 | } -Session $VMSession 251 | 252 | Write-DLabLog "Tweaking default taskbar and explorer settings" 253 | 254 | Invoke-Command -ScriptBlock { 255 | $DefaultUserReg = "HKLM\TempDefault" 256 | $NtuserDatPath = "C:\Users\Default\NTUSER.DAT" 257 | reg load $DefaultUserReg $NtuserDatPath 258 | $HKDU = "Registry::$DefaultUserReg" 259 | $RegExplorerPath = "$HKDU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" 260 | New-Item -Path $RegExplorerPath -Force | Out-Null 261 | Set-ItemProperty -Path $RegExplorerPath -Name "IsSearchBoxSettingEnabled" -Value 0 -Type DWORD 262 | Set-ItemProperty -Path $RegExplorerPath -Name "ShowTaskViewButton" -Value 0 -Type DWORD 263 | Set-ItemProperty -Path $RegExplorerPath -Name "HideFileExt" -Value 0 -Type DWORD 264 | Set-ItemProperty -Path $RegExplorerPath -Name "Hidden" -Value 1 -Type DWORD 265 | Set-ItemProperty -Path $RegExplorerPath -Name "TaskbarDa" -Value 0 -Type DWORD 266 | Set-ItemProperty -Path $RegExplorerPath -Name "TaskbarMn" -Value 0 -Type DWORD 267 | Set-ItemProperty -Path $RegExplorerPath -Name "ShowCopilotButton" -Value 0 -Type DWORD 268 | $RegSearchPath = "$HKDU\Software\Microsoft\Windows\CurrentVersion\Search" 269 | New-Item -Path $RegSearchPath -Force | Out-Null 270 | Set-ItemProperty -Path $RegSearchPath -Name "SearchboxTaskbarMode" -Value 0 -Type DWORD 271 | [GC]::Collect() 272 | [GC]::WaitForPendingFinalizers() 273 | reg unload $DefaultUserReg 274 | } -Session $VMSession 275 | 276 | Write-DLabLog "Removing Azure Arc setup with annoying tray icon" 277 | 278 | Invoke-Command -ScriptBlock { 279 | Remove-WindowsCapability -Online -Name AzureArcSetup~~~~ 280 | } -Session $VMSession 281 | 282 | $CloudflareWarpCA = Get-ChildItem "cert:\LocalMachine\Root" | 283 | Where-Object { $_.Subject -like "*Gateway CA - Cloudflare Managed G1*" } 284 | 285 | if ($CloudflareWarpCA) { 286 | Write-DLabLog "Found Cloudflare WARP CA: $($CloudflareWarpCA.Subject)" 287 | 288 | $TempCertPath = Join-Path $Env:TEMP "CloudflareWarpCA.cer" 289 | Export-Certificate -Cert $CloudflareWarpCA -FilePath $TempCertPath -Force 290 | Copy-Item -Path $TempCertPath -Destination "C:\Users\Public\" -ToSession $VMSession 291 | Remove-Item $TempCertPath -Force | Out-Null 292 | 293 | Write-DLabLog "Importing Cloudflare WARP CA..." 294 | Invoke-Command -ScriptBlock { 295 | $CertPath = "C:\Users\Public\CloudflareWarpCA.cer" 296 | Import-Certificate -FilePath $CertPath -CertStoreLocation "cert:\LocalMachine\Root" | Out-Null 297 | Remove-Item $CertPath -Force | Out-Null 298 | } -Session $VMSession 299 | } 300 | 301 | Write-DLabLog "Configuring initial PowerShell environment" 302 | 303 | Invoke-Command -ScriptBlock { 304 | Set-ExecutionPolicy Unrestricted -Force 305 | Install-PackageProvider Nuget -Force 306 | Install-Module -Name PowerShellGet -Force 307 | Set-PSRepository -Name "PSGallery" -InstallationPolicy "Trusted" 308 | } -Session $VMSession 309 | 310 | Write-DLabLog "Installing chocolatey package manager" 311 | 312 | Invoke-Command -ScriptBlock { 313 | iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 314 | } -Session $VMSession 315 | 316 | $VMSession = New-DLabVMSession $VMName -UserName $UserName -Password $Password 317 | 318 | Write-DLabLog "Installing .NET Framework 4.8" 319 | 320 | Invoke-Command -ScriptBlock { 321 | choco install -y --no-progress netfx-4.8 322 | } -Session $VMSession 323 | 324 | Write-DLabLog "Installing git" 325 | 326 | Invoke-Command -ScriptBlock { 327 | $GitReleaseApi = "https://api.github.com/repos/git-for-windows/git/releases/latest" 328 | $GitRelease = Invoke-RestMethod -Uri $GitReleaseApi -Headers @{ "User-Agent" = "PowerShell" } 329 | $GitArchSuffix = if ($Env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { "arm64" } else { "64-bit" } 330 | $Asset = $GitRelease.assets | Where-Object { $_.name -match "^Git-[\d\.]+-$GitArchSuffix\.exe$" } | Select-Object -First 1 331 | $InstallerUrl = $Asset.browser_download_url 332 | $InstallerPath = Join-Path $Env:TEMP $($Asset.name) 333 | $ProgressPreference = 'SilentlyContinue' 334 | Invoke-WebRequest -Uri $InstallerUrl -OutFile $InstallerPath 335 | $InstallerArgs = @("/VERYSILENT", "/NORESTART", "/NOCANCEL", "/COMPONENTS=gitlfs,assoc,assoc_sh,ext,path") 336 | Start-Process -FilePath $InstallerPath -ArgumentList $InstallerArgs -Wait 337 | Remove-Item $InstallerPath -Force 338 | } -Session $VMSession 339 | 340 | if ($InstallChocolateyPackages) { 341 | $Packages = @( 342 | 'vlc', 343 | '7zip', 344 | 'gsudo', 345 | 'ripgrep', 346 | 'nssm', 347 | 'azcopy10' 348 | 'firefox', 349 | 'microsoft-edge', 350 | 'vscode', 351 | 'kdiff3', 352 | 'filezilla', 353 | 'wireshark', 354 | 'sublimetext3', 355 | 'notepadplusplus' 356 | ) 357 | 358 | foreach ($Package in $Packages) { 359 | Write-DLabLog "Installing $Package" 360 | Invoke-Command -Session $VMSession -ScriptBlock { param($Package) 361 | choco install -y --no-progress $Package 362 | } -ArgumentList @($Package) 363 | } 364 | } 365 | 366 | Write-DLabLog "Installing OpenSSL" 367 | 368 | Invoke-Command -ScriptBlock { 369 | $ProgressPreference = "SilentlyContinue" 370 | $openssl_hashes = 'https://github.com/slproweb/opensslhashes/raw/master/win32_openssl_hashes.json' 371 | $openssl_json = (Invoke-WebRequest -UseBasicParsing $openssl_hashes).Content | ConvertFrom-Json 372 | $openssl_filenames = Get-Member -InputObject $openssl_json.files -MemberType NoteProperty | Select-Object -ExpandProperty Name 373 | $openssl_file = $openssl_filenames | ForEach-Object { $openssl_json.files.$($_) } | Where-Object { 374 | ($_.installer -eq 'msi') -and ($_.bits -eq 64) -and ($_.arch -eq 'INTEL') -and ($_.light -eq $false) -and ($_.basever -like "3.*") 375 | } | Select-Object -First 1 376 | $openssl_file_url = $openssl_file.url 377 | $openssl_file_hash = $openssl_file.sha256 378 | Invoke-WebRequest -UseBasicParsing $openssl_file_url -OutFile "OpenSSL.msi" 379 | $FileHash = (Get-FileHash "OpenSSL.msi" -Algorithm SHA256).Hash 380 | if ($FileHash -ine $openssl_file_hash) { 381 | throw "Unexpected OpenSSL file hash: actual: $FileHash, expected: $openssl_file_hash" 382 | } 383 | Start-Process msiexec.exe -Wait -ArgumentList @("/i", "OpenSSL.msi", "/qn") 384 | [Environment]::SetEnvironmentVariable("PATH", "${Env:PATH};${Env:ProgramFiles}\OpenSSL-Win64\bin", "Machine") 385 | Remove-Item "OpenSSL.msi" 386 | } -Session $VMSession 387 | 388 | Write-DLabLog "Installing Devolutions MsRdpEx" 389 | 390 | Invoke-Command -ScriptBlock { 391 | $ProgressPreference = "SilentlyContinue" 392 | $MsRdpExVersion = (Invoke-RestMethod "https://api.github.com/repos/Devolutions/MsRdpEx/releases/latest").tag_name.TrimStart("v") 393 | $MsRdpExUrl = "https://github.com/Devolutions/MsRdpEx/releases/download/v$MsRdpExVersion/MsRdpEx-$MsRdpExVersion-x64.msi" 394 | Invoke-WebRequest -UseBasicParsing -Uri $MsRdpExUrl -OutFile "MsRdpEx.msi" 395 | Start-Process msiexec.exe -Wait -ArgumentList @("/i", "MsRdpEx.msi", "/qn") 396 | Remove-Item "MsRdpEx.msi" 397 | } -Session $VMSession 398 | 399 | Write-DLabLog "Installing Devolutions Windows Terminal" 400 | 401 | Invoke-Command -ScriptBlock { 402 | $ProgressPreference = "SilentlyContinue" 403 | $WtVersion = (Invoke-RestMethod "https://api.github.com/repos/Devolutions/wt-distro/releases/latest").tag_name.TrimStart("v") 404 | $WtDownloadBase = "https://github.com/Devolutions/wt-distro/releases/download" 405 | $WtDownloadUrl = "$WtDownloadBase/v${WtVersion}/WindowsTerminal-${WtVersion}-x64.msi" 406 | Invoke-WebRequest -UseBasicParsing $WtDownloadUrl -OutFile "WindowsTerminal.msi" 407 | Start-Process msiexec.exe -Wait -ArgumentList @("/i", "WindowsTerminal.msi", "/qn") 408 | Remove-Item "WindowsTerminal.msi" 409 | } -Session $VMSession 410 | 411 | Write-DLabLog "Fixing DbgHelp DLLs and _NT_SYMBOL_PATH" 412 | 413 | Invoke-Command -ScriptBlock { 414 | $ProgressPreference = "SilentlyContinue" 415 | New-Item -ItemType Directory -Path "C:\symbols" -ErrorAction SilentlyContinue | Out-Null 416 | [Environment]::SetEnvironmentVariable("_NT_SYMBOL_PATH", "srv*c:\symbols*https://msdl.microsoft.com/download/symbols", "Machine") 417 | 418 | $DbgHelpDir = "c:\symbols\DbgHelp" 419 | New-Item -ItemType Directory -Path $DbgHelpDir -ErrorAction SilentlyContinue | Out-Null 420 | 421 | $NativeDir = if ($Env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { "arm64" } else { "amd64" } 422 | $Packages = @{ 423 | "Microsoft.Debugging.Platform.DbgEng" = "content/$NativeDir/dbghelp.dll"; 424 | "Microsoft.Debugging.Platform.SrcSrv" = "content/$NativeDir/srcsrv.dll"; 425 | "Microsoft.Debugging.Platform.SymSrv" = "content/$NativeDir/symsrv.dll" 426 | } 427 | foreach ($Package in $Packages.GetEnumerator()) { 428 | $PackageName = $Package.Key 429 | $FilePath = $Package.Value 430 | $TempNupkgPath = "$Env:TEMP\$PackageName.zip" 431 | $TempExtractPath = "$Env:TEMP\$PackageName" 432 | $DownloadUrl = "https://www.nuget.org/api/v2/package/$PackageName" 433 | 434 | # Download raw .nupkg as a .zip file 435 | Invoke-WebRequest -Uri $DownloadUrl -OutFile $TempNupkgPath 436 | Expand-Archive -Path $TempNupkgPath -DestinationPath $TempExtractPath 437 | 438 | $FileToCopy = Join-Path $TempExtractPath $FilePath 439 | if (Test-Path -Path $FileToCopy) { 440 | Copy-Item -Path $FileToCopy -Destination $DbgHelpDir 441 | } 442 | 443 | Remove-Item -Path $TempNupkgPath 444 | Remove-Item -Path $TempExtractPath -Recurse 445 | } 446 | 447 | $DefaultUserReg = "HKLM\TempDefault" 448 | $NtuserDatPath = "C:\Users\Default\NTUSER.DAT" 449 | reg load $DefaultUserReg $NtuserDatPath 450 | $HKDU = "Registry::$DefaultUserReg" 451 | @('Process Monitor', 'Process Explorer') | ForEach-Object { 452 | $RegPath = "$HKDU\Software\Sysinternals\$_" 453 | New-Item -Path $RegPath -Force | Out-Null 454 | Set-ItemProperty -Path $RegPath -Name "EulaAccepted" -Value 1 -Type DWORD 455 | Set-ItemProperty -Path $RegPath -Name "DbgHelpPath" -Value "C:\symbols\DbgHelp\dbghelp.dll" -Type String 456 | } 457 | [GC]::Collect() 458 | [GC]::WaitForPendingFinalizers() 459 | reg unload $DefaultUserReg 460 | } -Session $VMSession 461 | 462 | Write-DLabLog "Accepting EULA on sysinternals tools" 463 | 464 | Invoke-Command -ScriptBlock { 465 | $DefaultUserReg = "HKLM\TempDefault" 466 | $NtuserDatPath = "C:\Users\Default\NTUSER.DAT" 467 | reg load $DefaultUserReg $NtuserDatPath 468 | $HKDU = "Registry::$DefaultUserReg" 469 | $ToolNames = @( 470 | "AccessChk", "AccessEnum", "Active Directory Explorer", "ADInsight", "Autologon", 471 | "Autoruns", "BGInfo", "CacheSet", "ClockRes", "Contig", "Coreinfo", "CPUSTRES", 472 | "Ctrl2cap", "DbgView", "Desktops", "Disk2Vhd", "Diskmon", "DiskView", "EFSDump", 473 | "Handle", "Hex2Dec", "Junction", "LdmDump", "ListDLLs", "LiveKd", "LoadOrder", 474 | "LogonSessions", "Movefile", "NotMyFault", "NTFSInfo", "PendMove", "Portmon", 475 | "ProcDump", "Process Explorer", "Process Monitor", "PsExec", "PsFile", "PsGetSid", 476 | "PsInfo", "PsKill", "PsList", "PsLoggedon", "PsLoglist", "PsPasswd", "PsPing", 477 | "PsService", "PsShutdown", "PsSuspend", "RamMap", "RegDelNull", "Regjump", 478 | "Regsize", "SDelete", "Share Enum", "ShareEnum", "ShellRunas", "sigcheck", 479 | "Streams", "Strings", "Sync", "Sysmon", "TcpView", "VMMap", "VolumeID", "Whois", 480 | "WinObj", "ZoomIt" 481 | ) 482 | $ToolNames | ForEach-Object { 483 | $RegPath = "$HKDU\Software\Sysinternals\$_" 484 | New-Item -Path $RegPath -Force | Out-Null 485 | Set-ItemProperty -Path $RegPath -Name "EulaAccepted" -Value 1 -Type DWORD 486 | } 487 | [GC]::Collect() 488 | [GC]::WaitForPendingFinalizers() 489 | reg unload $DefaultUserReg 490 | } -Session $VMSession 491 | 492 | Write-DLabLog "Downloading tool installers" 493 | 494 | Invoke-Command -ScriptBlock { 495 | $ProgressPreference = "SilentlyContinue" 496 | New-Item -ItemType Directory -Path "C:\tools" -ErrorAction SilentlyContinue | Out-Null 497 | New-Item -ItemType Directory -Path "C:\tools\bin" -ErrorAction SilentlyContinue | Out-Null 498 | New-Item -ItemType Directory -Path "C:\tools\scripts" -ErrorAction SilentlyContinue | Out-Null 499 | New-Item -ItemType Directory -Path "C:\tools\installers" -ErrorAction SilentlyContinue | Out-Null 500 | [Environment]::SetEnvironmentVariable("PATH", "${Env:PATH};C:\tools\bin", "Machine") 501 | Set-Location "C:\tools\installers" 502 | Invoke-WebRequest 'https://npcap.com/dist/npcap-1.78.exe' -OutFile "npcap-1.78.exe" 503 | Invoke-WebRequest 'http://update.youngzsoft.com/ccproxy/update/ccproxysetup.exe' -OutFile "CCProxySetup.exe" 504 | Invoke-WebRequest "https://assets.dataflare.app/release/windows/x86_64/Dataflare-Setup.exe" -OutFile "Dataflare-Setup.exe" 505 | } -Session $VMSession 506 | 507 | Write-DLabLog "Copying PowerShell helper scripts" 508 | 509 | Copy-Item -Path "$PSScriptRoot\scripts\*" -Destination "C:\tools\scripts" -ToSession $VMSession -Recurse -Force 510 | 511 | Write-DLabLog "Installing WinSpy" 512 | 513 | Invoke-Command -ScriptBlock { 514 | $ProgressPreference = "SilentlyContinue" 515 | # https://www.catch22.net/projects/winspy/ 516 | Invoke-WebRequest "https://github.com/strobejb/winspy/releases/download/v1.8.4/WinSpy_Release_x64.zip" -OutFile "WinSpy_Release.zip" 517 | Expand-Archive -Path ".\WinSpy_Release.zip" -DestinationPath ".\WinSpy_Release" -Force 518 | Copy-Item ".\WinSpy_Release\winspy.exe" "C:\tools\bin" -Force 519 | Remove-Item ".\WinSpy_Release*" -Recurse -Force 520 | } -Session $VMSession 521 | 522 | Write-DLabLog "Installing Sysinternals Suite" 523 | 524 | Invoke-Command -ScriptBlock { 525 | $ProgressPreference = "SilentlyContinue" 526 | Invoke-WebRequest "https://download.sysinternals.com/files/SysinternalsSuite.zip" -OutFile "SysinternalsSuite.zip" 527 | Expand-Archive -Path ".\SysinternalsSuite.zip" -DestinationPath "C:\tools\bin" -Force 528 | Remove-Item ".\SysinternalsSuite.zip" 529 | } -Session $VMSession 530 | 531 | Write-DLabLog "Installing Nirsoft tools" 532 | 533 | Invoke-Command -ScriptBlock { 534 | $ProgressPreference = "SilentlyContinue" 535 | Set-Location "C:\tools" 536 | # https://www.nirsoft.net/utils/regscanner.html 537 | Invoke-WebRequest 'https://www.nirsoft.net/utils/regscanner_setup.exe' -OutFile "regscanner_setup.exe" 538 | Start-Process -FilePath ".\regscanner_setup.exe" -ArgumentList @('/S') -Wait -NoNewWindow 539 | Remove-Item ".\regscanner_setup.exe" 540 | # https://www.nirsoft.net/utils/full_event_log_view.html 541 | Invoke-WebRequest 'https://www.nirsoft.net/utils/fulleventlogview-x64.zip' -OutFile "fulleventlogview-x64.zip" 542 | Expand-Archive -Path ".\fulleventlogview-x64.zip" -DestinationPath "C:\tools\bin" -Force 543 | Remove-Item ".\fulleventlogview-x64.zip" 544 | # https://www.nirsoft.net/utils/gui_prop_view.html 545 | Invoke-WebRequest 'https://www.nirsoft.net/utils/guipropview-x64.zip' -OutFile "guipropview-x64.zip" 546 | Expand-Archive -Path ".\guipropview-x64.zip" -DestinationPath "C:\tools\bin" -Force 547 | Remove-Item ".\guipropview-x64.zip" 548 | # https://www.nirsoft.net/utils/dns_query_sniffer.html 549 | Invoke-WebRequest 'https://www.nirsoft.net/utils/dnsquerysniffer-x64.zip' -OutFile "dnsquerysniffer-x64.zip" 550 | Expand-Archive -Path ".\dnsquerysniffer-x64.zip" -DestinationPath "C:\tools\bin" -Force 551 | Remove-Item ".\dnsquerysniffer-x64.zip" 552 | # https://www.nirsoft.net/utils/dns_lookup_view.html 553 | Invoke-WebRequest 'https://www.nirsoft.net/utils/dnslookupview.zip' -OutFile "dnslookupview.zip" 554 | Expand-Archive -Path ".\dnslookupview.zip" -DestinationPath "C:\tools\bin" -Force 555 | Remove-Item ".\dnslookupview.zip" 556 | # https://www.nirsoft.net/utils/inside_clipboard.html 557 | Invoke-WebRequest 'https://www.nirsoft.net/utils/insideclipboard.zip' -OutFile "insideclipboard.zip" 558 | Expand-Archive -Path ".\insideclipboard.zip" -DestinationPath "C:\tools\bin" -Force 559 | Remove-Item ".\insideclipboard.zip" 560 | # https://www.nirsoft.net/utils/file_activity_watch.html 561 | Invoke-WebRequest 'https://www.nirsoft.net/utils/fileactivitywatch-x64.zip' -OutFile "fileactivitywatch-x64.zip" 562 | Expand-Archive -Path ".\fileactivitywatch-x64.zip" -DestinationPath "C:\tools\bin" -Force 563 | Remove-Item ".\fileactivitywatch-x64.zip" 564 | # https://www.nirsoft.net/utils/registry_changes_view.html 565 | Invoke-WebRequest 'https://www.nirsoft.net/utils/registrychangesview-x64.zip' -OutFile "registrychangesview-x64.zip" 566 | Expand-Archive -Path ".\registrychangesview-x64.zip" -DestinationPath "C:\tools\bin" -Force 567 | Remove-Item ".\registrychangesview-x64.zip" 568 | # https://www.nirsoft.net/utils/reg_file_from_application.html 569 | Invoke-WebRequest 'https://www.nirsoft.net/utils/regfromapp-x64.zip' -OutFile "regfromapp-x64.zip" 570 | Expand-Archive -Path ".\regfromapp-x64.zip" -DestinationPath "C:\tools\bin" -Force 571 | Remove-Item ".\regfromapp-x64.zip" 572 | # cleanup binary output directory 573 | Remove-Item "C:\tools\bin\*.txt" 574 | Remove-Item "C:\tools\bin\*.chm" 575 | } -Session $VMSession 576 | 577 | Write-DLabLog "Installing UltraVNC" 578 | 579 | Invoke-Command -ScriptBlock { 580 | Invoke-WebRequest 'https://www.uvnc.eu/download/1430/UltraVNC_1431_X64_Setup.exe' -OutFile "UltraVNC_Setup.exe" 581 | Start-Process .\UltraVNC_Setup.exe -Wait -ArgumentList ("/VERYSILENT", "/NORESTART") 582 | Remove-Item .\UltraVNC_Setup.exe 583 | 584 | $Params = @{ 585 | Name = "uvnc_service"; 586 | DisplayName = "UltraVNC Server"; 587 | Description = "Provides secure remote desktop sharing"; 588 | BinaryPathName = "$Env:ProgramFiles\uvnc bvba\UltraVNC\winvnc.exe -service"; 589 | DependsOn = "Tcpip"; 590 | StartupType = "Automatic"; 591 | } 592 | New-Service @Params 593 | 594 | $Params = @{ 595 | DisplayName = "Allow UltraVNC"; 596 | Direction = "Inbound"; 597 | Program = "$Env:ProgramFiles\uvnc bvba\UltraVNC\winvnc.exe"; 598 | Action = "Allow" 599 | } 600 | New-NetFirewallRule @Params 601 | 602 | $IniFile = "$Env:ProgramFiles\uvnc bvba\UltraVNC\ultravnc.ini" 603 | $IniData = Get-Content $IniFile | foreach { 604 | switch ($_) { 605 | "MSLogonRequired=0" { "MSLogonRequired=1" } 606 | "NewMSLogon=0" { "NewMSLogon=1" } 607 | default { $_ } 608 | } 609 | } 610 | $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False 611 | [System.IO.File]::WriteAllLines($IniFile, $IniData, $Utf8NoBomEncoding) 612 | 613 | $AclFile = "$Env:ProgramFiles\uvnc bvba\UltraVNC\acl.txt" 614 | $AclData = "allow`t0x00000003`t`"BUILTIN\Remote Desktop Users`"" 615 | $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False 616 | [System.IO.File]::WriteAllLines($AclFile, $AclData, $Utf8NoBomEncoding) 617 | Start-Process -FilePath "$Env:ProgramFiles\uvnc bvba\UltraVNC\MSLogonACL.exe" -ArgumentList @('/i', '/o', $AclFile) -Wait -NoNewWindow 618 | } -Session $VMSession 619 | 620 | Write-DLabLog "Configuring Firefox to trust system root CAs" 621 | 622 | Invoke-Command -ScriptBlock { 623 | $RegPath = "HKLM:\Software\Policies\Mozilla\Firefox\Certificates" 624 | New-Item -Path $RegPath -Force | Out-Null 625 | New-ItemProperty -Path $RegPath -Name ImportEnterpriseRoots -Value 1 -Force | Out-Null 626 | } -Session $VMSession 627 | 628 | Write-DLabLog "Disabling Microsoft Edge first run experience" 629 | 630 | Invoke-Command -ScriptBlock { 631 | $RegPath = "HKLM:\Software\Policies\Microsoft\Edge" 632 | New-Item -Path $RegPath -Force | Out-Null 633 | New-ItemProperty -Path $RegPath -Name "HideFirstRunExperience" -Value 1 -Force | Out-Null 634 | New-ItemProperty -Path $RegPath -Name "NewTabPageLocation" -Value "https://www.google.com" -Force | Out-Null 635 | } -Session $VMSession 636 | 637 | Write-DLabLog "Disabling Secure Time Seeding (STS) problematic feature" 638 | 639 | Invoke-Command -ScriptBlock { 640 | Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" -Name "UtilizeSslTimeData" -Value 0 -Type DWORD 641 | } -Session $VMSession 642 | 643 | Write-DLabLog "Installing Remote Server Administration DNS tools" 644 | 645 | Invoke-Command -ScriptBlock { 646 | Install-WindowsFeature RSAT-DNS-Server 647 | } -Session $VMSession 648 | 649 | Write-DLabLog "Enabling OpenSSH client and server features" 650 | 651 | Invoke-Command -ScriptBlock { 652 | Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 653 | Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 654 | } -Session $VMSession 655 | 656 | Write-DLabLog "Installing PowerShell 7" 657 | 658 | Invoke-Command -ScriptBlock { 659 | [Environment]::SetEnvironmentVariable("POWERSHELL_UPDATECHECK", "0", "Machine") 660 | [Environment]::SetEnvironmentVariable("POWERSHELL_TELEMETRY_OPTOUT", "1", "Machine") 661 | iex "& { $(irm https://aka.ms/install-powershell.ps1) } -UseMSI -Quiet -EnablePSRemoting" 662 | } -Session $VMSession 663 | 664 | Write-DLabLog "Rebooting VM" 665 | 666 | Invoke-Command -ScriptBlock { 667 | Restart-Computer -Force 668 | } -Session $VMSession 669 | 670 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 671 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $UserName -Password $Password 672 | 673 | $VMSession = New-DLabVMSession $VMName -UserName $UserName -Password $Password 674 | 675 | Write-DLabLog "Installing useful PowerShell modules" 676 | 677 | Invoke-Command -ScriptBlock { 678 | Install-Module -Name PsHosts -Scope AllUsers 679 | Install-Module -Name Posh-ACME -Scope AllUsers 680 | Install-Module -Name PSWindowsUpdate -Scope AllUsers 681 | Install-Module -Name Evergreen -Scope AllUsers -Force 682 | Install-Module -Name PSDetour -Scope AllUsers -Force 683 | Install-Module -Name AwakeCoding.DebugTools -Scope AllUsers -Force 684 | Install-Module -Name Microsoft.PowerShell.SecretManagement -Scope AllUsers 685 | Install-Module -Name Microsoft.PowerShell.SecretStore -Scope AllUsers 686 | } -Session $VMSession 687 | 688 | Write-DLabLog "Enabling and starting sshd service" 689 | 690 | Invoke-Command -ScriptBlock { 691 | Install-Module -Name Microsoft.PowerShell.RemotingTools -Scope AllUsers -Force 692 | Set-Service -Name sshd -StartupType 'Automatic' 693 | Start-Service sshd 694 | } -Session $VMSession 695 | 696 | $VMSession = New-DLabVMSession $VMName -UserName $UserName -Password $Password 697 | 698 | Write-DLabLog "Enabling PowerShell Remoting over SSH" 699 | 700 | Invoke-Command -ScriptBlock { 701 | & pwsh.exe -NoLogo -Command "Enable-SSHRemoting -Force" 702 | Restart-Service sshd 703 | } -Session $VMSession 704 | 705 | Write-DLabLog "Enabling ICMP requests (ping) in firewall" 706 | 707 | Invoke-Command -ScriptBlock { 708 | New-NetFirewallRule -Name 'ICMPv4' -DisplayName 'ICMPv4' ` 709 | -Description 'Allow ICMPv4' -Profile Any -Direction Inbound -Action Allow ` 710 | -Protocol ICMPv4 -Program Any -LocalAddress Any -RemoteAddress Any 711 | } -Session $VMSession 712 | 713 | Write-DLabLog "Enabling network discovery, file and printer sharing in firewall" 714 | 715 | Invoke-Command -ScriptBlock { 716 | & netsh advfirewall firewall set rule group="Network Discovery" new enable=yes 717 | & netsh advfirewall firewall set rule group="File and Printer Sharing" new enable=yes 718 | } -Session $VMSession 719 | 720 | Write-DLabLog "Enabling remote desktop server and firewall rule" 721 | 722 | Invoke-Command -ScriptBlock { 723 | Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections" -Value 0 724 | Enable-NetFirewallRule -DisplayGroup "Remote Desktop" 725 | 726 | Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows NT\Terminal Services' -Name 'ColorDepth' -Type DWORD -Value 5 727 | Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows NT\Terminal Services' -Name 'fEnableVirtualizedGraphics' -Type DWORD -Value 1 728 | } -Session $VMSession 729 | 730 | Write-DLabLog "Changing Windows taskbar default pinned apps" 731 | 732 | Invoke-Command -ScriptBlock { 733 | $LnkPaths = @( 734 | "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk" 735 | "%APPDATA%\Microsoft\Windows\Start Menu\Programs\File Explorer.lnk" 736 | "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Windows Terminal.lnk" 737 | "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\mstscex.lnk" 738 | ) 739 | $OutputPath = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\LayoutModification.xml" 740 | $xml = New-Object System.Xml.XmlDocument 741 | $root = $xml.CreateElement("LayoutModificationTemplate", "http://schemas.microsoft.com/Start/2014/LayoutModification") 742 | $xml.AppendChild($root) | Out-Null 743 | $root.SetAttribute("xmlns:defaultlayout", "http://schemas.microsoft.com/Start/2014/FullDefaultLayout") 744 | $root.SetAttribute("xmlns:taskbar", "http://schemas.microsoft.com/Start/2014/TaskbarLayout") 745 | $root.SetAttribute("Version", "1") 746 | $collection = $xml.CreateElement("CustomTaskbarLayoutCollection", $root.NamespaceURI) 747 | $collection.SetAttribute("PinListPlacement", "Replace") 748 | $root.AppendChild($collection) | Out-Null 749 | $layout = $xml.CreateElement("defaultlayout:TaskbarLayout", $root.GetAttribute("xmlns:defaultlayout")) 750 | $collection.AppendChild($layout) | Out-Null 751 | $pinList = $xml.CreateElement("taskbar:TaskbarPinList", $root.GetAttribute("xmlns:taskbar")) 752 | $layout.AppendChild($pinList) | Out-Null 753 | foreach ($lnk in $LnkPaths) { 754 | $desktopApp = $xml.CreateElement("taskbar:DesktopApp", $root.GetAttribute("xmlns:taskbar")) 755 | $desktopApp.SetAttribute("DesktopApplicationLinkPath", $lnk) 756 | $pinList.AppendChild($desktopApp) | Out-Null 757 | } 758 | $xml.Save($OutputPath) 759 | } -Session $VMSession 760 | 761 | if ($InstallSkillableServices) { 762 | Write-DLabLog "Installing Skillable Integration Services" 763 | 764 | Invoke-Command -ScriptBlock { 765 | $ProgressPreference = 'SilentlyContinue' 766 | $RepoName = "Skillable-Integration-Service" 767 | $ZipFile = "${RepoName}.zip" 768 | $DownloadUrl = "https://github.com/LearnOnDemandSystems/${RepoName}/archive/refs/heads/main.zip" 769 | $OutputPath = Join-Path $Env:TEMP "${RepoName}-main" 770 | Remove-Item $OutputPath -Recurse -ErrorAction SilentlyContinue | Out-Null 771 | Invoke-WebRequest -Uri $DownloadUrl -OutFile $ZipFile 772 | Expand-Archive -Path $ZipFile -DestinationPath $Env:TEMP 773 | Remove-Item $ZipFile | Out-Null 774 | Remove-Item "C:\VmIntegrationService" -Recurse -ErrorAction SilentlyContinue | Out-Null 775 | Expand-Archive "$OutputPath\VmIntegrationService.zip" -DestinationPath "C:\VmIntegrationService" 776 | Unblock-File "$OutputPath\install.ps1" 777 | & "$OutputPath\install.ps1" 778 | Remove-Item $OutputPath -Recurse | Out-Null 779 | } -Session $VMSession 780 | } 781 | 782 | Write-DLabLog "Rebooting VM" 783 | 784 | Invoke-Command -ScriptBlock { 785 | Restart-Computer -Force 786 | } -Session $VMSession 787 | 788 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 789 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $UserName -Password $Password 790 | 791 | $VMSession = New-DLabVMSession $VMName -UserName $UserName -Password $Password 792 | 793 | if ($InstallWindowsUpdates) { 794 | Write-DLabLog "Installing Windows updates until VM is fully up-to-date" 795 | 796 | do { 797 | $WUStatus = Invoke-Command -ScriptBlock { 798 | $Updates = Get-WUList 799 | if ($Updates.Count -gt 0) { 800 | Write-Host "Install-WindowsUpdate $($Updates.Count): $(Get-Date)" 801 | Install-WindowsUpdate -AcceptAll -AutoReboot | Out-Null 802 | } 803 | [PSCustomObject]@{ 804 | UpdateCount = $Updates.Count 805 | PendingReboot = Get-WURebootStatus -Silent 806 | } 807 | } -Session $VMSession 808 | 809 | Write-DLabLog "WUStatus: $($WUStatus.UpdateCount), PendingReboot: $($WUStatus.PendingReboot): $(Get-Date)" 810 | 811 | if ($WUStatus.PendingReboot) { 812 | Write-Host "Waiting for VM reboot: $(Get-Date)" 813 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 814 | Wait-VM $VMName -For IPAddress -Timeout 360 815 | Start-Sleep -Seconds 60 816 | $VMSession = New-DLabVMSession $VMName -UserName $UserName -Password $Password 817 | } 818 | } until (($WUStatus.PendingReboot -eq $false) -and ($WUStatus.UpdateCount -eq 0)) 819 | } 820 | 821 | Write-DLabLog "Remove Appx packages that can break sysprep" 822 | 823 | Invoke-Command -ScriptBlock { 824 | Get-AppxPackage -Name Microsoft.MicrosoftEdge.Stable | Remove-AppxPackage 825 | Get-AppxPackage *notepadplusplus* | Remove-AppxPackage 826 | } -Session $VMSession 827 | 828 | Write-DLabLog "Cleaning up Windows base image (WinSxS folder)" 829 | 830 | Invoke-Command -ScriptBlock { 831 | & dism.exe /Online /Cleanup-Image /StartComponentCleanup /ResetBase 832 | } -Session $VMSession 833 | 834 | if ($DisableWindowsUpdates) { 835 | Write-DLabLog "Disabling Windows Update service permanently" 836 | 837 | Invoke-Command -ScriptBlock { 838 | Stop-service wuauserv | Set-Service -StartupType Disabled 839 | New-Item -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force | Out-Null 840 | Set-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name NoAutoUpdate -Value 1 -Type DWORD 841 | } -Session $VMSession 842 | } 843 | 844 | Write-DLabLog "Running sysprep to generalize the image for OOBE experience and shut down VM" 845 | 846 | Invoke-Command -ScriptBlock { 847 | & "$Env:WinDir\System32\Sysprep\sysprep.exe" '/oobe' '/generalize' '/shutdown' '/mode:vm' 848 | } -Session $VMSession 849 | 850 | Write-DLabLog "Waiting for VM to shut down completely" 851 | Wait-DLabVM $VMName 'Shutdown' -Timeout 900 852 | 853 | Write-DLabLog "Deleting the VM (but not the VHDX)" 854 | Remove-VM $VMName -Force 855 | 856 | $ParentDisksPath = Get-DLabPath "IMGs" 857 | $ParentDiskFileName = $VMName, 'vhdx' -Join '.' 858 | $ParentDiskPath = Join-Path $ParentDisksPath $ParentDiskFileName 859 | 860 | $GoldenDiskFileName = "Windows Server $OSVersion Standard - $(Get-Date -Format FileDate).vhdx" 861 | $GoldenDiskPath = Join-Path $ParentDisksPath $GoldenDiskFileName 862 | 863 | if (-Not [string]::IsNullOrEmpty($OutputVhdxPath)) { 864 | $GoldenDiskPath = $OutputVhdxPath 865 | } 866 | 867 | Write-DLabLog "Moving golden VHDX" 868 | 869 | if (Test-Path $GoldenDiskPath) { 870 | Write-DLabLog "Removing previous golden VHDX" 871 | Remove-Item $GoldenDiskPath -Force | Out-Null 872 | } 873 | Move-Item -Path $ParentDiskPath -Destination $GoldenDiskPath 874 | 875 | Write-DLabLog "Optimizing golden VHDX for compact size" 876 | Optimize-VHD -Path $GoldenDiskPath -Mode Full 877 | 878 | Write-DLabLog "Setting golden VHDX file as read-only" 879 | Set-ItemProperty -Path $GoldenDiskPath -Name IsReadOnly $true 880 | 881 | Write-DLabLog "Golden VHDX Path: '$GoldenDiskPath'" 882 | -------------------------------------------------------------------------------- /powershell/gw_vm.ps1: -------------------------------------------------------------------------------- 1 | . .\common.ps1 2 | 3 | $VMAlias = "GW" 4 | $VMNumber = 7 5 | $VMName = $LabPrefix, $VMAlias -Join "-" 6 | $IpAddress = Get-DLabIpAddress $LabNetworkBase $VMNumber 7 | 8 | New-DLabVM $VMName -Password $LocalPassword -OSVersion $OSVersion -Force 9 | Start-DLabVM $VMName 10 | 11 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $LocalUserName -Password $LocalPassword 12 | $VMSession = New-DLabVMSession $VMName -UserName $LocalUserName -Password $LocalPassword 13 | 14 | Set-DLabVMNetAdapter $VMName -VMSession $VMSession ` 15 | -SwitchName $SwitchName -NetAdapterName $NetAdapterName ` 16 | -IPAddress $IPAddress -DefaultGateway $DefaultGateway ` 17 | -DnsServerAddress $DnsServerAddress 18 | 19 | Write-Host "Joining domain" 20 | 21 | Add-DLabVMToDomain $VMName -VMSession $VMSession ` 22 | -DomainName $DomainName -DomainController $DCHostName ` 23 | -UserName $DomainUserName -Password $DomainPassword 24 | 25 | # Wait for virtual machine to reboot after domain join operation 26 | 27 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 28 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 29 | 30 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 31 | 32 | Write-Host "Requesting RDP server certificate" 33 | 34 | Request-DLabRdpCertificate $VMName -VMSession $VMSession ` 35 | -CAHostName $CAHostName -CACommonName $CACommonName 36 | 37 | Write-Host "Initializing PSRemoting" 38 | 39 | Initialize-DLabPSRemoting $VMName -VMSession $VMSession 40 | 41 | Write-Host "Initializing VNC server" 42 | 43 | Initialize-DLabVncServer $VMName -VMSession $VMSession 44 | 45 | # Install Remote Desktop Gateway 46 | 47 | Write-Host "Installing RD Gateway, RD Web Access and RD Connection Broker" 48 | 49 | Invoke-Command -ScriptBlock { 50 | @('RDS-Gateway', 51 | 'RDS-Web-Access', 52 | 'RDS-Licensing', 53 | 'RDS-Licensing-UI', 54 | 'RDS-Connection-Broker', 55 | 'RSAT-RDS-Tools', 56 | 'RSAT-RDS-Gateway' 57 | ) | ForEach-Object { Install-WindowsFeature -Name $_ | Out-Null } 58 | } -Session $VMSession 59 | 60 | Write-Host "Creating default RD CAP and RD RAP" 61 | 62 | Invoke-Command -ScriptBlock { 63 | Import-Module RemoteDesktopServices 64 | $UserGroups = @("Administrators@BUILTIN", "Remote Desktop Users@BUILTIN") 65 | New-Item -Path RDS:\GatewayServer\CAP -Name "RD-CAP" -UserGroups $UserGroups -AuthMethod 1 66 | New-Item -Path RDS:\GatewayServer\RAP -Name "RD-RAP" -UserGroups $UserGroups -ComputerGroupType 2 67 | } -Session $VMSession 68 | 69 | $DnsName = "rdg" 70 | $RdgHostName = "$DnsName.$DomainName" 71 | $CertificateFile = "~\Documents\rdg-cert.pfx" 72 | $CertificatePassword = "cert123!" 73 | 74 | Write-Host "Creating new DNS record for RD Gateway" 75 | 76 | Invoke-Command -ScriptBlock { Param($DnsName, $DnsZoneName, $IPAddress, $DnsServer) 77 | Install-WindowsFeature RSAT-DNS-Server | Out-Null 78 | Add-DnsServerResourceRecordA -Name $DnsName -ZoneName $DnsZoneName -IPv4Address $IPAddress -AllowUpdateAny -ComputerName $DnsServer 79 | } -Session $VMSession -ArgumentList @($DnsName, $DomainName, $IPAddress, $DCHostName) 80 | 81 | Write-Host "Requesting new certificate for RD Gateway" 82 | 83 | Request-DLabCertificate $VMName -VMSession $VMSession ` 84 | -CommonName $RdgHostName ` 85 | -CAHostName $CAHostName -CACommonName $CACommonName ` 86 | -CertificateFile $CertificateFile -Password $CertificatePassword 87 | 88 | Write-Host "Installing RD Session Host" 89 | 90 | Invoke-Command -ScriptBlock { 91 | Install-WindowsFeature -Name RDS-RD-Server | Out-Null 92 | Restart-Computer -Force 93 | } -Session $VMSession 94 | 95 | Write-Host "Rebooting VM" 96 | 97 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 98 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 99 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 100 | 101 | # Create new RD session deployment 102 | 103 | $GWMachineName = "$VMName.$DomainName" 104 | $ConnectionBroker = $GWMachineName 105 | $SessionHost = $GWMachineName 106 | $WebAccessServer = $GWMachineName 107 | $GatewayExternalFQDN = $RdgHostName 108 | 109 | Invoke-Command -ScriptBlock { Param($ConnectionBroker, $SessionHost, $WebAccessServer, $GatewayExternalFQDN) 110 | $Params = @{ 111 | ConnectionBroker = $ConnectionBroker; 112 | SessionHost = $SessionHost; 113 | WebAccessServer = $WebAccessServer; 114 | } 115 | New-RDSessionDeployment @Params 116 | 117 | Add-RDServer -Server $ConnectionBroker -Role RDS-Licensing ` 118 | -ConnectionBroker $ConnectionBroker 119 | 120 | Add-RDServer -Server $WebAccessServer -Role RDS-Gateway ` 121 | -ConnectionBroker $ConnectionBroker -GatewayExternalFqdn $GatewayExternalFQDN 122 | } -Session $VMSession -ArgumentList @($ConnectionBroker, $SessionHost, $WebAccessServer, $GatewayExternalFQDN) 123 | 124 | Write-Host "Configuring RD Gateway certificate" 125 | 126 | Invoke-Command -ScriptBlock { Param($CertificateFile, $CertificatePassword) 127 | Import-Module RemoteDesktopServices 128 | $CertificatePassword = ConvertTo-SecureString $CertificatePassword -AsPlainText -Force 129 | $Params = @{ 130 | FilePath = $CertificateFile; 131 | CertStoreLocation = "cert:\LocalMachine\My"; 132 | Password = $CertificatePassword; 133 | Exportable = $true; 134 | } 135 | $Certificate = Import-PfxCertificate @Params 136 | Set-Item "RDS:\GatewayServer\SSLCertificate\Thumbprint" -Value $Certificate.Thumbprint 137 | Restart-Service TSGateway 138 | } -Session $VMSession -ArgumentList @($CertificateFile, $CertificatePassword) 139 | 140 | Write-Host "Creating RD session collection" 141 | 142 | $CollectionName = "Session Collection" 143 | $CollectionDescription = "Default Session Collection" 144 | 145 | Invoke-Command -ScriptBlock { Param($ConnectionBroker, $SessionHost, $CollectionName, $CollectionDescription) 146 | $Params = @{ 147 | CollectionName = $CollectionName; 148 | CollectionDescription = $CollectionDescription; 149 | SessionHost = @($SessionHost); 150 | ConnectionBroker = $ConnectionBroker; 151 | } 152 | New-RDSessionCollection @Params 153 | 154 | $Params = @{ 155 | DisplayName = "Notepad"; 156 | FilePath = "C:\Windows\System32\Notepad.exe"; 157 | CollectionName = $CollectionName; 158 | } 159 | New-RDRemoteApp @Params 160 | 161 | $Params = @{ 162 | DisplayName = "Windows Explorer"; 163 | FilePath = "C:\Windows\explorer.exe"; 164 | CollectionName = $CollectionName; 165 | } 166 | New-RDRemoteApp @Params 167 | 168 | $Params = @{ 169 | DisplayName = "Windows PowerShell"; 170 | FilePath = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"; 171 | CollectionName = $CollectionName; 172 | } 173 | New-RDRemoteApp @Params 174 | 175 | $Params = @{ 176 | DisplayName = "PowerShell 7"; 177 | FilePath = "C:\Program Files\PowerShell\7\pwsh.exe"; 178 | CollectionName = $CollectionName; 179 | } 180 | New-RDRemoteApp @Params 181 | 182 | $CAMachineName = $Env:ComputerName -Replace "-GW", "-DC" 183 | $Params = @{ 184 | DisplayName = "DNS Manager"; 185 | FilePath = "C:\Windows\System32\dnsmgmt.msc"; 186 | IconPath = "C:\Windows\System32\dnsmgr.dll"; 187 | CommandLineSetting = "Allow"; 188 | RequiredCommandLine = "/ComputerName $CAMachineName"; 189 | CollectionName = $CollectionName; 190 | } 191 | New-RDRemoteApp @Params 192 | 193 | $CollectionName = "Session Collection" 194 | $TransformedName = $CollectionName -Replace " ", "_" 195 | $FarmsRegPath = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\CentralPublishedResources\PublishedFarms" 196 | $FarmRegName = Get-ChildItem $FarmsRegPath | 197 | Where-Object { $TransformedName.StartsWith($_.PSChildName) } | 198 | Select-Object -ExpandProperty PSChildName 199 | 200 | Set-ItemProperty -Path "$FarmsRegPath\$FarmRegName\RemoteDesktops\$FarmRegName" -Name "Name" -Value "Remote Desktop" 201 | Set-ItemProperty -Path "$FarmsRegPath\$FarmRegName\RemoteDesktops\$FarmRegName" -Name "ShowInPortal" -Value 1 -Type DWORD 202 | 203 | } -Session $VMSession -ArgumentList @($ConnectionBroker, $SessionHost, $CollectionName, $CollectionDescription) 204 | 205 | Write-Host "Installing RD Web Client" 206 | 207 | Invoke-Command -ScriptBlock { Param($ConnectionBroker, $CertificateFile, $CertificatePassword) 208 | Import-Module RemoteDesktopServices 209 | $Thumbprint = $(Get-Item "RDS:\GatewayServer\SSLCertificate\Thumbprint").CurrentValue 210 | @('RDGateway', 'RDWebAccess', 'RDPublishing', 'RDRedirector') | ForEach-Object { 211 | Set-RDCertificate -Role $_ -Thumbprint $Thumbprint ` 212 | -ConnectionBroker $ConnectionBroker -Force 213 | } 214 | Install-Module RDWebClientManagement -Force -AcceptLicense 215 | Install-RDWebClientPackage 216 | $CertificateFile = Resolve-Path $CertificateFile 217 | $CertificatePassword = ConvertTo-SecureString $CertificatePassword -AsPlainText -Force 218 | Import-RDWebClientBrokerCert -Path $CertificateFile -Password $CertificatePassword 219 | Publish-RDWebClientPackage -Type Production -Latest 220 | } -Session $VMSession -ArgumentList @($ConnectionBroker, $CertificateFile, $CertificatePassword) 221 | 222 | Write-Host "Configure Terminal Server Licensing" 223 | 224 | Invoke-Command -ScriptBlock { 225 | $ts = Get-CimInstance -Namespace "Root/CIMV2/TerminalServices" -ClassName "Win32_TerminalServiceSetting" 226 | Invoke-CimMethod -InputObject $ts -MethodName "ChangeMode" -Arguments @{LicensingType = 4} # per user licensing 227 | Invoke-CimMethod -InputObject $ts -MethodName "SetSpecifiedLicenseServerList" -Arguments @{SpecifiedLSList = @($Env:ComputerName)} 228 | $licenseServers = Invoke-CimMethod -InputObject $ts -MethodName "GetSpecifiedLicenseServerList" 229 | Write-Host "Specified License Servers: $($licenseServers.SpecifiedLSList)" 230 | } -Session $VMSession 231 | 232 | Write-Host "Creating DNS record for Devolutions Gateway" 233 | 234 | $DnsName = "gateway" 235 | $DGatewayFQDN = "$DnsName.$DomainName" 236 | $CertificateFile = "~\Documents\gateway-cert.pfx" 237 | $CertificatePassword = "cert123!" 238 | 239 | Invoke-Command -ScriptBlock { Param($DnsName, $DnsZoneName, $IPAddress, $DnsServer) 240 | Add-DnsServerResourceRecordA -Name $DnsName -ZoneName $DnsZoneName -IPv4Address $IPAddress -AllowUpdateAny -ComputerName $DnsServer 241 | } -Session $VMSession -ArgumentList @($DnsName, $DomainName, $IPAddress, $DCHostName) 242 | 243 | Write-Host "Requesting certificate for Devolutions Gateway" 244 | 245 | Request-DLabCertificate $VMName -VMSession $VMSession ` 246 | -CommonName $DGatewayFQDN ` 247 | -CAHostName $CAHostName -CACommonName $CACommonName ` 248 | -CertificateFile $CertificateFile -Password $CertificatePassword 249 | 250 | Write-Host "Installing Devolutions Gateway" 251 | 252 | Invoke-Command -ScriptBlock { 253 | Install-Module -Name DevolutionsGateway -Force 254 | Import-Module DevolutionsGateway 255 | Install-DGatewayPackage 256 | } -Session $VMSession 257 | 258 | Write-Host "Configuring Devolutions Gateway" 259 | 260 | Invoke-Command -ScriptBlock { Param($DGatewayFQDN, $CertificateFile, $CertificatePassword) 261 | Import-Module DevolutionsGateway 262 | Import-DGatewayCertificate -CertificateFile $CertificateFile -Password $CertificatePassword 263 | Set-DGatewayHostname $DGatewayFQDN 264 | 265 | Set-DGatewayListeners @( 266 | $(New-DGatewayListener 'https://*:7171' 'https://*:7171'), 267 | $(New-DGatewayListener 'tcp://*:8181' 'tcp://*:8181')) 268 | 269 | Set-Service 'DevolutionsGateway' -StartupType 'Automatic' 270 | Start-Service 'DevolutionsGateway' 271 | } -Session $VMSession -ArgumentList @($DGatewayFQDN, $CertificateFile, $CertificatePassword) 272 | 273 | Write-Host "Creating DNS record for KDC Proxy" 274 | 275 | $DnsName = "kdc" 276 | $KdcPort = "4343" 277 | $KdcFQDN = "$DnsName.$DomainName" 278 | $CertificateFile = "~\Documents\kdc-cert.pfx" 279 | $CertificatePassword = "cert123!" 280 | 281 | Invoke-Command -ScriptBlock { Param($DnsName, $DnsZoneName, $IPAddress, $DnsServer) 282 | Add-DnsServerResourceRecordA -Name $DnsName -ZoneName $DnsZoneName -IPv4Address $IPAddress -AllowUpdateAny -ComputerName $DnsServer 283 | } -Session $VMSession -ArgumentList @($DnsName, $DomainName, $IPAddress, $DCHostName) 284 | 285 | Write-Host "Requesting certificate for KDC Proxy" 286 | 287 | Request-DLabCertificate $VMName -VMSession $VMSession ` 288 | -CommonName $KdcFQDN ` 289 | -CAHostName $CAHostName -CACommonName $CACommonName ` 290 | -CertificateFile $CertificateFile -Password $CertificatePassword 291 | 292 | Write-Host "Importing certificate for KDC Proxy" 293 | 294 | Invoke-Command -ScriptBlock { Param($CertificateFile, $CertificatePassword) 295 | $CertificatePassword = ConvertTo-SecureString $CertificatePassword -AsPlainText -Force 296 | $Params = @{ 297 | FilePath = $CertificateFile; 298 | CertStoreLocation = "cert:\LocalMachine\My"; 299 | Password = $CertificatePassword; 300 | Exportable = $true; 301 | } 302 | $Certificate = Import-PfxCertificate @Params 303 | } -Session $VMSession -ArgumentList @($CertificateFile, $CertificatePassword) 304 | 305 | Write-Host "Configuring KDC Proxy" 306 | 307 | Invoke-Command -ScriptBlock { Param($KdcFQDN, $KdcPort) 308 | $Certificate = Get-ChildItem -Path "cert:\LocalMachine\My" | ` 309 | Where-Object { $_.Subject -Like "*$KdcFQDN*" } | Select-Object -First 1 310 | 311 | $CertHash = $Certificate.Thumbprint 312 | $AppId = [Guid]::NewGuid().ToString("B") 313 | 314 | & "netsh" "http" "add" "urlacl" "url=https://+:$KdcPort/KdcProxy" 'user="NT AUTHORITY\Network Service"' 315 | & "netsh" "http" "add" "sslcert" "hostnameport=$KdcFQDN`:$KdcPort" "certhash=$CertHash" "appid=$AppId" "certstorename=MY" 316 | 317 | $KpsSvcSettingsReg = "HKLM:\SYSTEM\CurrentControlSet\Services\KPSSVC\Settings" 318 | New-ItemProperty -Path $KpsSvcSettingsReg -Name "HttpsClientAuth" -Type DWORD -Value 0 -Force 319 | New-ItemProperty -Path $KpsSvcSettingsReg -Name "DisallowUnprotectedPasswordAuth" -Type DWORD -Value 0 -Force 320 | New-ItemProperty -Path $KpsSvcSettingsReg -Name "HttpsUrlGroup" -Type MultiString -Value "+`:$KdcPort" -Force 321 | 322 | Set-Service -Name KPSSVC -StartupType Automatic 323 | Start-Service -Name KPSSVC 324 | 325 | New-NetFirewallRule -DisplayName "Allow KDCProxy TCP $KdcPort" -Direction Inbound -Protocol TCP -LocalPort $KdcPort 326 | } -Session $VMSession -ArgumentList @($KdcFQDN, $KdcPort) 327 | 328 | Write-Host "Creating DNS record for Windows Admin Center" 329 | 330 | $DnsName = "wac" 331 | $WacFQDN = "$DnsName.$DomainName" 332 | $CertificateFile = "~\Documents\cert.pfx" 333 | $CertificatePassword = "cert123!" 334 | 335 | Invoke-Command -ScriptBlock { Param($DnsName, $DnsZoneName, $IPAddress, $DnsServer) 336 | Add-DnsServerResourceRecordA -Name $DnsName -ZoneName $DnsZoneName -IPv4Address $IPAddress -AllowUpdateAny -ComputerName $DnsServer 337 | } -Session $VMSession -ArgumentList @($DnsName, $DomainName, $IPAddress, $DCHostName) 338 | 339 | Write-Host "Creating SPN for Windows Admin Center DNS name" 340 | 341 | $SpnAccountName = "$DomainNetbiosName\$VMName" 342 | 343 | Invoke-Command -ScriptBlock { Param($WacFQDN, $SpnAccountName) 344 | & "setspn" "-A" "HTTP/$WacFQDN" $SpnAccountName 345 | } -Session $VMSession -ArgumentList @($WacFQDN, $SpnAccountName) 346 | 347 | Write-Host "Requesting certificate for Windows Admin Center" 348 | 349 | Request-DLabCertificate $VMName -VMSession $VMSession ` 350 | -CommonName $WacFQDN ` 351 | -CAHostName $CAHostName -CACommonName $CACommonName ` 352 | -CertificateFile $CertificateFile -Password $CertificatePassword 353 | 354 | Write-Host "Adding Windows Admin Center firewall exceptions" 355 | 356 | Invoke-Command -ScriptBlock { 357 | $Params = @{ 358 | Profile = "Any"; 359 | LocalPort = 6516; 360 | Protocol = "TCP"; 361 | Action = "Allow"; 362 | DisplayName = "Windows Admin Center"; 363 | } 364 | New-NetFirewallRule -Direction Outbound @Params | Out-Null 365 | New-NetFirewallRule -Direction Inbound @Params | Out-Null 366 | } -Session $VMSession 367 | 368 | Write-Host "Installing Windows Admin Center" 369 | 370 | # https://docs.microsoft.com/en-us/windows-server/manage/windows-admin-center/deploy/install 371 | 372 | Invoke-Command -ScriptBlock { Param($WacFQDN, $CertificateFile, $CertificatePassword) 373 | $ProgressPreference = 'SilentlyContinue' 374 | $WacMsi = "$(Resolve-Path ~)\Documents\WAC.msi" 375 | #Invoke-WebRequest 'https://aka.ms/WACDownload' -OutFile $WacMsi 376 | Invoke-WebRequest 'https://download.microsoft.com/download/1/0/5/1059800B-F375-451C-B37E-758FFC7C8C8B/WindowsAdminCenter2410.exe' -OutFile $WacMsi 377 | $CertificatePassword = ConvertTo-SecureString $CertificatePassword -AsPlainText -Force 378 | $Params = @{ 379 | FilePath = $CertificateFile; 380 | CertStoreLocation = "cert:\LocalMachine\My"; 381 | Password = $CertificatePassword; 382 | Exportable = $true; 383 | } 384 | $Certificate = Import-PfxCertificate @Params 385 | $Thumbprint = $Certificate.Thumbprint 386 | $MsiArgs = @("/i", $WacMsi, "/qn", "/L*v", "log.txt", 387 | "SME_PORT=6516", "SME_THUMBPRINT=$Thumbprint", "SSL_CERTIFICATE_OPTION=installed") 388 | Start-Process msiexec.exe -Wait -ArgumentList $MsiArgs 389 | } -Session $VMSession -ArgumentList @($WacFQDN, $CertificateFile, $CertificatePassword) 390 | -------------------------------------------------------------------------------- /powershell/host_init.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | #Requires -PSEdition Core 3 | 4 | function Invoke-HostInit { 5 | param( 6 | ) 7 | 8 | $IsChocoPresent = [bool](Get-Command -Name choco -CommandType Application -ErrorAction SilentlyContinue) 9 | $IsWingetPresent = [bool](Get-Command -Name winget -CommandType Application -ErrorAction SilentlyContinue) 10 | 11 | $7ZipExe = Get-Command -Name 7z -CommandType Application -ErrorAction SilentlyContinue | 12 | Select-Object -ExpandProperty Source 13 | if (-Not $7ZipExe) { 14 | $7ZipExe = @( 15 | "${Env:ProgramFiles}\7-Zip\7z.exe", 16 | "${Env:ProgramFiles(x86)}\7-Zip\7z.exe" 17 | ) | Where-Object { Test-Path $_ } | Select-Object -First 1 18 | } 19 | 20 | if (-Not $7ZipExe) { 21 | if ($IsWingetPresent) { 22 | winget install 7zip.7zip 23 | } elseif ($IsChocoPresent) { 24 | choco install -y --no-progress 7zip 25 | } else { 26 | Write-Warning "7z.exe cannot be found or installed automatically" 27 | } 28 | } 29 | 30 | $RegPath = "HKLM:\Software\Policies\Mozilla\Firefox\Certificates" 31 | New-Item -Path $RegPath -Force | Out-Null 32 | New-ItemProperty -Path $RegPath -Name ImportEnterpriseRoots -Value 1 -Force | Out-Null 33 | 34 | New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\Credssp" ` 35 | -Name UseCachedCRLOnlyAndIgnoreRevocationUnknownErrors -Value 1 -Force | Out-Null 36 | 37 | if ($(Get-WindowsCapability -Online -Name "OpenSSH.Client~~~~0.0.1.0").State -ne "Installed") { 38 | Add-WindowsCapability -Online -Name "OpenSSH.Client~~~~0.0.1.0" 39 | } 40 | 41 | if (-Not (Get-InstalledModule PsHosts -ErrorAction SilentlyContinue)) { 42 | Install-Module PsHosts -Scope AllUsers -Force 43 | } 44 | 45 | if (Get-InstalledModule RemoteDesktopManager -ErrorAction SilentlyContinue) { 46 | Uninstall-Module RemoteDesktopManager -AllVersions 47 | } 48 | 49 | if (-Not (Get-InstalledModule Devolutions.PowerShell -ErrorAction SilentlyContinue)) { 50 | Install-Module Devolutions.PowerShell -Scope AllUsers -Force 51 | } 52 | 53 | # Enable WinRM client 54 | 55 | Set-Service 'WinRM' -StartupType 'Automatic' 56 | Start-Service 'WinRM' 57 | 58 | # Create Hyper-V directory structure 59 | 60 | $HyperVPath = if (Test-Path Env:DLAB_HOME) { $Env:DLAB_HOME } else { "C:\Hyper-V" } 61 | New-Item -ItemType Directory -Path $HyperVPath -ErrorAction SilentlyContinue | Out-Null 62 | 63 | @('ISOs','IMGs','VHDs') | ForEach-Object { 64 | New-Item -ItemType Directory -Path $(Join-Path $HyperVPath $_) -ErrorAction SilentlyContinue | Out-Null 65 | } 66 | 67 | # Download Windows Server 2025 ISO with the latest Windows updates and place it in C:\Hyper-V\ISOs 68 | # To avoid logging in to the Visual Studio subscriber download portal inside the VM, one trick 69 | # is to start the download from another computer and then grab the short-lived download URL. 70 | 71 | # The .iso file name needs to include "windows_server_2025", like this: 72 | # en-us_windows_server_2025_x64_dvd_b7ec10f3.iso 73 | 74 | # Download latest Alpine Linux "virtual" edition (https://www.alpinelinux.org/downloads/) 75 | 76 | $AlpineVersion = "3.16.6" 77 | $AlpineRelease = $AlpineVersion -Replace "^(\d+)\.(\d+)\.(\d+)$", "v`$1.`$2" 78 | $AlpineIsoFileName = "alpine-virt-${AlpineVersion}-x86_64.iso" 79 | $AlpineIsoDownloadUrl = "https://dl-cdn.alpinelinux.org/alpine/${AlpineRelease}/releases/x86_64/$AlpineIsoFileName" 80 | $AlpineIsoDownloadPath = Join-Path "$HyperVPath\ISOs" $AlpineIsoFileName 81 | 82 | if (-Not $(Test-Path -Path $AlpineIsoDownloadPath -PathType 'Leaf')) { 83 | Write-Host "Downloading $AlpineIsoDownloadUrl" 84 | curl.exe $AlpineIsoDownloadUrl -o $AlpineIsoDownloadPath 85 | } 86 | 87 | # Enable Hyper-V (requires a reboot) 88 | if ($(Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Hyper-V").State -ne 'Enabled') { 89 | Enable-WindowsOptionalFeature -Online -FeatureName @("Microsoft-Hyper-V") -All -NoRestart 90 | } 91 | 92 | # Create LAN switch for the host and VMs 93 | 94 | $SwitchName = "LAN Switch" 95 | $IPAddress = "10.10.0.1" 96 | if (-Not (Get-VMSwitch -Name $SwitchName -ErrorAction SilentlyContinue)) { 97 | New-VMSwitch -SwitchName $SwitchName -SwitchType Internal -Verbose 98 | } 99 | $NetAdapter = Get-NetAdapter | Where-Object { $_.Name -Like "*($SwitchName)" } 100 | if ($(Get-NetIpAddress -InterfaceIndex $NetAdapter.IfIndex).IPAddress -ne $IPAddress) { 101 | Remove-NetIPAddress -InterfaceIndex $NetAdapter.IfIndex -Confirm:$false 102 | New-NetIPAddress -InterfaceIndex $NetAdapter.IfIndex -IPAddress $IPAddress -PrefixLength 24 103 | Set-DnsClientServerAddress -InterfaceIndex $NetAdapter.IfIndex -ServerAddresses @() 104 | } 105 | 106 | # Create NAT switch for the router VM WAN 107 | 108 | $SwitchName = "NAT Switch" 109 | $IPAddress = "10.9.0.1" 110 | $NatName = "NatNetwork" 111 | $NatPrefix = "10.9.0.0/24" 112 | if (-Not (Get-VMSwitch -Name $SwitchName -ErrorAction SilentlyContinue)) { 113 | New-VMSwitch -SwitchName $SwitchName -SwitchType Internal -Verbose 114 | } 115 | $NetAdapter = Get-NetAdapter | Where-Object { $_.Name -Like "*($SwitchName)" } 116 | if ($(Get-NetIpAddress -InterfaceIndex $NetAdapter.IfIndex).IPAddress -ne $IPAddress) { 117 | Remove-NetIPAddress -InterfaceIndex $NetAdapter.IfIndex -Confirm:$false 118 | New-NetIPAddress -InterfaceIndex $NetAdapter.IfIndex -IPAddress $IPAddress -PrefixLength 24 119 | } 120 | if (-Not (Get-NetNat -Name $NatName -ErrorAction SilentlyContinue)) { 121 | New-NetNat -Name $NatName -InternalIPInterfaceAddressPrefix $NatPrefix 122 | } 123 | } 124 | 125 | Invoke-HostInit @args 126 | -------------------------------------------------------------------------------- /powershell/host_sync.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | #Requires -PSEdition Core 3 | 4 | . .\common.ps1 5 | 6 | $VMAlias = "DC" 7 | $VMName = $LabPrefix, $VMAlias -Join "-" 8 | 9 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 10 | 11 | # Synchronize DNS records with hosts file 12 | 13 | $IpFilter = $LabNetworkBase -Replace "^(\d+)\.(\d+)\.(\d+).(\d+)$", "`$1.`$2.`$3.`*" 14 | 15 | $HostEntries = Invoke-Command -ScriptBlock { Param($DnsZoneName, $IpFilter) 16 | Get-DnsServerResourceRecord -ZoneName $DnsZoneName -RRType 'A' | ` 17 | Where-Object { ($_.RecordData.IPv4Address -Like $IpFilter) ` 18 | -and ($_.HostName -ne '@') -and ($_.HostName -NotLike '*DnsZones') } | ` 19 | ForEach-Object { 20 | [PSCustomObject]@{ 21 | HostName = $_.HostName; 22 | Address = $_.RecordData.IPv4Address.ToString(); 23 | } 24 | } 25 | } -Session $VMSession -ArgumentList @($DnsZoneName, $IpFilter) 26 | 27 | function Set-HostEntrySafe 28 | { 29 | [CmdletBinding()] 30 | param( 31 | [Parameter(Mandatory=$true,Position=0)] 32 | [string] $Name, 33 | [Parameter(Mandatory=$true,Position=1)] 34 | [string] $Address, 35 | [switch] $Force 36 | ) 37 | 38 | $success = $false 39 | while (-Not $success) { 40 | try { 41 | Set-HostEntry -Name $Name -Address $Address -Force:$Force 42 | $success = $true 43 | } catch [System.IO.IOException] { 44 | # The process cannot access the file because it is being used by another process. 45 | Start-Sleep 1 46 | } catch { 47 | throw $_.Exception 48 | } 49 | } 50 | } 51 | 52 | $HostEntries | Where-Object { $_.HostName -Like "$LabPrefix-*" } | ForEach-Object { 53 | $MachineName = $_.HostName.ToUpper() 54 | $MachineFQDN = "$MachineName.$DnsZoneName" 55 | Set-HostEntrySafe -Name $MachineName -Address $_.Address -Force 56 | Set-HostEntrySafe -Name $MachineFQDN -Address $_.Address -Force 57 | } 58 | 59 | $HostEntries | Where-Object { $_.HostName -NotLike "$LabPrefix-*" } | ForEach-Object { 60 | $HostFQDN = "$($_.HostName).$DnsZoneName" 61 | Set-HostEntrySafe -Name $HostFQDN -Address $_.Address -Force 62 | } 63 | 64 | # Add DNS client rule for lab DNS suffix 65 | 66 | Get-DnsClientNrptRule | Where-Object { $_.Namespace -eq ".$DnsZoneName" } | Remove-DnsClientNrptRule -Force 67 | Add-DnsClientNrptRule -Namespace ".$DnsZoneName" -NameServers @($DnsServerAddress) 68 | 69 | # Synchronize trusted root CAs 70 | 71 | $VMAlias = "DC" 72 | $VMName = $LabPrefix, $VMAlias -Join "-" 73 | 74 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 75 | 76 | $CertSubject = (@("CN=$CAMachineName") + @($DnsZoneName -Split '\.' | ForEach-Object { "DC=$_" })) -Join ", " 77 | 78 | [byte[]] $CACert = Invoke-Command -ScriptBlock { Param($CertSubject) 79 | $MyCert = Get-ChildItem "cert:\LocalMachine\My" | Where-Object { $_.Subject -Like $CertSubject } 80 | $RootCert = Get-ChildItem "cert:\LocalMachine\Root" | Where-Object { 81 | $_.Subject -Like $CertSubject -and $_.Thumbprint -eq $MyCert.Thumbprint } | Select-Object -First 1 82 | $RootCert.GetRawCertData() 83 | } -Session $VMSession -ArgumentList @($CertSubject) 84 | 85 | $CACertPath = "~\ca-cert.cer" 86 | $AsByteStream = if ($PSEdition -eq 'Core') { @{AsByteStream = $true} } else { @{'Encoding' = 'Byte'} } 87 | Set-Content -Value $CACert -Path $CACertPath @AsByteStream -Force 88 | 89 | $CertificateFile = Resolve-Path $CACertPath 90 | $Certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertificateFile) 91 | 92 | if (-Not (Get-ChildItem "Cert:\LocalMachine\Root" | 93 | Where-Object { $_.Thumbprint -eq $Certificate.Thumbprint })) { 94 | Import-Certificate -FilePath $CACertPath -CertStoreLocation "Cert:\LocalMachine\Root" 95 | } 96 | 97 | # Flush CRL cache 98 | 99 | & certutil.exe "-urlcache" "crl" "delete" 100 | & certutil.exe "-setreg" "chain\ChainCacheResyncFiletime" "@now" 101 | 102 | # Synchronize WinRM client trusted hosts 103 | 104 | $LabTrustedHost = "*.$DnsZoneName" 105 | $TrustedHostsValue = $(Get-Item "WSMan:localhost\Client\TrustedHosts").Value 106 | if ($TrustedHostsValue -ne '*') { 107 | if ([string]::IsNullOrEmpty($TrustedHostsValue)) { 108 | $TrustedHostsValue = $LabTrustedHost 109 | } else { 110 | $TrustedHosts = $TrustedHostsValue -Split ',' | ForEach-Object { $_.Trim() } 111 | if (-Not $TrustedHosts.Contains($LabTrustedHost)) { 112 | $TrustedHosts += @($LabTrustedHost) 113 | } 114 | $TrustedHostsValue = $TrustedHosts -Join ',' 115 | } 116 | Set-Item "WSMan:localhost\Client\TrustedHosts" -Value $TrustedHostsValue -Force 117 | } 118 | -------------------------------------------------------------------------------- /powershell/host_vm.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | #Requires -PSEdition Core 3 | 4 | param( 5 | [string] $VMAlias = "HOST", 6 | [int] $VMNumber = 50 7 | ) 8 | 9 | . .\common.ps1 10 | 11 | $VMName = $LabPrefix, $VMAlias -Join "-" 12 | $IpAddress = Get-DLabIpAddress $LabNetworkBase $VMNumber 13 | 14 | New-DLabVM $VMName -Password $LocalPassword -OSVersion $OSVersion ` 15 | -MemoryBytes 16GB ` 16 | -DynamicMemory $false ` 17 | -EnableVirtualization $true -Force 18 | 19 | Start-DLabVM $VMName 20 | 21 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $LocalUserName -Password $LocalPassword 22 | $VMSession = New-DLabVMSession $VMName -UserName $LocalUserName -Password $LocalPassword 23 | 24 | Set-DLabVMNetAdapter $VMName -VMSession $VMSession ` 25 | -SwitchName $SwitchName -NetAdapterName $NetAdapterName ` 26 | -IPAddress $IPAddress -DefaultGateway $DefaultGateway ` 27 | -DnsServerAddress $DnsServerAddress 28 | 29 | Write-DLabLog "Joining domain" 30 | 31 | Add-DLabVMToDomain $VMName -VMSession $VMSession ` 32 | -DomainName $DomainName -DomainController $DCHostName ` 33 | -UserName $DomainUserName -Password $DomainPassword 34 | 35 | Write-DLabLog "Waiting for VM to reboot after domain join" 36 | 37 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 38 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 39 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 40 | 41 | Write-DLabLog "Installing Hyper-V with management tools" 42 | 43 | Invoke-Command -ScriptBlock { 44 | Install-WindowsFeature -Name Hyper-V -IncludeManagementTools -Restart 45 | } -Session $VMSession 46 | 47 | Write-DLabLog "Waiting for VM to reboot after enabling Hyper-V" 48 | 49 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 50 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 51 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 52 | 53 | Write-DLabLog "Adding current user to the local Hyper-V Administrators group" 54 | 55 | Invoke-Command -ScriptBlock { 56 | $CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name 57 | if (-Not (Get-LocalGroupMember -Group "Hyper-V Administrators" -Member $CurrentUser -ErrorAction SilentlyContinue)) { 58 | Add-LocalGroupMember -Group "Hyper-V Administrators" -Member @($CurrentUser) 59 | } 60 | } -Session $VMSession 61 | -------------------------------------------------------------------------------- /powershell/licensing.json: -------------------------------------------------------------------------------- 1 | { 2 | "RDM": "", 3 | "DVLS": "", 4 | "PAM": "", 5 | "DGW": "" 6 | } -------------------------------------------------------------------------------- /powershell/rdm_init.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Position=0)] 3 | [string] $DataSourceName 4 | ) 5 | 6 | . .\common.ps1 7 | 8 | Import-Module Devolutions.PowerShell -Force 9 | 10 | $Refresh = $true 11 | $ErrorActionPreference = "Stop" 12 | 13 | $LabName = "$LabPrefix-LAB" 14 | $VMAliases = @("DC", "DVLS", "GW", "RDM") 15 | 16 | if ([string]::IsNullOrEmpty($DataSourceName)) { 17 | $DataSourceName = $LabName 18 | } 19 | 20 | if (-Not (Get-RDMDataSource | Select-Object -ExpandProperty Name).Contains($DataSourceName)) { 21 | $DBFileName = ($DataSourceName -Replace ' ', '') + ".db" 22 | $DBFilePath = "$Env:LocalAppData\Devolutions\RemoteDesktopManager\$DBFileName" 23 | Remove-Item -Path $DBFilePath -ErrorAction SilentlyContinue | Out-Null 24 | $Params = @{ 25 | Name = $DataSourceName; 26 | SQLite = $true; 27 | Database = $DBFilePath; 28 | } 29 | $DataSource = New-RDMDataSource @Params 30 | Set-RDMDataSource -DataSource $DataSource 31 | } 32 | 33 | $LabDataSource = Get-RDMDataSource -Name $DataSourceName 34 | Set-RDMCurrentDataSource -DataSource $LabDataSource 35 | 36 | function Test-RDMGroup 37 | { 38 | [CmdletBinding()] 39 | param( 40 | [Parameter(Mandatory=$true,Position=0)] 41 | [string] $Name 42 | ) 43 | 44 | [bool] $(Get-RDMSession -GroupName $Name -ErrorAction SilentlyContinue) 45 | } 46 | 47 | # Lab Folder 48 | $LabFolderName = $LabCompanyName 49 | $LabGroupName = $LabFolderName 50 | if (-Not (Test-RDMGroup $LabGroupName)) { 51 | $LabFolder = New-RDMSession -Type "Group" -Name $LabFolderName 52 | $LabFolder.Group = $LabGroupName 53 | Set-RDMSession -Session $LabFolder -Refresh:$Refresh 54 | } 55 | 56 | # Active Directory Folder 57 | $ADFolderName = "Active Directory" 58 | $ADGroupName = "$LabFolderName\$ADFolderName" 59 | if (-Not (Test-RDMGroup $ADGroupName)) { 60 | $ADFolder = New-RDMSession -Type "Group" -Name $ADFolderName 61 | $ADFolder.Group = $ADGroupName 62 | Set-RDMSession -Session $ADFolder -Refresh:$Refresh 63 | } 64 | 65 | # Local Network Folder 66 | $LANFolderName = "Local Network" 67 | $LANGroupName = "$LabFolderName\$LANFolderName" 68 | if (-Not (Test-RDMGroup $LANGroupName)) { 69 | $LANFolder = New-RDMSession -Type "Group" -Name $LANFolderName 70 | $LANFolder.Group = $LANGroupName 71 | Set-RDMSession -Session $LANFolder -Refresh:$Refresh 72 | } 73 | 74 | # RD Gateway Folder 75 | $RDGFolderName = "RD Gateway" 76 | $RDGGroupName = "$LabFolderName\$RDGFolderName" 77 | if (-Not (Test-RDMGroup $RDGGroupName)) { 78 | $RDGFolder = New-RDMSession -Type "Group" -Name $RDGFolderName 79 | $RDGFolder.Group = $RDGGroupName 80 | Set-RDMSession -Session $RDGFolder -Refresh:$Refresh 81 | } 82 | 83 | # Devolutions Gateway Folder 84 | $DGWFolderName = "Devolutions Gateway" 85 | $DGWGroupName = "$LabFolderName\$DGWFolderName" 86 | if (-Not (Test-RDMGroup $DGWGroupName)) { 87 | $DGWFolder = New-RDMSession -Type "Group" -Name $DGWFolderName 88 | $DGWFolder.Group = $DGWGroupName 89 | Set-RDMSession -Session $DGWFolder -Refresh:$Refresh 90 | } 91 | 92 | # Hyper-V Folder 93 | $HVFolderName = "Hyper-V Host" 94 | $HVGroupName = "$LabFolderName\$HVFolderName" 95 | if (-Not (Test-RDMGroup $HVGroupName)) { 96 | $HVFolder = New-RDMSession -Type "Group" -Name $HVFolderName 97 | $HVFolder.Group = $HVGroupName 98 | Set-RDMSession -Session $HVFolder -Refresh:$Refresh 99 | } 100 | 101 | # WAC Folder 102 | $WACFolderName = "Windows Admin Center" 103 | $WACGroupName = "$LabFolderName\$WACFolderName" 104 | if (-Not (Test-RDMGroup $WACGroupName)) { 105 | $WACFolder = New-RDMSession -Type "Group" -Name $WACFolderName 106 | $WACFolder.Group = $WACGroupName 107 | Set-RDMSession -Session $WACFolder -Refresh:$Refresh 108 | } 109 | 110 | # Domain Administrator 111 | 112 | $DomainAdminUPN = "Administrator@$DomainDnsName" 113 | 114 | $Params = @{ 115 | Name = $DomainAdminUPN 116 | Type = "Credential"; 117 | } 118 | 119 | $Session = New-RDMSession @Params 120 | $Session.Group = $ADGroupName 121 | $Session.MetaInformation.UPN = $DomainAdminUPN 122 | $Session | Set-RDMSessionUsername -UserName "Administrator" 123 | $Session | Set-RDMSessionDomain -Domain $DomainDnsName 124 | $Session | Set-RDMSessionPassword -Password (ConvertTo-SecureString $DomainPassword -AsPlainText -Force) 125 | Set-RDMSession -Session $Session -Refresh:$Refresh 126 | 127 | $DomainAdminEntry = Get-RDMSession -GroupName $ADGroupName -Name $DomainAdminUPN 128 | $DomainAdminId = $DomainAdminEntry.ID 129 | 130 | # Protected User 131 | 132 | $ProtectedUserUPN = "ProtectedUser@$DomainDnsName" 133 | 134 | $Params = @{ 135 | Name = $ProtectedUserUPN 136 | Type = "Credential"; 137 | } 138 | 139 | $Session = New-RDMSession @Params 140 | $Session.Group = $ADGroupName 141 | $Session.MetaInformation.UPN = $ProtectedUserUPN 142 | $Session | Set-RDMSessionUsername -UserName $ProtectedUserName 143 | $Session | Set-RDMSessionDomain -Domain $DomainDnsName 144 | $Session | Set-RDMSessionPassword -Password (ConvertTo-SecureString $ProtectedUserPassword -AsPlainText -Force) 145 | Set-RDMSession -Session $Session -Refresh:$Refresh 146 | 147 | # RD Gateway 148 | 149 | $RDGatewayFQDN = "rdg.$DnsZoneName" 150 | 151 | $Params = @{ 152 | Name = $RDGatewayFQDN 153 | Host = $RDGatewayFQDN 154 | Type = "Gateway"; 155 | } 156 | 157 | $Session = New-RDMSession @Params 158 | $Session.Group = $RDGGroupName 159 | $Session.CredentialConnectionID = $DomainAdminId 160 | $Session.RDP.GatewayCredentialsSource = "UserPassword" 161 | $Session.RDP.GatewayProfileUsageMethod = "Explicit" 162 | $Session.RDP.GatewaySelection = "SpecificGateway" 163 | $Session.RDP.GatewayUsageMethod = "ModeDirect" 164 | Set-RDMSession -Session $Session -Refresh:$Refresh 165 | 166 | $RDGatewayEntry = Get-RDMSession -GroupName $RDGGroupName -Name $RDGatewayFQDN | Select-Object -First 1 167 | 168 | # Windows Admin Center 169 | 170 | $WacFQDN = "wac.$DnsZoneName" 171 | $WacURL = "https://$WacFQDN`:6516" 172 | 173 | $Params = @{ 174 | Name = $WacFQDN 175 | Host = $WacURL 176 | Type = "WebBrowser"; 177 | } 178 | 179 | $Session = New-RDMSession @Params 180 | $Session.Group = $WACGroupName 181 | $Session.WebBrowserURL = $WacURL 182 | $Session.OpenEmbedded = $false 183 | $Session.Web.AutoFillLogin = $false 184 | $Session.Web.AutoSubmit = $false 185 | Set-RDMSession -Session $Session -Refresh:$Refresh 186 | 187 | # RDP (Regular) 188 | 189 | $VMAliases | ForEach-Object { 190 | $VMAlias = $_ 191 | $VMName = $LabPrefix, $VMAlias -Join "-" 192 | 193 | $MachineName = $VMName 194 | $MachineFQDN = "$MachineName.$DnsZoneName" 195 | 196 | $Params = @{ 197 | Name = "$MachineName"; 198 | Host = $MachineFQDN; 199 | Type = "RDPConfigured"; 200 | } 201 | 202 | $Session = New-RDMSession @Params 203 | $Session.Group = "$LabFolderName\$LANFolderName" 204 | $Session.CredentialConnectionID = $DomainAdminId 205 | Set-RDMSession -Session $Session -Refresh:$Refresh 206 | } 207 | 208 | # RDP (Hyper-V) 209 | 210 | $VMAliases | ForEach-Object { 211 | $VMAlias = $_ 212 | $VMName = $LabPrefix, $VMAlias -Join "-" 213 | 214 | $VMId = $(Get-VM $VMName).Id 215 | $VMHost = "localhost" 216 | 217 | $Params = @{ 218 | Name = "$VMName"; 219 | Host = $VMHost; 220 | Type = "RDPConfigured"; 221 | } 222 | 223 | $Session = New-RDMSession @Params 224 | $Session.Group = "$LabFolderName\$HVFolderName" 225 | $Session.RDP.RDPType = "HyperV" 226 | $Session.RDP.HyperVInstanceID = $VMId 227 | $Session.RDP.FrameBufferRedirection = $false 228 | $Session.RDP.UseEnhancedSessionMode = $true 229 | $Session.RDP.VMConnectImplicitCredentials = $true 230 | Set-RDMSession -Session $Session -Refresh:$Refresh 231 | } 232 | 233 | # RDP (RD Gateway) 234 | 235 | $VMAliases | ForEach-Object { 236 | $VMAlias = $_ 237 | $VMName = $LabPrefix, $VMAlias -Join "-" 238 | 239 | $MachineName = $VMName 240 | $MachineFQDN = "$MachineName.$DnsZoneName" 241 | 242 | $Params = @{ 243 | Name = "$MachineName"; 244 | Host = $MachineFQDN; 245 | Type = "RDPConfigured"; 246 | } 247 | 248 | $Session = New-RDMSession @Params 249 | $Session.Group = $RDGGroupName 250 | $Session.CredentialConnectionID = $DomainAdminId 251 | $Session.VPN.Application = "Gateway" 252 | $Session.VPN.Enabled = $true 253 | $Session.VPN.Mode = "AlwaysConnect" 254 | $Session.VPN.ExistingGatewayID = $RDGatewayEntry.ID 255 | Set-RDMSession -Session $Session -Refresh:$Refresh 256 | } 257 | 258 | # RDP (Devolutions Gateway) 259 | 260 | $VMAliases | ForEach-Object { 261 | $VMAlias = $_ 262 | $VMName = $LabPrefix, $VMAlias -Join "-" 263 | 264 | $MachineName = $VMName 265 | $MachineFQDN = "$MachineName.$DnsZoneName" 266 | 267 | $Params = @{ 268 | Name = "$MachineName"; 269 | Host = $MachineFQDN; 270 | Type = "RDPConfigured"; 271 | } 272 | 273 | $Session = New-RDMSession @Params 274 | $Session.Group = $DGWGroupName 275 | $Session.CredentialConnectionID = $DomainAdminId 276 | $Session.VPN.Application = "Inherited" 277 | $Session.VPN.Enabled = $true 278 | $Session.VPN.Mode = "AlwaysConnect" 279 | Set-RDMSession -Session $Session -Refresh:$Refresh 280 | } 281 | 282 | # PowerShell (Hyper-V) 283 | 284 | $VMAliases | ForEach-Object { 285 | $VMAlias = $_ 286 | $VMName = $LabPrefix, $VMAlias -Join "-" 287 | 288 | $Params = @{ 289 | Name = "$VMName"; 290 | Host = $VMName; 291 | Type = "PowerShellRemoteConsole"; 292 | } 293 | 294 | $Session = New-RDMSession @Params 295 | $Session.Group = $HVGroupName 296 | $Session.CredentialConnectionID = $DomainAdminId 297 | $Session.PowerShell.Host = $Params.Host 298 | $Session.PowerShell.RemoteConsoleConnectionMode = "VMName" 299 | Set-RDMSession -Session $Session -Refresh:$Refresh 300 | } 301 | 302 | # PowerShell (WinRM) 303 | 304 | $VMAliases | ForEach-Object { 305 | $VMAlias = $_ 306 | $VMName = $LabPrefix, $VMAlias -Join "-" 307 | 308 | $MachineName = $VMName 309 | $MachineFQDN = "$MachineName.$DnsZoneName" 310 | 311 | $Params = @{ 312 | Name = "$MachineName"; 313 | Host = $MachineFQDN; 314 | Type = "PowerShellRemoteConsole"; 315 | } 316 | 317 | $Session = New-RDMSession @Params 318 | $Session.Group = $LANGroupName 319 | $Session.CredentialConnectionID = $DomainAdminId 320 | $Session.PowerShell.Host = $Params.Host 321 | $Session.PowerShell.RemoteConsoleConnectionMode = "ComputerName" 322 | Set-RDMSession -Session $Session -Refresh:$Refresh 323 | } 324 | 325 | # SSH (Direct) 326 | 327 | $VMAliases | ForEach-Object { 328 | $VMAlias = $_ 329 | $VMName = $LabPrefix, $VMAlias -Join "-" 330 | 331 | $MachineName = $VMName 332 | $MachineFQDN = "$MachineName.$DnsZoneName" 333 | 334 | $Params = @{ 335 | Name = "$MachineName"; 336 | Host = $MachineFQDN; 337 | Type = "SSHShell"; 338 | } 339 | 340 | $Session = New-RDMSession @Params 341 | $Session.Group = $LANGroupName 342 | $Session.CredentialConnectionID = $DomainAdminId 343 | $Session.Terminal.Host = $MachineFQDN 344 | $Session.Terminal.HostPort = 22 345 | Set-RDMSession -Session $Session -Refresh:$Refresh 346 | } 347 | 348 | # SSH (Devolutions Gateway) 349 | 350 | $VMAliases | ForEach-Object { 351 | $VMAlias = $_ 352 | $VMName = $LabPrefix, $VMAlias -Join "-" 353 | 354 | $MachineName = $VMName 355 | $MachineFQDN = "$MachineName.$DnsZoneName" 356 | 357 | $Params = @{ 358 | Name = "$MachineName"; 359 | Host = $MachineFQDN; 360 | Type = "SSHShell"; 361 | } 362 | 363 | $Session = New-RDMSession @Params 364 | $Session.Group = $DGWGroupName 365 | $Session.CredentialConnectionID = $DomainAdminId 366 | $Session.Terminal.Host = $MachineFQDN 367 | $Session.Terminal.HostPort = 22 368 | $Session.VPN.Application = "Inherited" 369 | $Session.VPN.Enabled = $true 370 | $Session.VPN.Mode = "AlwaysConnect" 371 | Set-RDMSession -Session $Session -Refresh:$Refresh 372 | } 373 | 374 | # VNC (Direct) 375 | 376 | $VMAliases | ForEach-Object { 377 | $VMAlias = $_ 378 | $VMName = $LabPrefix, $VMAlias -Join "-" 379 | 380 | $MachineName = $VMName 381 | $MachineFQDN = "$MachineName.$DnsZoneName" 382 | 383 | $Params = @{ 384 | Name = "$MachineName"; 385 | Host = $MachineFQDN; 386 | Type = "VNC"; 387 | } 388 | 389 | $Session = New-RDMSession @Params 390 | $Session.Group = $LANGroupName 391 | $Session.CredentialConnectionID = $DomainAdminId 392 | $Session.VNC.Host = $MachineFQDN 393 | $Session.VNC.VNCEmbeddedType = "FreeVNC" 394 | Set-RDMSession -Session $Session -Refresh:$Refresh 395 | } 396 | 397 | # VNC (Devolutions Gateway) 398 | 399 | $VMAliases | ForEach-Object { 400 | $VMAlias = $_ 401 | $VMName = $LabPrefix, $VMAlias -Join "-" 402 | 403 | $MachineName = $VMName 404 | $MachineFQDN = "$MachineName.$DnsZoneName" 405 | 406 | $Params = @{ 407 | Name = "$MachineName"; 408 | Host = $MachineFQDN; 409 | Type = "VNC"; 410 | } 411 | 412 | $Session = New-RDMSession @Params 413 | $Session.Group = $DGWGroupName 414 | $Session.CredentialConnectionID = $DomainAdminId 415 | $Session.VNC.Host = $MachineFQDN 416 | $Session.VNC.VNCEmbeddedType = "FreeVNC" 417 | $Session.VPN.Application = "Inherited" 418 | $Session.VPN.Enabled = $true 419 | $Session.VPN.Mode = "AlwaysConnect" 420 | Set-RDMSession -Session $Session -Refresh:$Refresh 421 | } 422 | 423 | # Active Directory Accounts 424 | 425 | if (Test-Path -Path "ADAccounts.json" -PathType 'Leaf') { 426 | $ADAccounts = $(Get-Content -Path "ADAccounts.json") | ConvertFrom-Json 427 | 428 | $ADAccounts | ForEach-Object { 429 | $Username = $_.Identity 430 | $Password = $_.Password 431 | $AccountUPN = "$Username@$DomainDnsName" 432 | 433 | $Params = @{ 434 | Name = $AccountUPN; 435 | Type = "Credential"; 436 | } 437 | 438 | $Session = New-RDMSession @Params 439 | $Session.Group = $ADGroupName 440 | $Session.MetaInformation.UPN = $AccountUPN 441 | $Session | Set-RDMSessionUsername -UserName $Username 442 | $Session | Set-RDMSessionDomain -Domain $DomainDnsName 443 | $Session | Set-RDMSessionPassword -Password (ConvertTo-SecureString $Password -AsPlainText -Force) 444 | Set-RDMSession -Session $Session -Refresh:$Refresh 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /powershell/rdm_vm.ps1: -------------------------------------------------------------------------------- 1 | . .\common.ps1 2 | 3 | $VMAlias = "RDM" 4 | $VMNumber = 9 5 | $VMName = $LabPrefix, $VMAlias -Join "-" 6 | $IpAddress = Get-DLabIpAddress $LabNetworkBase $VMNumber 7 | 8 | New-DLabVM $VMName -Password $LocalPassword -OSVersion $OSVersion -Force 9 | Start-DLabVM $VMName 10 | 11 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $LocalUserName -Password $LocalPassword 12 | $VMSession = New-DLabVMSession $VMName -UserName $LocalUserName -Password $LocalPassword 13 | 14 | Set-DLabVMNetAdapter $VMName -VMSession $VMSession ` 15 | -SwitchName $SwitchName -NetAdapterName $NetAdapterName ` 16 | -IPAddress $IPAddress -DefaultGateway $DefaultGateway ` 17 | -DnsServerAddress $DnsServerAddress 18 | 19 | Write-Host "Joining domain" 20 | 21 | Add-DLabVMToDomain $VMName -VMSession $VMSession ` 22 | -DomainName $DomainName -DomainController $DCHostName ` 23 | -UserName $DomainUserName -Password $DomainPassword 24 | 25 | # Wait for virtual machine to reboot after domain join operation 26 | 27 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 28 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 29 | 30 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 31 | 32 | Write-Host "Requesting RDP server certificate" 33 | 34 | Request-DLabRdpCertificate $VMName -VMSession $VMSession ` 35 | -CAHostName $CAHostName -CACommonName $CACommonName 36 | 37 | Write-Host "Initializing PSRemoting" 38 | 39 | Initialize-DLabPSRemoting $VMName -VMSession $VMSession 40 | 41 | Write-Host "Initializing VNC server" 42 | 43 | Initialize-DLabVncServer $VMName -VMSession $VMSession 44 | 45 | Write-Host "Installing Devolutions PowerShell module" 46 | 47 | Invoke-Command -ScriptBlock { 48 | Install-Module -Name Devolutions.PowerShell -Scope AllUsers -Force 49 | } -Session $VMSession 50 | 51 | Write-Host "Installing .NET Desktop Runtime" 52 | 53 | Invoke-Command -ScriptBlock { 54 | $MajorVersion = "9.0" 55 | $RuntimeType = "windowsdesktop" 56 | $Architecture = if ($Env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { "win-arm64" } else { "win-x64" } 57 | $ReleasesJsonUrl = "https://builds.dotnet.microsoft.com/dotnet/release-metadata/$MajorVersion/releases.json" 58 | $ReleasesData = Invoke-RestMethod -Uri $ReleasesJsonUrl 59 | 60 | $LatestReleaseWithDesktop = $ReleasesData.releases | 61 | Where-Object { $_.windowsdesktop -and $_.windowsdesktop.files } | 62 | Sort-Object -Property 'release-date' -Descending | Select-Object -First 1 63 | 64 | if (-not $LatestReleaseWithDesktop) { 65 | throw "Could not find any releases with $RuntimeType runtime." 66 | } 67 | 68 | $DesktopRuntimeVersion = $LatestReleaseWithDesktop.windowsdesktop.version 69 | $DesktopRuntimeFiles = $LatestReleaseWithDesktop.windowsdesktop.files 70 | $Installer = $DesktopRuntimeFiles | Where-Object { 71 | $_.rid -eq $Architecture -and $_.name -like "$RuntimeType-runtime-*-*.exe" 72 | } | Select-Object -First 1 73 | 74 | if (-not $Installer) { 75 | throw "Could not find $RuntimeType runtime installer for $Architecture" 76 | } 77 | 78 | $DownloadUrl = $Installer.url 79 | $ExpectedFileHash = $Installer.hash 80 | $InstallerFileName = Split-Path -Leaf $DownloadUrl 81 | $InstallerLocalPath = Join-Path $Env:TEMP $InstallerFileName 82 | $ProgressPreference = 'SilentlyContinue' 83 | Invoke-WebRequest $DownloadUrl -OutFile $InstallerLocalPath 84 | $ActualFileHash = (Get-FileHash -Algorithm SHA512 $InstallerLocalPath).Hash 85 | if ($ExpectedFileHash -ine $ActualFileHash) { throw "Unexpected SHA512 file hash for $InstallerFileName`: $ActualFileHash" } 86 | Start-Process -FilePath $InstallerLocalPath -ArgumentList @('/install', '/quiet', '/norestart', 'OPT_NO_X86=1') -Wait -NoNewWindow 87 | Remove-Item $InstallerLocalPath -Force | Out-Null 88 | 89 | Write-Host ".NET $RuntimeType runtime $DesktopRuntimeVersion installed successfully." 90 | } -Session $VMSession 91 | 92 | Write-Host "Installing Devolutions Remote Desktop Manager" 93 | 94 | Invoke-Command -ScriptBlock { 95 | $ProductsHtm = Invoke-RestMethod -Uri "https://devolutions.net/productinfo.htm" -Method 'GET' -ContentType 'text/plain' 96 | $RdmMatches = $($ProductsHtm | Select-String -AllMatches -Pattern "(RDM\S+).Url=(\S+)").Matches 97 | $RdmKeyName = if ($Env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { "RDMmsiArm" } else { "RDMmsiX64" } 98 | $RdmWindows = $RdmMatches | Where-Object { $_.Groups[1].Value -eq $RdmKeyName } 99 | $RdmDownloadUrl = $RdmWindows.Groups[2].Value 100 | $RdmFileName = [System.IO.Path]::GetFileName($RdmDownloadUrl) 101 | $TempMsiPath = Join-Path $env:TEMP $RdmFileName 102 | $ProgressPreference = 'SilentlyContinue' 103 | Invoke-WebRequest -Uri $RdmDownloadUrl -OutFile $TempMsiPath 104 | Start-Process -FilePath "msiexec.exe" -ArgumentList "/i `"$TempMsiPath`" /quiet /norestart" -Wait -NoNewWindow 105 | Remove-Item -Path $TempMsiPath -Force | Out-Null 106 | } -Session $VMSession 107 | 108 | Write-Host "Changing Windows taskbar default pinned apps" 109 | 110 | Invoke-Command -ScriptBlock { 111 | $LnkPaths = @( 112 | "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk" 113 | "%APPDATA%\Microsoft\Windows\Start Menu\Programs\File Explorer.lnk" 114 | "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Remote Desktop Manager\Remote Desktop Manager (RDM).lnk" 115 | "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Windows Terminal.lnk" 116 | "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\mstscex.lnk" 117 | ) 118 | $OutputPath = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\LayoutModification.xml" 119 | $xml = New-Object System.Xml.XmlDocument 120 | $root = $xml.CreateElement("LayoutModificationTemplate", "http://schemas.microsoft.com/Start/2014/LayoutModification") 121 | $xml.AppendChild($root) | Out-Null 122 | $root.SetAttribute("xmlns:defaultlayout", "http://schemas.microsoft.com/Start/2014/FullDefaultLayout") 123 | $root.SetAttribute("xmlns:taskbar", "http://schemas.microsoft.com/Start/2014/TaskbarLayout") 124 | $root.SetAttribute("Version", "1") 125 | $collection = $xml.CreateElement("CustomTaskbarLayoutCollection", $root.NamespaceURI) 126 | $collection.SetAttribute("PinListPlacement", "Replace") 127 | $root.AppendChild($collection) | Out-Null 128 | $layout = $xml.CreateElement("defaultlayout:TaskbarLayout", $root.GetAttribute("xmlns:defaultlayout")) 129 | $collection.AppendChild($layout) | Out-Null 130 | $pinList = $xml.CreateElement("taskbar:TaskbarPinList", $root.GetAttribute("xmlns:taskbar")) 131 | $layout.AppendChild($pinList) | Out-Null 132 | foreach ($lnk in $LnkPaths) { 133 | $desktopApp = $xml.CreateElement("taskbar:DesktopApp", $root.GetAttribute("xmlns:taskbar")) 134 | $desktopApp.SetAttribute("DesktopApplicationLinkPath", $lnk) 135 | $pinList.AppendChild($desktopApp) | Out-Null 136 | } 137 | $xml.Save($OutputPath) 138 | Remove-Item "$Env:AppData\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\*" -Force -ErrorAction SilentlyContinue 139 | Remove-Item "$Env:AppData\Microsoft\Windows\Shell\*.dat" -Force -ErrorAction SilentlyContinue 140 | Remove-Item "$Env:AppData\Microsoft\Windows\Shell\LayoutModification.xml" -Force -ErrorAction SilentlyContinue 141 | Remove-Item -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Taskband" -Recurse -ErrorAction SilentlyContinue 142 | Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\TrayNotify" -Name "IconStreams" -ErrorAction SilentlyContinue 143 | Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\TrayNotify" -Name "PastIconsStream" -ErrorAction SilentlyContinue 144 | } -Session $VMSession 145 | -------------------------------------------------------------------------------- /powershell/rds_farm.ps1: -------------------------------------------------------------------------------- 1 | . .\common.ps1 2 | 3 | $FarmSize = 3 4 | 5 | foreach ($FarmIndex in 1..$FarmSize) { 6 | $VMAlias = "RDSH" + $FarmIndex 7 | $VMNumber = 10 + $FarmIndex 8 | $VMName = $LabPrefix, $VMAlias -Join "-" 9 | $IpAddress = Get-DLabIpAddress $LabNetworkBase $VMNumber 10 | 11 | New-DLabVM $VMName -Password $LocalPassword -OSVersion $OSVersion -Force 12 | Start-DLabVM $VMName 13 | 14 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $LocalUserName -Password $LocalPassword 15 | $VMSession = New-DLabVMSession $VMName -UserName $LocalUserName -Password $LocalPassword 16 | 17 | Set-DLabVMNetAdapter $VMName -VMSession $VMSession ` 18 | -SwitchName $SwitchName -NetAdapterName $NetAdapterName ` 19 | -IPAddress $IPAddress -DefaultGateway $DefaultGateway ` 20 | -DnsServerAddress $DnsServerAddress 21 | 22 | Write-Host "Joining domain" 23 | 24 | Add-DLabVMToDomain $VMName -VMSession $VMSession ` 25 | -DomainName $DomainName -DomainController $DCHostName ` 26 | -UserName $DomainUserName -Password $DomainPassword 27 | 28 | # Wait for virtual machine to reboot after domain join operation 29 | 30 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 31 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 32 | 33 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 34 | 35 | Write-Host "Requesting RDP server certificate" 36 | 37 | Request-DLabRdpCertificate $VMName -VMSession $VMSession ` 38 | -CAHostName $CAHostName -CACommonName $CACommonName 39 | 40 | Write-Host "Initializing PSRemoting" 41 | 42 | Initialize-DLabPSRemoting $VMName -VMSession $VMSession 43 | 44 | Write-Host "Initializing VNC server" 45 | 46 | Initialize-DLabVncServer $VMName -VMSession $VMSession 47 | 48 | Write-Host "Installing RD Session Host" 49 | 50 | Invoke-Command -ScriptBlock { 51 | Install-WindowsFeature -Name RDS-RD-Server 52 | Restart-Computer -Force 53 | } -Session $VMSession 54 | 55 | Write-Host "Rebooting VM" 56 | 57 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 58 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 59 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 60 | } 61 | 62 | # IT-HELP-GW 63 | 64 | $VMAlias = "GW" 65 | $VMNumber = 7 66 | $VMName = $LabPrefix, $VMAlias -Join "-" 67 | $IpAddress = Get-DLabIpAddress $LabNetworkBase $VMNumber 68 | 69 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 70 | 71 | $SessionHosts = @() 72 | foreach ($FarmIndex in 1..$FarmSize) { 73 | $SessionHost = $LabPrefix + "-RDSH" + $FarmIndex + "." + $DomainName 74 | $SessionHosts += $SessionHost 75 | } 76 | 77 | Write-Host "Create new RD session collection" 78 | 79 | $ConnectionBroker = "$VMName.$DomainName" 80 | 81 | Invoke-Command -ScriptBlock { Param($ConnectionBroker, $SessionHosts) 82 | $SessionHosts | ForEach-Object { 83 | Add-RDServer -Server $_ -Role RDS-RD-SERVER 84 | } 85 | 86 | $CollectionName = "IT-HELP-FARM" 87 | $CollectionDescription = "IT Help RDS Farm" 88 | 89 | $Params = @{ 90 | CollectionName = "IT-HELP-FARM"; 91 | CollectionDescription = "IT Help RDS Farm"; 92 | SessionHost = $SessionHosts; 93 | ConnectionBroker = $ConnectionBroker; 94 | } 95 | New-RDSessionCollection @Params 96 | } -Session $VMSession -ArgumentList @($ConnectionBroker, $SessionHosts) 97 | -------------------------------------------------------------------------------- /powershell/rtr_vm.ps1: -------------------------------------------------------------------------------- 1 | . .\common.ps1 2 | 3 | $VMAlias = "RTR" 4 | $VMName = $LabPrefix, $VMAlias -Join "-" 5 | 6 | $NatHostIpAddress = "10.9.0.1" 7 | $NatGuestIpAddress = "10.9.0.2" 8 | 9 | $NetworkInterfaces = @" 10 | auto lo 11 | iface lo inet loopback 12 | 13 | auto eth0 14 | iface eth0 inet static 15 | address $NatGuestIpAddress 16 | netmask 255.255.255.0 17 | gateway $NatHostIpAddress 18 | 19 | auto eth1 20 | iface eth1 inet static 21 | address $DefaultGateway 22 | netmask 255.255.255.0 23 | "@ 24 | 25 | $DnsMasqConf = @" 26 | interface=eth1 27 | dhcp-range=$DhcpRangeStart,$DhcpRangeEnd,255.255.255.0,12h 28 | "@ 29 | 30 | New-DLabRouterVM $VMName ` 31 | -WanSwitchName $WanSwitchName ` 32 | -LanSwitchName $LanSwitchName ` 33 | -NetworkInterfaces $NetworkInterfaces ` 34 | -DnsMasqConf $DnsMasqConf ` 35 | -Force 36 | 37 | Start-DLabVM $VMName 38 | 39 | Wait-DLabVM $VMName 'Shutdown' -Timeout 600 40 | 41 | Start-DLabVM $VMName 42 | -------------------------------------------------------------------------------- /powershell/scripts/Fix-HyperVNetworkAdapters.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Position = 0)] 3 | [string] $IPAddressMatch = "10.10.0.*" 4 | ) 5 | 6 | function Get-HyperVNetworkAdapterInfo { 7 | [CmdletBinding()] 8 | param() 9 | 10 | $PnpDevices = @(Get-PnpDevice -Class Net | Where-Object { $_.FriendlyName -like '*Hyper-V Network Adapter*' }) 11 | 12 | $PnpDevices | ForEach-Object { 13 | $ClassGuid = $_.ClassGuid 14 | $DeviceDriverProperty = Get-PnpDeviceProperty -InstanceId $_.InstanceId -KeyName 'DEVPKEY_Device_Driver' -ErrorAction SilentlyContinue 15 | if ($DeviceDriverProperty.Data -match '\\(?\d{4})$') { 16 | $DeviceRegPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Class\$ClassGuid\$($matches.subkey)" 17 | $NetCfgInstanceId = Get-ItemPropertyValue -Path $DeviceRegPath -Name NetCfgInstanceId -ErrorAction SilentlyContinue 18 | $NetCfgInstanceRegPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\$NetCfgInstanceId" 19 | $NetCfg = Get-ItemProperty -Path $NetCfgInstanceRegPath | Select-Object IPAddress, SubnetMask, DefaultGateway, NameServer, EnableDHCP 20 | $NetConnectionRegPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Network\$ClassGuid\$NetCfgInstanceId\Connection" 21 | $NetAdapterName = Get-ItemPropertyValue -Path $NetConnectionRegPath -Name Name 22 | [PSCustomObject]@{ 23 | FriendlyName = $_.FriendlyName 24 | NetAdapterName = $NetAdapterName 25 | Status = $_.Status 26 | InstanceId = $_.InstanceId 27 | NetCfgInstanceId = $NetCfgInstanceId 28 | IPAddress = if ($NetCfg.IPAddress) { $NetCfg.IPAddress[0] } else { '' } 29 | SubnetMask = if ($NetCfg.SubnetMask) { $NetCfg.SubnetMask[0] } else { '' } 30 | DefaultGateway = if ($NetCfg.DefaultGateway) { $NetCfg.DefaultGateway[0] } else { '' } 31 | NameServer = $NetCfg.NameServer 32 | EnableDHCP = $NetCfg.EnableDHCP 33 | } 34 | } 35 | } 36 | } 37 | 38 | $NetAdapters = Get-HyperVNetworkAdapterInfo 39 | $OldAdapter = $NetAdapters | Where-Object { $_.Status -eq 'Unknown' -and $_.IPAddress -Match $IPAddressMatch } | Select-Object -First 1 40 | $NewAdapter = $NetAdapters | Where-Object { $_.Status -eq 'OK' -and [string]::IsNullOrEmpty($_.IPAddress) } | Select-Object -First 1 41 | 42 | if ($OldAdapter -and $NewAdapter) { 43 | Write-Host "Removing old network adapter: '$($OldAdapter.NetAdapterName)'" 44 | & pnputil /remove-device "$($OldAdapter.InstanceId)" 45 | 46 | $NetAdapterName = $OldAdapter.NetAdapterName 47 | $IPAddress = $OldAdapter.IPAddress 48 | $SubnetMask = $OldAdapter.SubnetMask 49 | $DefaultGateway = $OldAdapter.DefaultGateway 50 | $NameServer = $OldAdapter.NameServer 51 | Write-Host "Renaming new network adapter to '$NetAdapterName'" 52 | Rename-NetAdapter -Name $NewAdapter.NetAdapterName -NewName $NetAdapterName 53 | $PrefixLength = ([System.Net.IPAddress]::Parse($SubnetMask).GetAddressBytes() | 54 | ForEach-Object { [Convert]::ToString($_, 2).PadLeft(8, '0') -split '' } | Where-Object { $_ -eq '1' }).Count 55 | $Params = @{ 56 | IPAddress = $IPAddress; 57 | InterfaceAlias = $NetAdapterName; 58 | AddressFamily = "IPv4"; 59 | PrefixLength = $PrefixLength; 60 | DefaultGateway = $DefaultGateway; 61 | } 62 | Write-Host "Configuring '$NetAdapterName':" 63 | Write-Host "`tIPAddress: $IPAddress`n`tSubnetMask: $SubnetMask`n`tDefaultGateway: $DefaultGateway" 64 | Set-NetIPInterface -InterfaceAlias $NetAdapterName -Dhcp Disabled 65 | Get-NetIPAddress -InterfaceAlias $NetAdapterName -AddressFamily IPv4 -ErrorAction SilentlyContinue | Remove-NetIPAddress -Confirm:$false 66 | New-NetIPAddress @Params 67 | Write-Host "Setting DNS server: $NameServer" 68 | Set-DnsClientServerAddress -InterfaceAlias $NetAdapterName -ServerAddresses $NameServer 69 | } 70 | 71 | Get-HyperVNetworkAdapterInfo | Where-Object { $_.Status -eq 'Unknown' } | ForEach-Object { 72 | Write-Host "Removing ghost network adapter: '$($_.NetAdapterName)'" 73 | & pnputil /remove-device "$($_.InstanceId)" 74 | } 75 | -------------------------------------------------------------------------------- /powershell/scripts/New-CertificateTemplate.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory = $true, Position = 0)] 3 | [string] $BaseCertTypeName, 4 | [Parameter(Mandatory = $true, Position = 1)] 5 | [string] $NewCertTypeName, 6 | [Parameter(Mandatory = $true, Position = 2)] 7 | [string] $NewCertTypeFriendlyName, 8 | 9 | [switch] $EnrolleeSuppliesSubject, 10 | [switch] $AllowExportableKey, 11 | [switch] $EnableTemplate 12 | ) 13 | 14 | $certca = @" 15 | using System; 16 | using System.Runtime.InteropServices; 17 | 18 | public class CertCA 19 | { 20 | public const uint CA_FLAG_ENUM_ALL_TYPES = 0x00000004; 21 | public const uint CT_FIND_LOCAL_SYSTEM = 0x00000002; 22 | public const uint CT_ENUM_MACHINE_TYPES = 0x00000040; 23 | public const uint CT_ENUM_USER_TYPES = 0x00000080; 24 | public const uint CT_FIND_BY_OID = 0x00000200; 25 | public const uint CT_FLAG_NO_CACHE_LOOKUP = 0x00000400; 26 | public const uint CT_FLAG_SCOPE_IS_LDAP_HANDLE = 0x00000800; 27 | public const uint CT_ENUM_ADMINISTRATOR_FORCE_MACHINE = 0x00001000; 28 | public const uint CT_ENUM_NO_CACHE_TO_REGISTRY = 0x00002000; 29 | public const uint CT_FLAG_ENUM_INCLUDE_INVALID_TYPES = 0x00004000; 30 | 31 | // Cert Type Flags 32 | public const uint CERTTYPE_ENROLLMENT_FLAG = 0x01; 33 | public const uint CERTTYPE_SUBJECT_NAME_FLAG = 0x02; 34 | public const uint CERTTYPE_PRIVATE_KEY_FLAG = 0x03; 35 | public const uint CERTTYPE_GENERAL_FLAG = 0x04; 36 | 37 | // Subject Name Flags 38 | public const uint CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT = 0x00000001; 39 | public const uint CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME = 0x00010000; 40 | public const uint CT_FLAG_SUBJECT_REQUIRE_DIRECTORY_PATH = 0x80000000; 41 | public const uint CT_FLAG_SUBJECT_REQUIRE_COMMON_NAME = 0x40000000; 42 | public const uint CT_FLAG_SUBJECT_REQUIRE_EMAIL = 0x20000000; 43 | public const uint CT_FLAG_SUBJECT_REQUIRE_DNS_AS_CN = 0x10000000; 44 | public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_DNS = 0x08000000; 45 | public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_EMAIL = 0x04000000; 46 | public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_UPN = 0x02000000; 47 | public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_DIRECTORY_GUID = 0x01000000; 48 | public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_SPN = 0x00800000; 49 | public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_DOMAIN_DNS = 0x00400000; 50 | public const uint CT_FLAG_OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME = 0x00000008; 51 | 52 | // Private Key Flags 53 | public const uint CT_FLAG_ALLOW_PRIVATE_KEY_ARCHIVAL = 0x00000001; 54 | public const uint CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL = CT_FLAG_ALLOW_PRIVATE_KEY_ARCHIVAL; 55 | public const uint CT_FLAG_EXPORTABLE_KEY = 0x00000010; 56 | public const uint CT_FLAG_STRONG_KEY_PROTECTION_REQUIRED = 0x00000020; 57 | 58 | [DllImport("certca.dll", CharSet = CharSet.Unicode)] 59 | public static extern int CAFindCertTypeByName( 60 | string wszCertType, 61 | IntPtr hCAInfo, 62 | uint dwFlags, 63 | out IntPtr phCertType 64 | ); 65 | 66 | [DllImport("certca.dll", CharSet = CharSet.Unicode)] 67 | public static extern int CACloneCertType( 68 | IntPtr hCertType, 69 | string wszCertType, 70 | string wszFriendlyName, 71 | IntPtr pvldap, 72 | uint dwFlags, 73 | out IntPtr phCertType 74 | ); 75 | 76 | [DllImport("certca.dll", CharSet = CharSet.Unicode)] 77 | public static extern int CASetCertTypeFlagsEx( 78 | IntPtr hCertType, 79 | uint dwOption, 80 | uint dwFlags 81 | ); 82 | 83 | [DllImport("certca.dll", CharSet = CharSet.Unicode)] 84 | public static extern int CAUpdateCertType( 85 | IntPtr hCertType 86 | ); 87 | 88 | [DllImport("certca.dll", CharSet = CharSet.Unicode)] 89 | public static extern int CACloseCertType( 90 | IntPtr hCertType 91 | ); 92 | } 93 | "@ 94 | 95 | Add-Type -TypeDefinition $certca 96 | 97 | $dwFlags = [CertCA]::CT_FLAG_NO_CACHE_LOOKUP -bor [CertCA]::CT_ENUM_MACHINE_TYPES -bor [CertCA]::CT_ENUM_USER_TYPES 98 | $hCAInfo = [IntPtr]::Zero 99 | $hBaseCertType = [IntPtr]::Zero 100 | $hNewCertType = [IntPtr]::Zero 101 | 102 | try { 103 | # Find base certificate template type 104 | $hr = [CertCA]::CAFindCertTypeByName($BaseCertTypeName, $hCAInfo, $dwFlags, [ref]$hBaseCertType) 105 | 106 | if ($hBaseCertType -eq [IntPtr]::Zero) { 107 | throw "Base certificate type '$BaseCertTypeName' not found." 108 | } 109 | 110 | # Clone base certificate template type 111 | $hr = [CertCA]::CACloneCertType($hBaseCertType, $NewCertTypeName, $NewCertTypeFriendlyName, [IntPtr]::Zero, 0, [ref]$hNewCertType) 112 | 113 | if ($hNewCertType -eq [IntPtr]::Zero) { 114 | if ($hr -eq 0x80092005) { 115 | throw "New certificate type '$NewCertTypeName' already exists." 116 | } else { 117 | throw "Failed to create new certificate type '$NewCertTypeName': 0x" + $hr.ToString("X") 118 | } 119 | } 120 | 121 | # Close base certificate template type handle 122 | $hr = [CertCA]::CACloseCertType($hBaseCertType) 123 | 124 | $subjectNameFlag = 0 125 | if ($EnrolleeSuppliesSubject) { 126 | # Subject Name: "supply in the request" 127 | $subjectNameFlag = [CertCA]::CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT 128 | } 129 | $hr = [CertCA]::CASetCertTypeFlagsEx($hNewCertType, [CertCA]::CERTTYPE_SUBJECT_NAME_FLAG, $subjectNameFlag) 130 | 131 | $privateKeyFlag = 0 132 | if ($AllowExportableKey) { 133 | # Request Handling: "allow private key to be exported" 134 | $privateKeyFlag = [CertCA]::CT_FLAG_EXPORTABLE_KEY 135 | } 136 | $hr = [CertCA]::CASetCertTypeFlagsEx($hNewCertType, [CertCA]::CERTTYPE_PRIVATE_KEY_FLAG, $privateKeyFlag) 137 | 138 | # Save changes (write template to registry) 139 | $hr = [CertCA]::CAUpdateCertType($hNewCertType) 140 | 141 | # Close new certificate template type handle 142 | $hr = [CertCA]::CACloseCertType($hNewCertType) 143 | 144 | # Enable new certificate template type 145 | if ($EnableTemplate) { 146 | Add-CATemplate -Name $NewCertTypeName -Force 147 | } 148 | } 149 | catch { 150 | Write-Error $_.Exception.Message 151 | exit 1 152 | } 153 | 154 | Write-Host "New certificate template '$NewCertTypeName' successfully created and configured." 155 | 156 | # .\New-CertificateTemplate.ps1 "SmartcardLogon" "MySmartcardLogon" "My Smartcard Logon" -EnrolleeSuppliesSubject -AllowExportableKey -EnableTemplate 157 | -------------------------------------------------------------------------------- /powershell/test_vm.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | #Requires -PSEdition Core 3 | 4 | param( 5 | [string] $VMAlias = "TEST", 6 | [int] $VMNumber = 25 7 | ) 8 | 9 | . .\common.ps1 10 | 11 | $VMName = $LabPrefix, $VMAlias -Join "-" 12 | $IpAddress = Get-DLabIpAddress $LabNetworkBase $VMNumber 13 | 14 | New-DLabVM $VMName -Password $LocalPassword -OSVersion $OSVersion -Force 15 | Start-DLabVM $VMName 16 | 17 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $LocalUserName -Password $LocalPassword 18 | $VMSession = New-DLabVMSession $VMName -UserName $LocalUserName -Password $LocalPassword 19 | 20 | Set-DLabVMNetAdapter $VMName -VMSession $VMSession ` 21 | -SwitchName $SwitchName -NetAdapterName $NetAdapterName ` 22 | -IPAddress $IPAddress -DefaultGateway $DefaultGateway ` 23 | -DnsServerAddress $DnsServerAddress 24 | 25 | Write-Host "Joining domain" 26 | 27 | Add-DLabVMToDomain $VMName -VMSession $VMSession ` 28 | -DomainName $DomainName -DomainController $DCHostName ` 29 | -UserName $DomainUserName -Password $DomainPassword 30 | 31 | # Wait for virtual machine to reboot after domain join operation 32 | 33 | Wait-DLabVM $VMName 'Reboot' -Timeout 120 34 | Wait-DLabVM $VMName 'Heartbeat' -Timeout 600 -UserName $DomainUserName -Password $DomainPassword 35 | 36 | $VMSession = New-DLabVMSession $VMName -UserName $DomainUserName -Password $DomainPassword 37 | 38 | Write-Host "Requesting RDP server certificate" 39 | 40 | Request-DLabRdpCertificate $VMName -VMSession $VMSession ` 41 | -CAHostName $CAHostName -CACommonName $CACommonName 42 | 43 | Write-Host "Initializing PSRemoting" 44 | 45 | Initialize-DLabPSRemoting $VMName -VMSession $VMSession 46 | 47 | Write-Host "Initializing VNC server" 48 | 49 | Initialize-DLabVncServer $VMName -VMSession $VMSession 50 | -------------------------------------------------------------------------------- /powershell/unattend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Setup apk cache on USB drive 4 | mount -o remount,rw /media/sda1 5 | mkdir /media/sda1/cache 6 | setup-apkcache /media/sda1/cache 7 | 8 | # Install iptables 9 | apk add iptables 10 | rc-update add iptables 11 | 12 | # Configure routing 13 | echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf 14 | sysctl -p 15 | 16 | iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE 17 | iptables -A FORWARD -i eth1 -j ACCEPT 18 | /etc/init.d/iptables save 19 | /etc/init.d/iptables restart 20 | 21 | # Install DHCP server 22 | apk add dnsmasq 23 | rc-update add dnsmasq default 24 | 25 | # Install common tools 26 | apk add nano 27 | apk add sudo 28 | 29 | # Install Hyper-V extensions 30 | apk add hvtools 31 | rc-update add hv_fcopy_daemon 32 | rc-update add hv_kvp_daemon 33 | rc-update add hv_vss_daemon 34 | 35 | # Install OpenSSH 36 | apk add openssh 37 | rc-update add sshd 38 | 39 | # fix key permissions 40 | find /etc/ssh/*_key -type f -exec chmod 600 {} \;; 41 | 42 | # Enable SSH root login 43 | echo "PermitRootLogin yes" >> /etc/ssh/sshd_config 44 | 45 | # Install PowerShell 7 46 | 47 | apk add --no-cache ca-certificates less \ 48 | ncurses-terminfo-base krb5-libs \ 49 | libgcc libintl libssl1.1 libstdc++ \ 50 | tzdata userspace-rcu zlib icu-libs curl 51 | 52 | apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache lttng-ust 53 | 54 | curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.3.12/powershell-7.3.12-linux-alpine-x64.tar.gz -o /tmp/powershell.tar.gz 55 | 56 | mkdir -p /opt/microsoft/powershell/7 57 | tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 58 | chmod +x /opt/microsoft/powershell/7/pwsh 59 | ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh 60 | 61 | # Enable PowerShell Remoting 62 | echo "Subsystem powershell /opt/microsoft/powershell/7/pwsh -sshs -NoLogo" >> /etc/ssh/sshd_config 63 | 64 | # commit overlay changes 65 | lbu ci 66 | 67 | # disable run-once script 68 | mv /media/sda1/unattend.sh /media/sda1/unattend.old 69 | 70 | # shutdown, we're done! 71 | poweroff 72 | -------------------------------------------------------------------------------- /powershell/unattend.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | en-US 7 | 8 | 0c09:00000409 9 | en-US 10 | en-US 11 | en-US 12 | en-US 13 | 14 | 15 | 16 | 17 | 18 | 19 | 1 20 | 250 21 | Primary 22 | 23 | 24 | 2 25 | true 26 | Primary 27 | 28 | 29 | 30 | 31 | 1 32 | 1 33 | NTFS 34 | 35 | true 36 | 37 | 38 | 2 39 | 2 40 | NTFS 41 | 42 | 43 | 44 | 0 45 | true 46 | 47 | 48 | 49 | 50 | 51 | 52 | /IMAGE/NAME 53 | Windows Server 2025 SERVERSTANDARD 54 | 55 | 56 | 57 | 0 58 | 2 59 | 60 | OnError 61 | false 62 | 63 | 64 | 65 | true 66 | User 67 | Organization 68 | 69 | 70 | Never 71 | 72 | 73 | 74 | 75 | 76 | 77 | false 78 | 79 | 80 | 81 | 82 | 1 83 | 84 | 85 | 86 | 87 | 0409:00000409 88 | en-US 89 | en-US 90 | en-US 91 | en-US 92 | 93 | 94 | Computer 95 | 96 | 97 | 98 | 99 | 100 | 101 | Password123! 102 | true</PlainText> 103 | </AdministratorPassword> 104 | </UserAccounts> 105 | <OOBE> 106 | <HideEULAPage>true</HideEULAPage> 107 | <HideLocalAccountScreen>true</HideLocalAccountScreen> 108 | <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> 109 | <HideOnlineAccountScreens>true</HideOnlineAccountScreens> 110 | <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> 111 | <NetworkLocation>Work</NetworkLocation> 112 | <ProtectYourPC>3</ProtectYourPC> 113 | <SkipMachineOOBE>true</SkipMachineOOBE> 114 | <SkipUserOOBE>true</SkipUserOOBE> 115 | </OOBE> 116 | <TimeZone>Eastern Standard Time</TimeZone> 117 | </component> 118 | <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 119 | <InputLocale>en-US</InputLocale> 120 | <SystemLocale>en-US</SystemLocale> 121 | <UserLocale>en-CA</UserLocale> 122 | </component> 123 | </settings> 124 | <cpi:offlineImage cpi:source="" xmlns:cpi="urn:schemas-microsoft-com:cpi" /> 125 | </unattend> --------------------------------------------------------------------------------