Prepare Azure P2S domain deployment

This commit is contained in:
2026-09-08 15:32:07 -06:00
parent 991fc70600
commit a24c25a3fb
18 changed files with 1288 additions and 28 deletions
+140
View File
@@ -0,0 +1,140 @@
#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,
[Parameter(Mandatory)][string]$P2sRootCertificatePath,
[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]$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 (Test-Path -LiteralPath $P2sRootCertificatePath -PathType Leaf)) {
throw "P2S root certificate not found: $P2sRootCertificatePath"
}
if (-not $AdministratorPassword) {
$AdministratorPassword = Read-Host 'Password for the local Azure VM administrator' -AsSecureString
}
$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."
}
if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", 'Create Azure VNet, Windows Server 2025 VM, public IP, and P2S VPN Gateway')) {
& 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 }
p2sRootCertificateData = @{ value = $rootCertificateData }
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
ServerBootstrapArguments = $values.serverBootstrapArguments
}
}
+18 -3
View File
@@ -22,6 +22,8 @@ param(
[string]$NewComputerName,
[string]$NetworkInterfaceAlias = 'Ethernet',
[string[]]$DomainDnsServerAddresses = @('192.168.50.10'),
[ValidateSet('Direct', 'AzureP2S')]
[string]$ConnectivityMode = 'Direct',
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
[string]$DotNetRuntimeInstallerPath,
[string]$RustDeskServerAddress,
@@ -90,9 +92,21 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before jo
# The broker uses a domain DNS name even before the machine joins the
# domain. Point at AD DNS first so the provider-first health check works on
# a completely clean Windows installation.
Set-DnsClientServerAddress `
-InterfaceAlias $NetworkInterfaceAlias `
-ServerAddresses $DomainDnsServerAddresses
if ($ConnectivityMode -eq 'Direct') {
Set-DnsClientServerAddress `
-InterfaceAlias $NetworkInterfaceAlias `
-ServerAddresses $DomainDnsServerAddresses
}
else {
$nrptDisplayName = "SGU Azure P2S DNS - $DomainName"
$nrptRule = Get-DnsClientNrptRule -ErrorAction SilentlyContinue |
Where-Object DisplayName -eq $nrptDisplayName |
Select-Object -First 1
if (-not $nrptRule -or
@($DomainDnsServerAddresses | Where-Object { @($nrptRule.NameServers) -contains $_ }).Count -eq 0) {
throw "AzureP2S enrollment requires the managed NRPT rule '$nrptDisplayName'. Run Install-SguAzureP2sClient.ps1 first."
}
}
Resolve-DnsName -Type SRV "_ldap._tcp.dc._msdcs.$DomainName" -ErrorAction Stop | Out-Null
& (Join-Path $PSScriptRoot 'Install-CredentialProvider.ps1') @installParams | Out-Null
@@ -159,6 +173,7 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before jo
[pscustomobject]@{
ComputerName = if ($NewComputerName) { $NewComputerName } else { $env:COMPUTERNAME }
DomainName = $DomainName
ConnectivityMode = $ConnectivityMode
ProviderValidatedBeforeJoin = $true
RustDesk = $rustDeskResult
RestartRequired = [bool]$SkipRestart
+48
View File
@@ -0,0 +1,48 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$SubscriptionId,
[Parameter(Mandatory)][string]$ResourceGroupName,
[Parameter(Mandatory)][string]$VpnGatewayName,
[string]$OutputPath = (Join-Path $PSScriptRoot '..\artifacts\azure-p2s\sgu-azure-vpn-client.zip')
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
throw 'Azure CLI is required.'
}
& az account set --subscription $SubscriptionId --only-show-errors
if ($LASTEXITCODE -ne 0) {
throw "Could not select Azure subscription $SubscriptionId."
}
if ($PSCmdlet.ShouldProcess($OutputPath, 'Generate and download the Azure P2S client package')) {
$downloadUriText = & az network vnet-gateway vpn-client generate `
--resource-group $ResourceGroupName `
--name $VpnGatewayName `
--processor-architecture Amd64 `
--authentication-method EAPTLS `
--only-show-errors `
--output tsv
$downloadUriText = ($downloadUriText -join '').Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($downloadUriText)) {
throw 'Azure did not generate a P2S client package URL.'
}
$downloadUri = $null
if (-not [uri]::TryCreate($downloadUriText, [UriKind]::Absolute, [ref]$downloadUri) -or
$downloadUri.Scheme -ne 'https') {
throw 'Azure returned an invalid VPN client package URL.'
}
$resolvedOutputPath = [IO.Path]::GetFullPath($OutputPath)
New-Item -ItemType Directory -Path (Split-Path $resolvedOutputPath -Parent) -Force | Out-Null
Invoke-WebRequest -Uri $downloadUri -OutFile $resolvedOutputPath -UseBasicParsing
if ((Get-Item -LiteralPath $resolvedOutputPath).Length -lt 1024) {
throw 'The downloaded VPN client package is unexpectedly small.'
}
[pscustomobject]@{
PackagePath = $resolvedOutputPath
Sha256 = (Get-FileHash -LiteralPath $resolvedOutputPath -Algorithm SHA256).Hash
VpnGatewayName = $VpnGatewayName
}
}
+130 -12
View File
@@ -6,6 +6,9 @@ param(
[int]$PrefixLength = 24,
[string]$NetworkInterfaceAlias,
[ipaddress]$DefaultGateway,
[ValidateSet('GuestStatic', 'PlatformManaged')]
[string]$NetworkConfigurationMode = 'GuestStatic',
[string[]]$TrustedClientNetworks = @(),
[ipaddress[]]$DnsForwarders = @(),
[string]$DomainName = 'lci.lasalle.mx',
[string]$DomainNetbios = 'LCI',
@@ -69,6 +72,67 @@ function Get-DomainBaseDn {
return (($DnsDomainName -split '\.') | ForEach-Object { "DC=$_" }) -join ','
}
function Test-PrivateIPv4Address {
param([Parameter(Mandatory)][ipaddress]$Address)
if ($Address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
return $false
}
$bytes = $Address.GetAddressBytes()
return $bytes[0] -eq 10 -or
($bytes[0] -eq 172 -and $bytes[1] -ge 16 -and $bytes[1] -le 31) -or
($bytes[0] -eq 192 -and $bytes[1] -eq 168)
}
function ConvertTo-NetworkCidr {
param(
[Parameter(Mandatory)][ipaddress]$Address,
[Parameter(Mandatory)][ValidateRange(1, 32)][int]$NetworkPrefixLength
)
if ($Address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
throw 'Only IPv4 networks are supported by the SGU bootstrap.'
}
$addressBytes = $Address.GetAddressBytes()
$networkBytes = [byte[]]::new(4)
$remainingBits = $NetworkPrefixLength
for ($index = 0; $index -lt 4; $index++) {
$mask = if ($remainingBits -ge 8) {
255
}
elseif ($remainingBits -le 0) {
0
}
else {
256 - [Math]::Pow(2, 8 - $remainingBits)
}
$networkBytes[$index] = [byte]($addressBytes[$index] -band [int]$mask)
$remainingBits -= 8
}
return "$(($networkBytes | ForEach-Object { [string]$_ }) -join '.')/$NetworkPrefixLength"
}
function ConvertTo-PrivateNetworkCidr {
param([Parameter(Mandatory)][string]$Cidr)
if ($Cidr -notmatch '^([^/]+)/(\d{1,2})$') {
throw "Trusted client network '$Cidr' must use IPv4 CIDR notation, for example 172.30.0.0/24."
}
$address = $null
if (-not [ipaddress]::TryParse($Matches[1], [ref]$address) -or
$address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
throw "Trusted client network '$Cidr' is not a valid IPv4 network."
}
$networkPrefixLength = [int]$Matches[2]
if ($networkPrefixLength -lt 1 -or $networkPrefixLength -gt 32) {
throw "Trusted client network '$Cidr' has an invalid prefix length."
}
if (-not (Test-PrivateIPv4Address -Address $address)) {
throw "Trusted client network '$Cidr' is not private RFC1918 space. The bootstrap never exposes AD services to public client addresses."
}
return ConvertTo-NetworkCidr -Address $address -NetworkPrefixLength $networkPrefixLength
}
function Resolve-PrivateInterfaceAlias {
param([string]$RequestedAlias)
@@ -147,6 +211,27 @@ function Set-StaticDomainAddress {
-ServerAddresses $Address.IPAddressToString
}
function Assert-PlatformManagedDomainAddress {
param(
[Parameter(Mandatory)][string]$InterfaceAlias,
[Parameter(Mandatory)][ipaddress]$Address,
[Parameter(Mandatory)][int]$NetworkPrefixLength
)
$adapter = Get-NetAdapter -Name $InterfaceAlias -ErrorAction Stop
$matchingAddress = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 `
-IPAddress $Address.IPAddressToString -ErrorAction SilentlyContinue |
Where-Object PrefixLength -eq $NetworkPrefixLength |
Select-Object -First 1
if (-not $matchingAddress) {
$observed = @(Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 `
-ErrorAction SilentlyContinue |
Where-Object PrefixOrigin -ne 'WellKnown' |
ForEach-Object { "$($_.IPAddress)/$($_.PrefixLength)" }) -join ', '
throw "PlatformManaged mode expected $Address/$NetworkPrefixLength on $InterfaceAlias, but found: $observed. Configure a static private IP on the Azure NIC before running the bootstrap; do not assign it inside Windows."
}
}
function Register-ResumeTask {
param([Parameter(Mandatory)][string]$ScriptPath)
@@ -310,6 +395,8 @@ if ($Resume -or (-not $ServerIPv4Address -and $existingState)) {
$PrefixLength = [int]$existingState.PrefixLength
$NetworkInterfaceAlias = [string]$existingState.NetworkInterfaceAlias
$DefaultGateway = if ($existingState.DefaultGateway) { [ipaddress][string]$existingState.DefaultGateway } else { $null }
$NetworkConfigurationMode = if ($existingState.NetworkConfigurationMode) { [string]$existingState.NetworkConfigurationMode } else { 'GuestStatic' }
$TrustedClientNetworks = if ($existingState.TrustedClientNetworks) { @($existingState.TrustedClientNetworks | ForEach-Object { [string]$_ }) } else { @() }
$DnsForwarders = @($existingState.DnsForwarders | ForEach-Object { [ipaddress][string]$_ })
$DomainName = [string]$existingState.DomainName
$DomainNetbios = [string]$existingState.DomainNetbios
@@ -321,6 +408,16 @@ if ($Resume -or (-not $ServerIPv4Address -and $existingState)) {
if (-not $ServerIPv4Address) {
$ServerIPv4Address = [ipaddress](Read-Host 'Fixed IPv4 address for this domain controller')
}
if (-not (Test-PrivateIPv4Address -Address $ServerIPv4Address)) {
throw 'ServerIPv4Address must be the private address of the domain controller. An Azure public IP is never assigned to AD or published in domain DNS.'
}
$domainSubnet = ConvertTo-NetworkCidr -Address $ServerIPv4Address `
-NetworkPrefixLength $PrefixLength
$TrustedClientNetworks = @($TrustedClientNetworks |
ForEach-Object { ConvertTo-PrivateNetworkCidr -Cidr $_ } |
Where-Object { $_ -ne $domainSubnet } |
Select-Object -Unique)
$allowedRemoteAddresses = @($domainSubnet) + $TrustedClientNetworks
$sourceRoot = $PSScriptRoot
if (-not $Resume) {
@@ -383,6 +480,8 @@ if (-not $existingState) {
PrefixLength = $PrefixLength
NetworkInterfaceAlias = $NetworkInterfaceAlias
DefaultGateway = if ($DefaultGateway) { $DefaultGateway.IPAddressToString } else { $null }
NetworkConfigurationMode = $NetworkConfigurationMode
TrustedClientNetworks = $TrustedClientNetworks
DnsForwarders = @($DnsForwarders | ForEach-Object IPAddressToString)
DomainName = $DomainName
DomainNetbios = $DomainNetbios
@@ -396,9 +495,16 @@ if (-not $existingState) {
[Text.UTF8Encoding]::new($false))
}
Write-BootstrapLog "Configuring $NetworkInterfaceAlias as $ServerIPv4Address/$PrefixLength."
Set-StaticDomainAddress -InterfaceAlias $NetworkInterfaceAlias `
-Address $ServerIPv4Address -NetworkPrefixLength $PrefixLength -Gateway $DefaultGateway
if ($NetworkConfigurationMode -eq 'PlatformManaged') {
Write-BootstrapLog "Validating platform-managed address $ServerIPv4Address/$PrefixLength on $NetworkInterfaceAlias without changing DHCP, routes, or the Azure NIC."
Assert-PlatformManagedDomainAddress -InterfaceAlias $NetworkInterfaceAlias `
-Address $ServerIPv4Address -NetworkPrefixLength $PrefixLength
}
else {
Write-BootstrapLog "Configuring $NetworkInterfaceAlias as $ServerIPv4Address/$PrefixLength."
Set-StaticDomainAddress -InterfaceAlias $NetworkInterfaceAlias `
-Address $ServerIPv4Address -NetworkPrefixLength $PrefixLength -Gateway $DefaultGateway
}
$computer = Get-CimInstance Win32_ComputerSystem
if (-not $computer.PartOfDomain) {
@@ -472,8 +578,17 @@ Wait-ActiveDirectoryReady -ExpectedBaseDn $baseDn
$domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias `
-ErrorAction SilentlyContinue
if (-not $domainProfile -or $domainProfile.NetworkCategory -ne 'DomainAuthenticated') {
Write-BootstrapLog "Refreshing $NetworkInterfaceAlias so Windows detects the domain network profile."
Restart-NetAdapter -Name $NetworkInterfaceAlias -Confirm:$false
if ($NetworkConfigurationMode -eq 'GuestStatic') {
Write-BootstrapLog "Refreshing $NetworkInterfaceAlias so Windows detects the domain network profile."
Restart-NetAdapter -Name $NetworkInterfaceAlias -Confirm:$false
}
else {
# Restarting an Azure NIC from inside the guest can sever the only
# management path. Refresh NLA instead; this does not change the
# platform-managed address, DHCP lease, route, or link state.
Write-BootstrapLog 'Refreshing Network Location Awareness without restarting the Azure adapter.'
Restart-Service NlaSvc -Force -ErrorAction SilentlyContinue
}
for ($attempt = 1; $attempt -le 15; $attempt++) {
Start-Sleep -Seconds 2
$domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias `
@@ -577,7 +692,7 @@ if (Test-Path -LiteralPath $brokerConfigurationPath -PathType Leaf) {
-RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName `
-DefaultCompany 'La Salle' `
-FirewallLocalAddress $ServerIPv4Address `
-FirewallRemoteAddress "$($ServerIPv4Address.IPAddressToString)/$PrefixLength" `
-FirewallRemoteAddress $allowedRemoteAddresses `
-CreateMissingOus `
-DisableCertificateRevocationCheckForLab | Out-Null
@@ -591,9 +706,8 @@ foreach ($hostRecord in $hostRecords) {
}
}
$privateSubnet = "$($ServerIPv4Address.IPAddressToString)/$PrefixLength"
& (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') `
-AllowedRemoteAddress $privateSubnet | Out-Null
-AllowedRemoteAddress $allowedRemoteAddresses | Out-Null
$contentPath = Join-Path $bootstrapRoot 'payload\server-content\Packages'
if (Test-Path -LiteralPath $contentPath -PathType Container) {
@@ -613,7 +727,7 @@ if (-not $packageFirewallRule) {
-Protocol TCP `
-LocalPort 445 `
-LocalAddress $ServerIPv4Address.IPAddressToString `
-RemoteAddress $privateSubnet `
-RemoteAddress $allowedRemoteAddresses `
-Profile Any | Out-Null
}
else {
@@ -621,7 +735,7 @@ else {
$packageFirewallRule | Get-NetFirewallAddressFilter |
Set-NetFirewallAddressFilter `
-LocalAddress $ServerIPv4Address.IPAddressToString `
-RemoteAddress $privateSubnet | Out-Null
-RemoteAddress $allowedRemoteAddresses | Out-Null
}
$collectorFqdn = "$env:COMPUTERNAME.$DomainName"
@@ -642,7 +756,7 @@ $userPolicyParameters = @{
$rustDeskServer = & (Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1') `
-ServerAddress $rustDeskDnsName `
-FirewallRemoteAddress $privateSubnet
-FirewallRemoteAddress $allowedRemoteAddresses
$rustDeskManagementRoot = Join-Path $env:ProgramData 'SGU\RustDesk'
New-Item -ItemType Directory -Path $rustDeskManagementRoot -Force | Out-Null
foreach ($scriptName in @(
@@ -680,6 +794,9 @@ $validation = [ordered]@{
ComputerName = $env:COMPUTERNAME
DomainName = $DomainName
ServerIPv4Address = $ServerIPv4Address.IPAddressToString
NetworkConfigurationMode = $NetworkConfigurationMode
TrustedClientNetworks = $TrustedClientNetworks
AllowedRemoteAddresses = $allowedRemoteAddresses
BrokerDnsName = $brokerDnsName
BrokerCertificateThumbprint = $serverCertificate.Thumbprint
BrokerService = (Get-Service SGUAuthBroker).Status.ToString()
@@ -714,7 +831,8 @@ if ($validation.BrokerService -ne 'Running' -or
-not $validation.RustDeskHbbsListening -or
-not $validation.RustDeskHbbrListening -or
$validation.EventCollector -ne 'Running' -or
-not $validation.EventSubscription) {
-not $validation.EventSubscription -or
$validation.DomainNetworkProfile -ne 'DomainAuthenticated') {
throw 'Server finalization did not pass service validation. Review bootstrap.log and re-run the bootstrap.'
}
+123
View File
@@ -0,0 +1,123 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$VpnProfilePackagePath,
[Parameter(Mandatory)][string]$ClientCertificatePfxPath,
[securestring]$ClientCertificatePfxPassword,
[Parameter(Mandatory)][string]$ClientRootCertificatePath,
[string]$ConnectionName = 'SGU Azure P2S',
[string[]]$AzureNetworkPrefixes = @('10.77.0.0/16'),
[ipaddress]$DomainControllerIPv4Address = '10.77.0.4',
[string]$DomainName = 'lci.lasalle.mx',
[switch]$Connect
)
$ErrorActionPreference = 'Stop'
foreach ($path in @($VpnProfilePackagePath,$ClientCertificatePfxPath,$ClientRootCertificatePath)) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
throw "Required P2S file not found: $path"
}
}
if (-not $ClientCertificatePfxPassword) {
$ClientCertificatePfxPassword = Read-Host 'Password protecting the P2S client PFX' -AsSecureString
}
$temporaryRoot = Join-Path $env:ProgramData ("SGU\AzureP2S\Import-" + [Guid]::NewGuid().ToString('N'))
try {
Expand-Archive -LiteralPath $VpnProfilePackagePath -DestinationPath $temporaryRoot -Force
$vpnSettingsPath = Get-ChildItem -LiteralPath $temporaryRoot -Recurse -Filter VpnSettings.xml -File |
Select-Object -First 1 -ExpandProperty FullName
if (-not $vpnSettingsPath) {
throw 'The Azure package does not contain Generic\VpnSettings.xml. Generate it with IKEv2 enabled.'
}
[xml]$vpnSettings = Get-Content -LiteralPath $vpnSettingsPath -Raw
$vpnServerNode = $vpnSettings.SelectSingleNode('//*[local-name()="VpnServer"]')
if (-not $vpnServerNode -or [string]::IsNullOrWhiteSpace($vpnServerNode.InnerText)) {
throw 'VpnSettings.xml does not contain the Azure VPN gateway FQDN.'
}
$vpnServer = $vpnServerNode.InnerText.Trim()
$serverRootPath = Get-ChildItem -LiteralPath (Split-Path $vpnSettingsPath -Parent) `
-Filter VpnServerRoot.cer -File | Select-Object -First 1 -ExpandProperty FullName
if ($serverRootPath) {
Import-Certificate -FilePath $serverRootPath -CertStoreLocation Cert:\LocalMachine\Root | Out-Null
}
$clientRoot = Import-Certificate -FilePath $ClientRootCertificatePath `
-CertStoreLocation Cert:\LocalMachine\Root | Select-Object -First 1
$clientCertificates = @(Import-PfxCertificate -FilePath $ClientCertificatePfxPath `
-Password $ClientCertificatePfxPassword -CertStoreLocation Cert:\LocalMachine\My)
$clientCertificate = $clientCertificates |
Where-Object {
$_.HasPrivateKey -and
$_.NotAfter -gt (Get-Date) -and
@($_.EnhancedKeyUsageList | ForEach-Object ObjectId) -contains '1.3.6.1.5.5.7.3.2'
} |
Sort-Object NotAfter -Descending |
Select-Object -First 1
if (-not $clientCertificate) {
throw 'The imported PFX does not contain a valid Client Authentication certificate with a private key.'
}
if ($PSCmdlet.ShouldProcess($ConnectionName, 'Install an all-user IKEv2 Azure P2S connection using a machine certificate')) {
$existingConnection = Get-VpnConnection -Name $ConnectionName -AllUserConnection `
-ErrorAction SilentlyContinue
if ($existingConnection) {
Remove-VpnConnection -Name $ConnectionName -AllUserConnection -Force
}
Add-VpnConnection `
-Name $ConnectionName `
-ServerAddress $vpnServer `
-TunnelType Ikev2 `
-AuthenticationMethod MachineCertificate `
-MachineCertificateIssuerFilter $clientRoot `
-MachineCertificateEKUFilter '1.3.6.1.5.5.7.3.2' `
-EncryptionLevel Required `
-SplitTunneling `
-AllUserConnection `
-DnsSuffix $DomainName `
-Force | Out-Null
foreach ($prefix in $AzureNetworkPrefixes) {
Add-VpnConnectionRoute -ConnectionName $ConnectionName `
-DestinationPrefix $prefix -AllUserConnection -PassThru | Out-Null
}
$nrptDisplayName = "SGU Azure P2S DNS - $DomainName"
Get-DnsClientNrptRule -ErrorAction SilentlyContinue |
Where-Object DisplayName -eq $nrptDisplayName |
Remove-DnsClientNrptRule -Force
Add-DnsClientNrptRule `
-Namespace ".$DomainName" `
-NameServers $DomainControllerIPv4Address.IPAddressToString `
-DisplayName $nrptDisplayName `
-Comment 'Managed by SGU Azure P2S bootstrap; routes only the AD namespace to the domain controller.' | Out-Null
}
if ($Connect) {
& "$env:SystemRoot\System32\rasdial.exe" $ConnectionName
if ($LASTEXITCODE -ne 0) {
throw "Windows could not connect $ConnectionName. Verify UDP 500/4500 (IKEv2) or use the Azure-generated SSTP profile when the local network blocks IKEv2."
}
}
$connection = Get-VpnConnection -Name $ConnectionName -AllUserConnection
[pscustomobject]@{
ConnectionName = $connection.Name
ServerAddress = $connection.ServerAddress
TunnelType = $connection.TunnelType
AllUserConnection = $true
AuthenticationMethod = $connection.AuthenticationMethod
ConnectionStatus = $connection.ConnectionStatus
ClientCertificateThumbprint = $clientCertificate.Thumbprint
DomainControllerIPv4Address = $DomainControllerIPv4Address.IPAddressToString
DomainDnsNamespace = ".$DomainName"
AzureNetworkPrefixes = $AzureNetworkPrefixes
AvailableBeforeLogon = $true
}
}
finally {
$ClientCertificatePfxPassword = $null
if (Test-Path -LiteralPath $temporaryRoot) {
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
+2 -1
View File
@@ -6,7 +6,8 @@ param(
[string]$InstallRoot = "$env:ProgramFiles\SGU\RustDeskServer",
[string]$DataRoot = "$env:ProgramData\SGU\RustDesk\Server",
[string]$FirewallRemoteAddress = '192.168.50.0/24',
[ValidateNotNullOrEmpty()]
[string[]]$FirewallRemoteAddress = @('192.168.50.0/24'),
[uri]$DownloadUri = 'https://github.com/rustdesk/rustdesk-server/releases/download/1.1.16/rustdesk-server-windows-x86_64-unsigned.zip',
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
[string]$ExpectedSha256 = 'B865A3A62FC8755B45480C508F1C4871C3338590408DDA8C58C7E9C373B7ADB0'
+82 -3
View File
@@ -8,6 +8,14 @@ param(
[string]$DomainNetbios = 'LCI',
[string]$ComputerOuDn,
[string]$NewComputerName,
[ValidateSet('Direct', 'AzureP2S')]
[string]$ConnectivityMode = 'Direct',
[string]$VpnConnectionName = 'SGU Azure P2S',
[string]$VpnProfilePackagePath,
[string]$VpnClientCertificatePfxPath,
[securestring]$VpnClientCertificatePfxPassword,
[string]$VpnClientRootCertificatePath,
[string[]]$AzureNetworkPrefixes = @('10.77.0.0/16'),
[switch]$SkipRestart
)
@@ -95,6 +103,30 @@ function Test-TcpPort {
}
}
function Connect-SguAzureP2s {
param([Parameter(Mandatory)][string]$ConnectionName)
$connection = Get-VpnConnection -Name $ConnectionName -AllUserConnection `
-ErrorAction SilentlyContinue
if (-not $connection) {
throw "The all-user VPN connection '$ConnectionName' is not installed. Run Install-SguAzureP2sClient.ps1 in this VM first."
}
if ($connection.TunnelType -notcontains 'Ikev2' -and $connection.TunnelType -ne 'Ikev2') {
throw "The VPN connection '$ConnectionName' is not configured for IKEv2."
}
if ($connection.ConnectionStatus -ne 'Connected') {
& "$env:SystemRoot\System32\rasdial.exe" $ConnectionName
if ($LASTEXITCODE -ne 0) {
throw "Could not connect the Azure P2S profile '$ConnectionName'. Verify the machine certificate and that UDP 500/4500 is permitted by the local network."
}
}
$connection = Get-VpnConnection -Name $ConnectionName -AllUserConnection
if ($connection.ConnectionStatus -ne 'Connected') {
throw "The Azure P2S profile '$ConnectionName' did not reach Connected state."
}
return $connection
}
Assert-Administrator
$operatingSystem = Get-CimInstance Win32_OperatingSystem
if ([int]$operatingSystem.ProductType -ne 1) {
@@ -135,9 +167,52 @@ if (-not $runtimeInstaller) {
throw 'The offline Microsoft .NET 10 x64 runtime installer is missing from the client package.'
}
$NetworkInterfaceAlias = Resolve-ClientInterfaceAlias -RequestedAlias $NetworkInterfaceAlias
Set-DnsClientServerAddress -InterfaceAlias $NetworkInterfaceAlias `
-ServerAddresses $DomainControllerIPv4Address.IPAddressToString
if ($ConnectivityMode -eq 'AzureP2S') {
$existingVpnConnection = Get-VpnConnection -Name $VpnConnectionName -AllUserConnection `
-ErrorAction SilentlyContinue
if (-not $existingVpnConnection) {
$installerPath = Join-Path $packageRoot 'Install-SguAzureP2sClient.ps1'
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
throw 'Install-SguAzureP2sClient.ps1 is missing from the client bootstrap package.'
}
foreach ($vpnInput in @(
@{ Name = 'VpnProfilePackagePath'; Value = $VpnProfilePackagePath },
@{ Name = 'VpnClientCertificatePfxPath'; Value = $VpnClientCertificatePfxPath },
@{ Name = 'VpnClientRootCertificatePath'; Value = $VpnClientRootCertificatePath })) {
if ([string]::IsNullOrWhiteSpace([string]$vpnInput.Value)) {
throw "$($vpnInput.Name) is required the first time an Azure P2S client is enrolled."
}
}
$vpnInstallParameters = @{
VpnProfilePackagePath = $VpnProfilePackagePath
ClientCertificatePfxPath = $VpnClientCertificatePfxPath
ClientRootCertificatePath = $VpnClientRootCertificatePath
ConnectionName = $VpnConnectionName
AzureNetworkPrefixes = $AzureNetworkPrefixes
DomainControllerIPv4Address = $DomainControllerIPv4Address
DomainName = $DomainName
}
if ($VpnClientCertificatePfxPassword) {
$vpnInstallParameters.ClientCertificatePfxPassword = $VpnClientCertificatePfxPassword
}
& $installerPath @vpnInstallParameters | Out-Null
}
$vpnConnection = Connect-SguAzureP2s -ConnectionName $VpnConnectionName
$nrptDisplayName = "SGU Azure P2S DNS - $DomainName"
$nrptRule = Get-DnsClientNrptRule -ErrorAction SilentlyContinue |
Where-Object DisplayName -eq $nrptDisplayName |
Select-Object -First 1
if (-not $nrptRule -or
@($nrptRule.NameServers) -notcontains $DomainControllerIPv4Address.IPAddressToString) {
throw "The SGU NRPT rule for $DomainName is missing or does not point to $DomainControllerIPv4Address. Re-run Install-SguAzureP2sClient.ps1."
}
$NetworkInterfaceAlias = $vpnConnection.Name
}
else {
$NetworkInterfaceAlias = Resolve-ClientInterfaceAlias -RequestedAlias $NetworkInterfaceAlias
Set-DnsClientServerAddress -InterfaceAlias $NetworkInterfaceAlias `
-ServerAddresses $DomainControllerIPv4Address.IPAddressToString
}
if (-not (Test-TcpPort -Address $DomainControllerIPv4Address -Port 5985)) {
throw "The domain controller at $DomainControllerIPv4Address is not accepting WinRM on TCP 5985. Run the server bootstrap first and verify the selected IP."
@@ -292,6 +367,7 @@ try {
ComputerOuDn = $ComputerOuDn
NetworkInterfaceAlias = $NetworkInterfaceAlias
DomainDnsServerAddresses = @($DomainControllerIPv4Address.IPAddressToString)
ConnectivityMode = $ConnectivityMode
RemoteDesktopPrincipal = "$DomainNetbios\SG-Laboratorio-Usuarios-RDP"
DotNetRuntimeInstallerPath = $runtimeInstaller.FullName
RustDeskServerAddress = $serverIdentity.RustDeskServerAddress
@@ -356,6 +432,7 @@ finally {
}
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
$DomainCredential = $null
$VpnClientCertificatePfxPassword = $null
}
if ($SkipRestart) {
@@ -365,6 +442,8 @@ if ($SkipRestart) {
ProviderInstalled = $true
ClientCertificateRegistered = $true
BrokerEndpoint = $brokerEndpoint
ConnectivityMode = $ConnectivityMode
VpnConnectionName = if ($ConnectivityMode -eq 'AzureP2S') { $VpnConnectionName } else { $null }
RestartRequired = $true
RustDesk = if ($result) { $result.RustDesk } else { $null }
EnrollmentResult = $result
+89
View File
@@ -0,0 +1,89 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidatePattern('^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$')]
[string]$ClientName,
[string]$OutputDirectory = (Join-Path $PSScriptRoot '..\artifacts\azure-p2s'),
[securestring]$ClientPfxPassword,
[string]$RootSubject = 'CN=SGU Azure P2S Root',
[ValidateRange(1, 10)]
[int]$ClientValidityYears = 2,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
$resolvedOutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
New-Item -ItemType Directory -Path $resolvedOutputDirectory -Force | Out-Null
$rootCertificatePath = Join-Path $resolvedOutputDirectory 'sgu-azure-p2s-root.cer'
$clientCertificatePath = Join-Path $resolvedOutputDirectory "sgu-azure-p2s-$ClientName.pfx"
if ((Test-Path -LiteralPath $clientCertificatePath -PathType Leaf) -and -not $Force) {
throw "$clientCertificatePath already exists. Use -Force only when you intend to replace that exported client credential."
}
if (-not $ClientPfxPassword) {
$ClientPfxPassword = Read-Host 'Password that will protect the exported P2S client certificate' -AsSecureString
}
$rootCertificate = Get-ChildItem Cert:\CurrentUser\My |
Where-Object {
$_.Subject -eq $RootSubject -and
$_.HasPrivateKey -and
$_.NotAfter -gt (Get-Date).AddYears($ClientValidityYears)
} |
Sort-Object NotAfter -Descending |
Select-Object -First 1
if (-not $rootCertificate) {
if (-not $PSCmdlet.ShouldProcess($RootSubject, 'Create a non-exportable Azure P2S root certificate authority')) {
return
}
$rootCertificate = New-SelfSignedCertificate `
-Type Custom `
-Subject $RootSubject `
-CertStoreLocation Cert:\CurrentUser\My `
-KeyAlgorithm RSA `
-KeyLength 4096 `
-HashAlgorithm SHA256 `
-KeySpec Signature `
-KeyExportPolicy NonExportable `
-KeyUsage CertSign,CRLSign,DigitalSignature `
-NotAfter (Get-Date).AddYears(10) `
-TextExtension @('2.5.29.19={critical}{text}ca=1&pathlength=1')
}
if (-not $PSCmdlet.ShouldProcess($ClientName, 'Issue and export an Azure P2S machine certificate')) {
return
}
$clientSubject = "CN=SGU Azure P2S $ClientName"
$clientCertificate = New-SelfSignedCertificate `
-Type Custom `
-Subject $clientSubject `
-DnsName "sgu-p2s-$ClientName" `
-Signer $rootCertificate `
-CertStoreLocation Cert:\CurrentUser\My `
-KeyAlgorithm RSA `
-KeyLength 3072 `
-HashAlgorithm SHA256 `
-KeySpec Signature `
-KeyExportPolicy Exportable `
-KeyUsage DigitalSignature `
-NotAfter (Get-Date).AddYears($ClientValidityYears) `
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.2')
Export-Certificate -Cert $rootCertificate -FilePath $rootCertificatePath -Force | Out-Null
Export-PfxCertificate -Cert $clientCertificate -FilePath $clientCertificatePath `
-Password $ClientPfxPassword -ChainOption BuildChain -CryptoAlgorithmOption AES256_SHA256 `
-Force | Out-Null
[pscustomobject]@{
RootCertificatePath = $rootCertificatePath
RootCertificateThumbprint = $rootCertificate.Thumbprint
RootCertificateData = [Convert]::ToBase64String($rootCertificate.RawData)
ClientName = $ClientName
ClientCertificatePath = $clientCertificatePath
ClientCertificateThumbprint = $clientCertificate.Thumbprint
ClientCertificateExpires = $clientCertificate.NotAfter
RootPrivateKeyExportable = $false
}
+34 -2
View File
@@ -81,16 +81,18 @@ New-Item -ItemType Directory -Path $resolvedOutputRoot -Force | Out-Null
$clientRoot = Join-Path $resolvedOutputRoot "sgu-client-bootstrap-$Version"
$serverRoot = Join-Path $resolvedOutputRoot "sgu-server-bootstrap-$Version"
$linuxClientRoot = Join-Path $resolvedOutputRoot "sgu-linux-client-bootstrap-$Version"
$azureRoot = Join-Path $resolvedOutputRoot "sgu-azure-infrastructure-$Version"
$clientZip = "$clientRoot.zip"
$serverZip = "$serverRoot.zip"
$linuxClientZip = "$linuxClientRoot.zip"
foreach ($target in @($clientRoot,$serverRoot,$linuxClientRoot,$clientZip,$serverZip,$linuxClientZip)) {
$azureZip = "$azureRoot.zip"
foreach ($target in @($clientRoot,$serverRoot,$linuxClientRoot,$azureRoot,$clientZip,$serverZip,$linuxClientZip,$azureZip)) {
if (Test-Path -LiteralPath $target) {
throw "Release target already exists: $target"
}
}
New-Item -ItemType Directory -Path $clientRoot,$serverRoot,$linuxClientRoot -Force | Out-Null
New-Item -ItemType Directory -Path $clientRoot,$serverRoot,$linuxClientRoot,$azureRoot -Force | Out-Null
$welcomeFontNames = @(
'IndivisaTextSans-Regular.otf',
'IndivisaTextSans-Bold.otf',
@@ -103,6 +105,10 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Invoke-SguClientBootstrap.ps
-Destination (Join-Path $clientRoot 'Invoke-SguClientBootstrap.ps1')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguClientEnrollment.cmd') `
-Destination (Join-Path $clientRoot 'Start-SguClientEnrollment.cmd')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureClientEnrollment.cmd') `
-Destination (Join-Path $clientRoot 'Start-SguAzureClientEnrollment.cmd')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Install-SguAzureP2sClient.ps1') `
-Destination (Join-Path $clientRoot 'Install-SguAzureP2sClient.ps1')
$clientScripts = @(
'Enable-LabRemoteAccess.ps1',
'Enable-SguClientMonitoring.ps1',
@@ -163,6 +169,8 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Initialize-SguDomainControll
-Destination (Join-Path $serverRoot 'Initialize-SguDomainController.ps1')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguServerBootstrap.cmd') `
-Destination (Join-Path $serverRoot 'Start-SguServerBootstrap.cmd')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureServerBootstrap.cmd') `
-Destination (Join-Path $serverRoot 'Start-SguAzureServerBootstrap.cmd')
$serverScripts = @(
'Deploy-AuthBroker.ps1',
'Enable-SguServerRemoteManagement.ps1',
@@ -211,10 +219,32 @@ Write-PackageManifest -PackageRoot $serverRoot -PackageVersion $Version -Package
Compress-Archive -Path (Join-Path $serverRoot '*') -DestinationPath $serverZip `
-CompressionLevel Optimal
# Azure infrastructure is packaged separately because it runs on the trusted
# administrator workstation, not inside the domain controller or a client.
$azureScriptsRoot = Join-Path $azureRoot 'scripts'
$azureInfrastructureRoot = Join-Path $azureRoot 'infra\azure'
New-Item -ItemType Directory -Path $azureScriptsRoot,$azureInfrastructureRoot -Force | Out-Null
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'infra\azure\main.bicep') `
-Destination (Join-Path $azureInfrastructureRoot 'main.bicep')
foreach ($scriptName in @(
'New-SguAzureP2sCertificates.ps1',
'Deploy-SguAzureInfrastructure.ps1',
'Get-SguAzureP2sPackage.ps1',
'Install-SguAzureP2sClient.ps1')) {
Copy-RequiredFile -Source (Join-Path $PSScriptRoot $scriptName) `
-Destination (Join-Path $azureScriptsRoot $scriptName)
}
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\azure-vpn-deployment.md') `
-Destination (Join-Path $azureRoot 'README.md')
Write-PackageManifest -PackageRoot $azureRoot -PackageVersion $Version -PackageKind AzureInfrastructure
Compress-Archive -Path (Join-Path $azureRoot '*') -DestinationPath $azureZip `
-CompressionLevel Optimal
$checksums = @(
("{0} {1}" -f (Get-FileHash -LiteralPath $clientZip -Algorithm SHA256).Hash, (Split-Path $clientZip -Leaf))
("{0} {1}" -f (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash, (Split-Path $serverZip -Leaf))
("{0} {1}" -f (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash, (Split-Path $linuxClientZip -Leaf))
("{0} {1}" -f (Get-FileHash -LiteralPath $azureZip -Algorithm SHA256).Hash, (Split-Path $azureZip -Leaf))
)
$checksumsPath = Join-Path $resolvedOutputRoot "SHA256SUMS-$Version.txt"
[IO.File]::WriteAllLines($checksumsPath, $checksums, [Text.UTF8Encoding]::new($false))
@@ -227,6 +257,8 @@ $checksumsPath = Join-Path $resolvedOutputRoot "SHA256SUMS-$Version.txt"
LinuxClientSha256 = (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash
ServerPackage = $serverZip
ServerSha256 = (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash
AzureInfrastructurePackage = $azureZip
AzureInfrastructureSha256 = (Get-FileHash -LiteralPath $azureZip -Algorithm SHA256).Hash
Checksums = $checksumsPath
RuntimeInstaller = $runtimeInstaller.Name
}
+4
View File
@@ -18,6 +18,7 @@ $assetPaths = @(
(Join-Path $ReleaseDirectory "sgu-client-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-server-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-linux-client-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-azure-infrastructure-$Version.zip"),
(Join-Path $ReleaseDirectory "SHA256SUMS-$Version.txt")
)
foreach ($assetPath in $assetPaths) {
@@ -112,6 +113,9 @@ Bootstrap reproducible para el laboratorio SGU.
- `sgu-server-bootstrap-$Version.zip`: crea el bosque AD/DNS, OUs, grupo RDP, GPO, recurso `Packages`, broker mTLS y administración remota; se reanuda solo después del reinicio.
- `sgu-client-bootstrap-$Version.zip`: registra un certificado mTLS único, instala y valida el Credential Provider antes de unir el equipo al dominio, habilita RDP/WinRM y se repara al arranque.
- `sgu-linux-client-bootstrap-$Version.zip`: une clientes Debian/Ubuntu o RHEL/Fedora/Rocky/AlmaLinux con realmd, Kerberos y SSSD. Solicita interactivamente la contraseña de unión y no instala el Credential Provider de Windows.
- `sgu-azure-infrastructure-$Version.zip`: despliega mediante Bicep una VM Windows Server 2025, red privada, IP pública protegida por NSG y Azure VPN Gateway P2S; también genera certificados por equipo y descarga el perfil de cliente.
- El bootstrap Azure conserva la IP privada administrada por la NIC de Azure, autoriza el pool P2S en los firewalls SGU y nunca publica LDAP, Kerberos, SMB, RPC, WinRM ni el Auth Broker directamente a Internet.
- Los Windows 11 Pro pueden instalar un perfil IKEv2 de todos los usuarios con certificado de máquina, DNS dividido para `lci.lasalle.mx` y ejecutarlo desde la pantalla de inicio de sesión antes de autenticar una cuenta de dominio nueva.
- El Auth Broker clasifica sin tareas programadas cada cuenta autenticada: `AL` se agrega a `SGU-Alumnos`, `AD` a `SGU-Administrativos` y `DO` a `SGU-Docentes`; el bootstrap crea estos grupos de seguridad de forma idempotente.
- El enriquecimiento obtiene el sexo de los módulos SGU de personal/alumnos, lo conserva como la línea administrada `SGU-Gender: Male|Female` en Notas de AD y adapta el fondo de Windows/Linux; cuando falta utiliza redacción neutral.
- El servidor configura WEF/WEC para registrar sesiones y fallos, inventariar el estado alcanzable de las máquinas cada cinco minutos y conservar durante 183 días tanto esos eventos como el diagnóstico estructurado del Auth Broker.
@@ -0,0 +1,8 @@
@echo off
setlocal
set "SGU_BOOTSTRAP_IP=%~1"
set "SGU_VPN_PACKAGE=%~2"
set "SGU_VPN_PFX=%~3"
set "SGU_VPN_ROOT=%~4"
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Invoke-SguClientBootstrap.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-ConnectivityMode','AzureP2S'); if ($env:SGU_BOOTSTRAP_IP) { $arguments += @('-DomainControllerIPv4Address',('"' + $env:SGU_BOOTSTRAP_IP + '"')) }; if ($env:SGU_VPN_PACKAGE) { $arguments += @('-VpnProfilePackagePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PACKAGE) + '"')) }; if ($env:SGU_VPN_PFX) { $arguments += @('-VpnClientCertificatePfxPath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PFX) + '"')) }; if ($env:SGU_VPN_ROOT) { $arguments += @('-VpnClientRootCertificatePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_ROOT) + '"')) }; $process = Start-Process -FilePath powershell.exe -Verb RunAs -ArgumentList $arguments -Wait -PassThru; exit $process.ExitCode"
exit /b %errorlevel%
@@ -0,0 +1,7 @@
@echo off
setlocal
set "SGU_BOOTSTRAP_IP=%~1"
set "SGU_VPN_POOL=%~2"
if "%SGU_VPN_POOL%"=="" set "SGU_VPN_POOL=172.30.0.0/24"
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Initialize-SguDomainController.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-NetworkConfigurationMode','PlatformManaged','-TrustedClientNetworks',$env:SGU_VPN_POOL,'-DnsForwarders','168.63.129.16'); if ($env:SGU_BOOTSTRAP_IP) { $arguments += @('-ServerIPv4Address',$env:SGU_BOOTSTRAP_IP) }; $process = Start-Process -FilePath powershell.exe -Verb RunAs -ArgumentList $arguments -Wait -PassThru; exit $process.ExitCode"
exit /b %errorlevel%