#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" $existing = Get-ADOrganizationalUnit -Identity $distinguishedName -Server $Server ` -ErrorAction SilentlyContinue if (-not $existing) { New-ADOrganizationalUnit -Name $Name -Path $Path ` -ProtectedFromAccidentalDeletion $true -Server $Server | Out-Null } return $distinguishedName } 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 $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 $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.' Import-Module ActiveDirectory -ErrorAction Stop $domain = Get-ADDomain -Identity $DomainName -Server $env:COMPUTERNAME $laboratoryOuDn = Ensure-OrganizationalUnit -Name 'Laboratorio' -Path $baseDn -Server $env:COMPUTERNAME $usersOuDn = Ensure-OrganizationalUnit -Name 'Usuarios-SGU' -Path $baseDn -Server $env:COMPUTERNAME foreach ($ouName in @('Docentes', 'Alumnos', 'Administrativos')) { Ensure-OrganizationalUnit -Name $ouName -Path $usersOuDn -Server $env:COMPUTERNAME | Out-Null } $remoteDesktopGroupName = 'SG-Laboratorio-Usuarios-RDP' $remoteDesktopGroup = Get-ADGroup -Identity $remoteDesktopGroupName -Server $env:COMPUTERNAME ` -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 $env:COMPUTERNAME | Out-Null $remoteDesktopGroup = Get-ADGroup -Identity $remoteDesktopGroupName -Server $env:COMPUTERNAME } & (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 $env:COMPUTERNAME ` -BaseDn $baseDn ` -DomainNetbios $DomainNetbios ` -UpnSuffix $DomainName ` -RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName ` -DefaultCompany 'La Salle' ` -CreateMissingOus ` -DisableCertificateRevocationCheckForLab | Out-Null & (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') | 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 & (Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1') ` -TargetOuDn $laboratoryOuDn -DomainController $env:COMPUTERNAME | 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() PackageShare = "\\$env:COMPUTERNAME\Packages" LaboratoryOu = $laboratoryOuDn UsersOu = $usersOuDn RemoteDesktopGroup = $remoteDesktopGroup.DistinguishedName } if ($validation.BrokerService -ne 'Running' -or -not $validation.BrokerPortListening -or $validation.WinRM -ne 'Running' -or $validation.RemoteDesktop -ne 'Running') { 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