#Requires -Version 5.1 [CmdletBinding(SupportsShouldProcess)] param( [ipaddress]$ServerIPv4Address, [ValidateRange(1, 32)] [int]$PrefixLength = 24, [string]$NetworkInterfaceAlias, [ipaddress]$DefaultGateway, [ipaddress[]]$DnsForwarders = @(), [string]$DomainName = 'lci.lasalle.mx', [string]$DomainNetbios = 'LCI', [string]$BrokerRecordName = 'sgu-auth', [string]$PackageSharePath = 'C:\Packages', [securestring]$SafeModeAdministratorPassword, [switch]$SkipRestart, [switch]$Resume ) $ErrorActionPreference = 'Stop' $bootstrapRoot = Join-Path $env:ProgramData 'SGU\Bootstrap\Server' $statePath = Join-Path $bootstrapRoot 'bootstrap-state.json' $completionPath = Join-Path $bootstrapRoot 'bootstrap-complete.json' $logPath = Join-Path $bootstrapRoot 'bootstrap.log' $taskName = 'SGU-Complete-Domain-Controller-Bootstrap' function Write-BootstrapLog { param([Parameter(Mandatory)][string]$Message) $line = '{0:u} {1}' -f (Get-Date), $Message Write-Host $line if (Test-Path -LiteralPath $bootstrapRoot) { Add-Content -LiteralPath $logPath -Value $line -Encoding UTF8 } } function Assert-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this bootstrap from an elevated Windows PowerShell session.' } } function Assert-PackageManifest { param([Parameter(Mandatory)][string]$PackageRoot) $manifestPath = Join-Path $PackageRoot 'package-manifest.json' if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw 'package-manifest.json is missing. Use the complete SGU server bootstrap release.' } $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json foreach ($entry in $manifest.Files) { $path = Join-Path $PackageRoot ([string]$entry.Path) if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Bootstrap package file is missing: $($entry.Path)" } $actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash if ($actual -ne [string]$entry.Sha256) { throw "Bootstrap package integrity check failed: $($entry.Path)" } } } function Get-DomainBaseDn { param([Parameter(Mandatory)][string]$DnsDomainName) return (($DnsDomainName -split '\.') | ForEach-Object { "DC=$_" }) -join ',' } function Resolve-PrivateInterfaceAlias { param([string]$RequestedAlias) if ($RequestedAlias) { Get-NetAdapter -Name $RequestedAlias -ErrorAction Stop | Out-Null return $RequestedAlias } $upAdapters = @(Get-NetAdapter | Where-Object Status -eq 'Up') $withoutGateway = @($upAdapters | Where-Object { -not (Get-NetIPConfiguration -InterfaceIndex $_.ifIndex).IPv4DefaultGateway }) if ($withoutGateway.Count -eq 1) { return [string]$withoutGateway[0].Name } if ($upAdapters.Count -eq 1) { return [string]$upAdapters[0].Name } $aliases = ($upAdapters.Name | Sort-Object) -join ', ' throw "Could not select the private domain adapter unambiguously. Re-run with -NetworkInterfaceAlias. Available adapters: $aliases" } function Set-StaticDomainAddress { param( [Parameter(Mandatory)][string]$InterfaceAlias, [Parameter(Mandatory)][ipaddress]$Address, [Parameter(Mandatory)][int]$NetworkPrefixLength, [ipaddress]$Gateway ) $adapter = Get-NetAdapter -Name $InterfaceAlias -ErrorAction Stop Set-NetIPInterface -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 -Dhcp Disabled $addresses = @(Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object PrefixOrigin -ne 'WellKnown') foreach ($existingAddress in $addresses) { if ($existingAddress.IPAddress -ne $Address.IPAddressToString -or [int]$existingAddress.PrefixLength -ne $NetworkPrefixLength) { Remove-NetIPAddress -InputObject $existingAddress -Confirm:$false } } $matchingAddress = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 ` -IPAddress $Address.IPAddressToString -ErrorAction SilentlyContinue if (-not $matchingAddress) { $addressParameters = @{ InterfaceIndex = $adapter.ifIndex IPAddress = $Address.IPAddressToString PrefixLength = $NetworkPrefixLength AddressFamily = 'IPv4' } if ($Gateway) { $addressParameters.DefaultGateway = $Gateway.IPAddressToString } New-NetIPAddress @addressParameters | Out-Null } if ($Gateway) { $defaultRoutes = @(Get-NetRoute -InterfaceIndex $adapter.ifIndex ` -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue) foreach ($route in $defaultRoutes) { if ($route.NextHop -ne $Gateway.IPAddressToString) { Remove-NetRoute -InputObject $route -Confirm:$false } } if (-not (Get-NetRoute -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 ` -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Where-Object NextHop -eq $Gateway.IPAddressToString)) { New-NetRoute -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 ` -DestinationPrefix '0.0.0.0/0' -NextHop $Gateway.IPAddressToString | Out-Null } } Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex ` -ServerAddresses $Address.IPAddressToString } function Register-ResumeTask { param([Parameter(Mandatory)][string]$ScriptPath) $powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" $action = New-ScheduledTaskAction -Execute $powerShell ` -Argument "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$ScriptPath`" -Resume" $trigger = New-ScheduledTaskTrigger -AtStartup $trigger.Delay = 'PT1M' $settings = New-ScheduledTaskSettingsSet ` -StartWhenAvailable ` -ExecutionTimeLimit (New-TimeSpan -Minutes 30) ` -RestartCount 3 ` -RestartInterval (New-TimeSpan -Minutes 2) Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` -Settings $settings -User 'SYSTEM' -RunLevel Highest -Force | Out-Null } function Ensure-OrganizationalUnit { param( [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$Server ) $distinguishedName = "OU=$Name,$Path" $escapedName = $Name.Replace('\', '\5c').Replace('*', '\2a').Replace('(', '\28').Replace(')', '\29') $existing = Get-ADOrganizationalUnit -LDAPFilter "(ou=$escapedName)" ` -SearchBase $Path -SearchScope OneLevel -Server $Server -ErrorAction Stop | Select-Object -First 1 if (-not $existing) { New-ADOrganizationalUnit -Name $Name -Path $Path ` -ProtectedFromAccidentalDeletion $true -Server $Server | Out-Null } return $distinguishedName } function Wait-ActiveDirectoryReady { param( [Parameter(Mandatory)][string]$ExpectedBaseDn, [ValidateRange(1, 120)][int]$Attempts = 36, [ValidateRange(1, 30)][int]$DelaySeconds = 5 ) for ($attempt = 1; $attempt -le $Attempts; $attempt++) { try { $rootDse = Get-ADRootDSE -Server localhost -ErrorAction Stop if ($rootDse.DefaultNamingContext -eq $ExpectedBaseDn) { return } } catch { if ($attempt -eq $Attempts) { throw } } Start-Sleep -Seconds $DelaySeconds } throw "Active Directory did not publish $ExpectedBaseDn before the readiness timeout." } function Wait-DnsZoneReady { param( [Parameter(Mandatory)][string]$ZoneName, [Parameter(Mandatory)][ipaddress]$DnsServer, [ValidateRange(1, 120)][int]$Attempts = 30, [ValidateRange(1, 30)][int]$DelaySeconds = 2 ) for ($attempt = 1; $attempt -le $Attempts; $attempt++) { try { $soa = @(Resolve-DnsName $ZoneName -Type SOA -DnsOnly ` -Server $DnsServer.IPAddressToString -ErrorAction Stop | Where-Object Type -eq SOA) if ($soa.Count -gt 0) { return } } catch { if ($attempt -eq $Attempts) { throw } } Start-Sleep -Seconds $DelaySeconds } throw "DNS did not load the $ZoneName zone before the readiness timeout." } function Set-PackageShare { param( [Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$NetbiosName, [Parameter(Mandatory)][string]$DomainSid ) New-Item -ItemType Directory -Path $Path -Force | Out-Null $domainAdminsSid = [Security.Principal.SecurityIdentifier]::new("$DomainSid-512") $domainUsersSid = [Security.Principal.SecurityIdentifier]::new("$DomainSid-513") $domainComputersSid = [Security.Principal.SecurityIdentifier]::new("$DomainSid-515") $systemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18') $acl = New-Object Security.AccessControl.DirectorySecurity $acl.SetAccessRuleProtection($true, $false) $inheritance = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit' $propagation = [Security.AccessControl.PropagationFlags]::None $allow = [Security.AccessControl.AccessControlType]::Allow $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( $systemSid, [Security.AccessControl.FileSystemRights]::FullControl, $inheritance, $propagation, $allow)) $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( $domainAdminsSid, [Security.AccessControl.FileSystemRights]::FullControl, $inheritance, $propagation, $allow)) $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( $domainComputersSid, [Security.AccessControl.FileSystemRights]'ReadAndExecute, Synchronize', $inheritance, $propagation, $allow)) $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( $domainUsersSid, [Security.AccessControl.FileSystemRights]'ReadAndExecute, Synchronize', $inheritance, $propagation, $allow)) Set-Acl -LiteralPath $Path -AclObject $acl $domainAdmins = $domainAdminsSid.Translate([Security.Principal.NTAccount]).Value $domainUsers = $domainUsersSid.Translate([Security.Principal.NTAccount]).Value $domainComputers = $domainComputersSid.Translate([Security.Principal.NTAccount]).Value $share = Get-SmbShare -Name Packages -ErrorAction SilentlyContinue if ($share -and $share.Path -ne $Path) { throw "The existing Packages share points to $($share.Path), not $Path." } if (-not $share) { New-SmbShare -Name Packages -Path $Path -FullAccess $domainAdmins ` -ReadAccess $domainComputers,$domainUsers -FolderEnumerationMode AccessBased | Out-Null } else { Grant-SmbShareAccess -Name Packages -AccountName $domainAdmins ` -AccessRight Full -Force | Out-Null Grant-SmbShareAccess -Name Packages -AccountName $domainComputers ` -AccessRight Read -Force | Out-Null Grant-SmbShareAccess -Name Packages -AccountName $domainUsers ` -AccessRight Read -Force | Out-Null } } Assert-Administrator trap { Write-BootstrapLog ("ERROR: " + $_.Exception.Message) throw } $operatingSystem = Get-CimInstance Win32_OperatingSystem if ([int]$operatingSystem.ProductType -eq 1) { throw 'The domain controller bootstrap requires Windows Server, not a Windows client edition.' } $existingState = $null if (Test-Path -LiteralPath $statePath -PathType Leaf) { $existingState = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json } if ($Resume -or (-not $ServerIPv4Address -and $existingState)) { if (-not $existingState) { throw 'The persisted bootstrap state is missing; start the server bootstrap normally.' } $ServerIPv4Address = [ipaddress][string]$existingState.ServerIPv4Address $PrefixLength = [int]$existingState.PrefixLength $NetworkInterfaceAlias = [string]$existingState.NetworkInterfaceAlias $DefaultGateway = if ($existingState.DefaultGateway) { [ipaddress][string]$existingState.DefaultGateway } else { $null } $DnsForwarders = @($existingState.DnsForwarders | ForEach-Object { [ipaddress][string]$_ }) $DomainName = [string]$existingState.DomainName $DomainNetbios = [string]$existingState.DomainNetbios $BrokerRecordName = [string]$existingState.BrokerRecordName $PackageSharePath = [string]$existingState.PackageSharePath } if (-not $ServerIPv4Address) { $ServerIPv4Address = [ipaddress](Read-Host 'Fixed IPv4 address for this domain controller') } $sourceRoot = $PSScriptRoot if (-not $Resume) { Assert-PackageManifest -PackageRoot $sourceRoot New-Item -ItemType Directory -Path $bootstrapRoot -Force | Out-Null if ((Resolve-Path -LiteralPath $sourceRoot).Path -ne (Resolve-Path -LiteralPath $bootstrapRoot).Path) { Copy-Item -Path (Join-Path $sourceRoot '*') -Destination $bootstrapRoot -Recurse -Force } Assert-PackageManifest -PackageRoot $bootstrapRoot } else { Assert-PackageManifest -PackageRoot $bootstrapRoot } $NetworkInterfaceAlias = Resolve-PrivateInterfaceAlias -RequestedAlias $NetworkInterfaceAlias $baseDn = Get-DomainBaseDn -DnsDomainName $DomainName $brokerDnsName = "$BrokerRecordName.$DomainName" $stagedScriptPath = Join-Path $bootstrapRoot 'Initialize-SguDomainController.ps1' $scriptsRoot = Join-Path $bootstrapRoot 'payload\scripts' $brokerPublishPath = Join-Path $bootstrapRoot 'payload\broker' foreach ($requiredPath in @( $stagedScriptPath, (Join-Path $scriptsRoot 'Deploy-AuthBroker.ps1'), (Join-Path $scriptsRoot 'New-LabCertificate.ps1'), (Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1'), (Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1'), (Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1'), (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1'), (Join-Path $scriptsRoot 'Install-SguDomainMonitoring.ps1'), (Join-Path $scriptsRoot 'Invoke-SguMonitoringMaintenance.ps1'), (Join-Path $scriptsRoot 'Get-SguUsageReport.ps1'), (Join-Path $scriptsRoot 'Get-SguBrokerLog.ps1'), (Join-Path $brokerPublishPath 'SGU.AuthBroker.exe'))) { if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) { throw "The server bootstrap package is incomplete: $requiredPath" } } if (-not $existingState) { if ($DnsForwarders.Count -eq 0) { $DnsForwarders = @(Get-DnsClientServerAddress -AddressFamily IPv4 | Where-Object InterfaceAlias -ne $NetworkInterfaceAlias | Select-Object -ExpandProperty ServerAddresses | Where-Object { $_ -and $_ -ne $ServerIPv4Address.IPAddressToString } | ForEach-Object { [ipaddress]$_ } | Select-Object -Unique) } $existingState = [ordered]@{ Phase = 'Promote' ServerIPv4Address = $ServerIPv4Address.IPAddressToString PrefixLength = $PrefixLength NetworkInterfaceAlias = $NetworkInterfaceAlias DefaultGateway = if ($DefaultGateway) { $DefaultGateway.IPAddressToString } else { $null } DnsForwarders = @($DnsForwarders | ForEach-Object IPAddressToString) DomainName = $DomainName DomainNetbios = $DomainNetbios BrokerRecordName = $BrokerRecordName PackageSharePath = $PackageSharePath } [IO.File]::WriteAllText( $statePath, ($existingState | ConvertTo-Json -Depth 4), [Text.UTF8Encoding]::new($false)) } 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) { if ([string]$existingState.Phase -eq 'Finalize') { throw 'Active Directory promotion completed but Windows has not restarted. Restart the server to continue automatically.' } if (-not $SafeModeAdministratorPassword) { $SafeModeAdministratorPassword = Read-Host ` 'Directory Services Restore Mode password (not stored)' -AsSecureString } Write-BootstrapLog 'Installing Active Directory Domain Services, DNS, and management tools.' Install-WindowsFeature AD-Domain-Services,DNS,GPMC,RSAT-AD-Tools ` -IncludeManagementTools | Out-Null Register-ResumeTask -ScriptPath $stagedScriptPath Write-BootstrapLog "Creating the $DomainName forest. Windows must restart once." Install-ADDSForest ` -DomainName $DomainName ` -DomainNetbiosName $DomainNetbios ` -InstallDns ` -SafeModeAdministratorPassword $SafeModeAdministratorPassword ` -NoRebootOnCompletion ` -Force | Out-Null $existingState.Phase = 'Finalize' [IO.File]::WriteAllText( $statePath, ($existingState | ConvertTo-Json -Depth 4), [Text.UTF8Encoding]::new($false)) if ($SkipRestart) { Write-BootstrapLog 'Promotion succeeded. Restart manually; finalization will resume at startup.' return [pscustomobject]@{ Phase = 'AwaitingRestart' DomainName = $DomainName ServerIPv4Address = $ServerIPv4Address.IPAddressToString ResumeTask = $taskName } } Restart-Computer -Force return } if (-not $computer.Domain.Equals($DomainName, [StringComparison]::OrdinalIgnoreCase)) { throw "This server belongs to $($computer.Domain), not $DomainName." } Write-BootstrapLog 'Finalizing Active Directory, DNS, policies, broker, shares, and remote management.' # Once the machine is a DC, every active adapter must query the local DNS # service. Only the private domain adapter may publish its address in the AD # zone; otherwise clients can receive the DHCP/NAT address of the Internet NIC. Get-NetAdapter | Where-Object Status -eq 'Up' | ForEach-Object { Set-DnsClientServerAddress -InterfaceIndex $_.ifIndex ` -ServerAddresses $ServerIPv4Address.IPAddressToString Set-DnsClient -InterfaceIndex $_.ifIndex ` -RegisterThisConnectionsAddress:($_.Name -eq $NetworkInterfaceAlias) } Clear-DnsClientCache Register-DnsClient Import-Module ActiveDirectory -ErrorAction Stop Wait-ActiveDirectoryReady -ExpectedBaseDn $baseDn # A newly promoted Windows Server 2025 DC can retain the Public firewall # profile because network identification ran before local DNS and LDAP were # ready. A private-adapter bounce triggers the supported domain-detection path. $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 for ($attempt = 1; $attempt -le 15; $attempt++) { Start-Sleep -Seconds 2 $domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias ` -ErrorAction SilentlyContinue if ($domainProfile -and $domainProfile.NetworkCategory -eq 'DomainAuthenticated') { break } } } # Apply the single-address DNS listener only after any adapter refresh. That # avoids transient DNS socket errors while the private address is momentarily # unavailable, while still preventing the Internet/NAT address from being # published once finalization completes. New-ItemProperty ` -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\DNS\Parameters' ` -Name PublishAddresses ` -PropertyType String ` -Value $ServerIPv4Address.IPAddressToString ` -Force | Out-Null $dnsServerSetting = Get-DnsServerSetting -All -WarningAction SilentlyContinue $dnsServerSetting.ListeningIPAddress = @($ServerIPv4Address) Set-DnsServerSetting -InputObject $dnsServerSetting -WarningAction SilentlyContinue | Out-Null Restart-Service DNS -Force Wait-DnsZoneReady -ZoneName $DomainName -DnsServer $ServerIPv4Address $adServer = 'localhost' $domain = Get-ADDomain -Identity $DomainName -Server $adServer $laboratoryOuDn = Ensure-OrganizationalUnit -Name 'Laboratorio' -Path $baseDn -Server $adServer $usersOuDn = Ensure-OrganizationalUnit -Name 'Usuarios-SGU' -Path $baseDn -Server $adServer foreach ($ouName in @('Docentes', 'Alumnos', 'Administrativos')) { Ensure-OrganizationalUnit -Name $ouName -Path $usersOuDn -Server $adServer | Out-Null } $remoteDesktopGroupName = 'SG-Laboratorio-Usuarios-RDP' $remoteDesktopGroup = Get-ADGroup -LDAPFilter "(sAMAccountName=$remoteDesktopGroupName)" ` -SearchBase $baseDn -SearchScope Subtree -Server $adServer ` -ErrorAction SilentlyContinue if (-not $remoteDesktopGroup) { New-ADGroup -Name $remoteDesktopGroupName -SamAccountName $remoteDesktopGroupName ` -GroupCategory Security -GroupScope Global -Path $laboratoryOuDn ` -Description 'SGU users permitted to use Remote Desktop on laboratory clients.' ` -Server $adServer | Out-Null $remoteDesktopGroup = Get-ADGroup -LDAPFilter "(sAMAccountName=$remoteDesktopGroupName)" ` -SearchBase $laboratoryOuDn -SearchScope OneLevel -Server $adServer } & (Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1') ` -ZoneName $DomainName ` -RecordName $BrokerRecordName ` -IPv4Address $ServerIPv4Address ` -ExternalForwarders $DnsForwarders | Out-Null $certificateDirectory = Join-Path $bootstrapRoot 'certificates' $serverCertificate = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -eq "CN=$brokerDnsName" -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date).AddDays(30) } | Sort-Object NotAfter -Descending | Select-Object -First 1 if (-not $serverCertificate) { $certificateResult = & (Join-Path $scriptsRoot 'New-LabCertificate.ps1') ` -Role BrokerServer ` -BrokerDnsName $brokerDnsName ` -OutputDirectory $certificateDirectory $serverCertificate = Get-ChildItem Cert:\LocalMachine\My | Where-Object Thumbprint -eq $certificateResult.Thumbprint | Select-Object -First 1 } else { New-Item -ItemType Directory -Path $certificateDirectory -Force | Out-Null $publicCertificatePath = Join-Path $certificateDirectory 'sgu-auth-broker.cer' Export-Certificate -Cert $serverCertificate -FilePath $publicCertificatePath -Force | Out-Null if (-not (Get-ChildItem Cert:\LocalMachine\Root | Where-Object Thumbprint -eq $serverCertificate.Thumbprint)) { Import-Certificate -FilePath $publicCertificatePath ` -CertStoreLocation Cert:\LocalMachine\Root | Out-Null } } $allowedClientThumbprints = @() $brokerConfigurationPath = 'C:\Program Files\SGU\AuthBroker\appsettings.Production.json' if (Test-Path -LiteralPath $brokerConfigurationPath -PathType Leaf) { $priorConfiguration = Get-Content -LiteralPath $brokerConfigurationPath -Raw | ConvertFrom-Json $allowedClientThumbprints = @($priorConfiguration.Broker.Tls.AllowedClientThumbprints) } & (Join-Path $scriptsRoot 'Deploy-AuthBroker.ps1') ` -PublishPath $brokerPublishPath ` -ServerCertificateSubject $brokerDnsName ` -AllowedClientThumbprints $allowedClientThumbprints ` -LdapHost $adServer ` -BaseDn $baseDn ` -DomainNetbios $DomainNetbios ` -UpnSuffix $DomainName ` -RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName ` -DefaultCompany 'La Salle' ` -FirewallLocalAddress $ServerIPv4Address ` -FirewallRemoteAddress "$($ServerIPv4Address.IPAddressToString)/$PrefixLength" ` -CreateMissingOus ` -DisableCertificateRevocationCheckForLab | Out-Null # Remove stale A records registered by any non-domain/NAT adapter before its # dynamic DNS registration was disabled. $hostRecords = @(Get-DnsServerResourceRecord -ZoneName $DomainName ` -Name $env:COMPUTERNAME -RRType A -ErrorAction SilentlyContinue) foreach ($hostRecord in $hostRecords) { if ($hostRecord.RecordData.IPv4Address.IPAddressToString -ne $ServerIPv4Address.IPAddressToString) { Remove-DnsServerResourceRecord -ZoneName $DomainName -InputObject $hostRecord -Force } } $privateSubnet = "$($ServerIPv4Address.IPAddressToString)/$PrefixLength" & (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') ` -AllowedRemoteAddress $privateSubnet | Out-Null $contentPath = Join-Path $bootstrapRoot 'payload\server-content\Packages' if (Test-Path -LiteralPath $contentPath -PathType Container) { New-Item -ItemType Directory -Path $PackageSharePath -Force | Out-Null Copy-Item -Path (Join-Path $contentPath '*') -Destination $PackageSharePath -Recurse -Force } Set-PackageShare -Path $PackageSharePath -NetbiosName $DomainNetbios ` -DomainSid $domain.DomainSID.Value $packageFirewallRule = Get-NetFirewallRule -DisplayName 'SGU Bootstrap Packages (SMB)' ` -ErrorAction SilentlyContinue if (-not $packageFirewallRule) { New-NetFirewallRule ` -DisplayName 'SGU Bootstrap Packages (SMB)' ` -Direction Inbound ` -Action Allow ` -Protocol TCP ` -LocalPort 445 ` -LocalAddress $ServerIPv4Address.IPAddressToString ` -RemoteAddress $privateSubnet ` -Profile Any | Out-Null } else { $packageFirewallRule | Set-NetFirewallRule -Enabled True -Profile Any $packageFirewallRule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter ` -LocalAddress $ServerIPv4Address.IPAddressToString ` -RemoteAddress $privateSubnet | Out-Null } $collectorFqdn = "$env:COMPUTERNAME.$DomainName" & (Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1') ` -TargetOuDn $laboratoryOuDn ` -DomainController $env:COMPUTERNAME ` -EventCollectorFqdn $collectorFqdn | Out-Null & (Join-Path $scriptsRoot 'Install-SguDomainMonitoring.ps1') ` -CollectorFqdn $collectorFqdn ` -ComputerOuDn $laboratoryOuDn ` -RetentionDays 183 | Out-Null $userPolicyParameters = @{ TargetOuDn = $usersOuDn DomainController = $env:COMPUTERNAME } $wallpaper = Get-ChildItem -LiteralPath $PackageSharePath -File -ErrorAction SilentlyContinue | Where-Object { $_.BaseName -eq 'wallpaper' -and $_.Extension -in @('.jpg','.jpeg','.png','.bmp') } | Sort-Object Name | Select-Object -First 1 if ($wallpaper) { $userPolicyParameters.WallpaperPath = "\\$env:COMPUTERNAME\Packages\$($wallpaper.Name)" } & (Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1') @userPolicyParameters | Out-Null $validation = [ordered]@{ CompletedAt = (Get-Date).ToString('o') ComputerName = $env:COMPUTERNAME DomainName = $DomainName ServerIPv4Address = $ServerIPv4Address.IPAddressToString BrokerDnsName = $brokerDnsName BrokerCertificateThumbprint = $serverCertificate.Thumbprint BrokerService = (Get-Service SGUAuthBroker).Status.ToString() BrokerPortListening = [bool](Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue) WinRM = (Get-Service WinRM).Status.ToString() RemoteDesktop = (Get-Service TermService).Status.ToString() EventCollector = (Get-Service Wecsvc).Status.ToString() EventSubscription = @(& wecutil.exe enum-subscription) -contains 'SGU-Lab-Monitoring' MonitoringRetentionDays = 183 PackageShare = "\\$env:COMPUTERNAME\Packages" LaboratoryOu = $laboratoryOuDn UsersOu = $usersOuDn RemoteDesktopGroup = $remoteDesktopGroup.DistinguishedName DomainNetworkProfile = [string](Get-NetConnectionProfile ` -InterfaceAlias $NetworkInterfaceAlias -ErrorAction SilentlyContinue).NetworkCategory } if ($validation.BrokerService -ne 'Running' -or -not $validation.BrokerPortListening -or $validation.WinRM -ne 'Running' -or $validation.RemoteDesktop -ne 'Running' -or $validation.EventCollector -ne 'Running' -or -not $validation.EventSubscription) { throw 'Server finalization did not pass service validation. Review bootstrap.log and re-run the bootstrap.' } [IO.File]::WriteAllText( $completionPath, ($validation | ConvertTo-Json -Depth 4), [Text.UTF8Encoding]::new($false)) if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false } Remove-Item -LiteralPath $statePath -Force -ErrorAction SilentlyContinue Write-BootstrapLog 'SGU domain controller bootstrap completed successfully.' [pscustomobject]$validation