#Requires -Version 5.1 [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][string]$SubscriptionId, [string]$ResourceGroupName = 'rg-sgu-lab', [string]$Location = 'centralus', [string]$DeploymentPrefix = 'sgu-lab', [Parameter(Mandatory)][string]$AdministratorUsername, [securestring]$AdministratorPassword, [string]$P2sRootCertificatePath, [bool]$DeployVpnGateway = $true, [string]$ComputerName = 'SGU-DC01', [string]$VmSize = 'Standard_D2s_v5', [string]$VirtualNetworkAddressPrefix = '10.77.0.0/16', [string]$DomainControllerSubnetPrefix = '10.77.0.0/24', [ipaddress]$DomainControllerPrivateIp = '10.77.0.4', [string]$GatewaySubnetPrefix = '10.77.255.0/27', [string]$VpnClientAddressPoolPrefix = '172.30.0.0/24', [string[]]$PublicEnrollmentSourceAddressPrefixes = @(), [string]$AdministratorSourceAddressPrefix = '', [string]$TemplateFile = (Join-Path $PSScriptRoot '..\infra\azure\main.bicep') ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' if (-not (Get-Command az -ErrorAction SilentlyContinue)) { throw 'Azure CLI is required. Install it from https://aka.ms/installazurecliwindows and run az login.' } if (-not (Test-Path -LiteralPath $TemplateFile -PathType Leaf)) { throw "Azure Bicep template not found: $TemplateFile" } if (-not $AdministratorPassword) { $AdministratorPassword = Read-Host 'Password for the local Azure VM administrator' -AsSecureString } $rootCertificateData = '' if ($DeployVpnGateway) { if (-not $P2sRootCertificatePath -or -not (Test-Path -LiteralPath $P2sRootCertificatePath -PathType Leaf)) { throw 'P2sRootCertificatePath is required when DeployVpnGateway is true.' } $rootCertificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new( (Resolve-Path -LiteralPath $P2sRootCertificatePath).Path) if (-not ($rootCertificate.Extensions | Where-Object { $_.Oid -and $_.Oid.Value -eq '2.5.29.19' -and $_.Format($false) -match 'CA' })) { throw 'P2sRootCertificatePath must contain a certificate-authority certificate.' } $rootCertificateData = [Convert]::ToBase64String($rootCertificate.RawData) } $account = & az account show --output json 2>$null if ($LASTEXITCODE -ne 0) { throw 'Azure CLI is not signed in. Run az login, then retry.' } & az account set --subscription $SubscriptionId --only-show-errors if ($LASTEXITCODE -ne 0) { throw "Could not select Azure subscription $SubscriptionId." } $deploymentDescription = if ($DeployVpnGateway) { 'Create Azure VNet, Windows Server 2025 VM, public IP, and P2S VPN Gateway' } else { 'Create Azure VNet, Windows Server 2025 VM, and public IP for direct enrollment' } if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", $deploymentDescription)) { & az group create --name $ResourceGroupName --location $Location --only-show-errors --output none if ($LASTEXITCODE -ne 0) { throw "Could not create or update resource group $ResourceGroupName." } $temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) ("sgu-azure-" + [Guid]::NewGuid().ToString('N')) $parametersPath = Join-Path $temporaryRoot 'parameters.json' $passwordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($AdministratorPassword) try { New-Item -ItemType Directory -Path $temporaryRoot -Force | Out-Null $acl = Get-Acl -LiteralPath $temporaryRoot $acl.SetAccessRuleProtection($true, $false) $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( [Security.Principal.WindowsIdentity]::GetCurrent().User, [Security.AccessControl.FileSystemRights]::FullControl, [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit', [Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow)) Set-Acl -LiteralPath $temporaryRoot -AclObject $acl $plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($passwordPointer) $parameters = [ordered]@{ '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' contentVersion = '1.0.0.0' parameters = [ordered]@{ deploymentPrefix = @{ value = $DeploymentPrefix } location = @{ value = $Location } administratorUsername = @{ value = $AdministratorUsername } administratorPassword = @{ value = $plainPassword } computerName = @{ value = $ComputerName } vmSize = @{ value = $VmSize } virtualNetworkAddressPrefix = @{ value = $VirtualNetworkAddressPrefix } domainControllerSubnetPrefix = @{ value = $DomainControllerSubnetPrefix } gatewaySubnetPrefix = @{ value = $GatewaySubnetPrefix } domainControllerPrivateIp = @{ value = $DomainControllerPrivateIp.IPAddressToString } vpnClientAddressPoolPrefix = @{ value = $VpnClientAddressPoolPrefix } deployVpnGateway = @{ value = $DeployVpnGateway } p2sRootCertificateData = @{ value = $rootCertificateData } publicEnrollmentSourceAddressPrefixes = @{ value = @($PublicEnrollmentSourceAddressPrefixes) } administratorSourceAddressPrefix = @{ value = $AdministratorSourceAddressPrefix } } } [IO.File]::WriteAllText( $parametersPath, ($parameters | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false)) $plainPassword = $null $parameters.parameters.administratorPassword.value = $null $deploymentName = 'sgu-{0}' -f (Get-Date -Format 'yyyyMMdd-HHmmss') $deploymentOutput = & az deployment group create ` --name $deploymentName ` --resource-group $ResourceGroupName ` --template-file (Resolve-Path -LiteralPath $TemplateFile).Path ` --parameters "@$parametersPath" ` --only-show-errors ` --output json if ($LASTEXITCODE -ne 0) { throw 'Azure deployment failed. Review the Azure CLI error above; no bootstrap credential was persisted by this script.' } $deployment = ($deploymentOutput -join [Environment]::NewLine) | ConvertFrom-Json } finally { if ($passwordPointer -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordPointer) } $AdministratorPassword = $null if ($temporaryRoot -and (Test-Path -LiteralPath $temporaryRoot)) { Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue } } $values = @{} foreach ($property in $deployment.properties.outputs.PSObject.Properties) { $values[$property.Name] = $property.Value.value } [pscustomobject]@{ ResourceGroupName = $ResourceGroupName DeploymentName = $deploymentName DomainControllerName = $values.domainControllerName DomainControllerPrivateIp = $values.domainControllerPrivateIp DomainControllerPublicIp = $values.domainControllerPublicIp VpnGatewayName = $values.vpnGatewayName VpnClientAddressPoolPrefix = $values.vpnClientAddressPoolPrefix DeployVpnGateway = $DeployVpnGateway PublicEnrollmentSourceAddressPrefixes = @($PublicEnrollmentSourceAddressPrefixes) ServerBootstrapArguments = $values.serverBootstrapArguments } }