Compare commits

...
4 Commits
Author SHA1 Message Date
alexrg ac531db05e Apply dark theme through user policy 2026-09-04 10:11:32 -06:00
alexrg c8572eb8d4 Fix localized enrollment recovery 2026-09-04 09:43:45 -06:00
alexrg 57572c5567 Fix fresh domain controller bootstrap 2026-09-03 17:26:05 -06:00
alexrg 1d7c312a67 Clarify new forest recovery semantics 2026-09-03 16:35:39 -06:00
11 changed files with 341 additions and 53 deletions
+3
View File
@@ -8,6 +8,9 @@ The repository starts from the current
source and adds an SGU-specific provider, an mTLS-protected broker, Active source and adds an SGU-specific provider, an mTLS-protected broker, Active
Directory synchronization, deployment scripts, and tests. Directory synchronization, deployment scripts, and tests.
Ready-to-run bootstrap packages are published on the
[releases page](https://github.lci.ulsa.mx/alexrg/SGU-CredentialProvider/releases).
## Authentication contract ## Authentication contract
1. The Windows tile collects a `DO`, `AL`, or `AD` institutional key and a password. 1. The Windows tile collects a `DO`, `AL`, or `AD` institutional key and a password.
+12 -5
View File
@@ -52,10 +52,17 @@ El proceso crea o configura de forma idempotente:
- GPO de experiencia del equipo y restricciones de sesión SGU; - GPO de experiencia del equipo y restricciones de sesión SGU;
- certificado de servidor no exportable y broker mTLS en TCP 8443; - certificado de servidor no exportable y broker mTLS en TCP 8443;
- recurso `\\SERVIDOR\Packages`, con lectura para Domain Computers; - recurso `\\SERVIDOR\Packages`, con lectura para Domain Computers;
- RDP con NLA, WinRM/PowerShell Remoting y reglas administrativas sólo en el - RDP con NLA, WinRM/PowerShell Remoting y reglas administrativas limitadas a
perfil Domain; la subred privada indicada, incluso si Windows tarda en reconocer el perfil
Domain después de la promoción;
- pantalla, suspensión e hibernación en Nunca. - pantalla, suspensión e hibernación en Nunca.
En un servidor con dos NIC, el bootstrap desactiva el registro DNS de la NIC de
Internet y obliga al servicio DNS a escuchar y publicar únicamente la IP fija
privada. También vuelve a iniciar brevemente esa NIC privada si Windows Server
2025 todavía la clasifica como Public al terminar la promoción. La salida HTTPS
continúa por la NIC que tenga el gateway predeterminado.
El broker arranca con una lista de clientes vacía. Eso no abre el servicio: mTLS El broker arranca con una lista de clientes vacía. Eso no abre el servicio: mTLS
rechaza todos los certificados hasta que el primer cliente registra el suyo. rechaza todos los certificados hasta que el primer cliente registra el suyo.
Los archivos opcionales colocados en `payload\server-content\Packages` al crear Los archivos opcionales colocados en `payload\server-content\Packages` al crear
@@ -129,8 +136,8 @@ validaciones.
Desde el repositorio y con el SDK fijado en `global.json`: Desde el repositorio y con el SDK fijado en `global.json`:
```powershell ```powershell
.\scripts\New-SguBootstrapPackages.ps1 -Version 0.1.0 .\scripts\New-SguBootstrapPackages.ps1 -Version 0.1.1
.\scripts\Publish-GiteaRelease.ps1 -Version 0.1.0 .\scripts\Publish-GiteaRelease.ps1 -Version 0.1.1
``` ```
El segundo comando usa `GITEA_TOKEN` sólo en memoria o, si no está definido, El segundo comando usa `GITEA_TOKEN` sólo en memoria o, si no está definido,
@@ -139,7 +146,7 @@ la línea de comandos. Para empaquetar recursos institucionales adicionales:
```powershell ```powershell
.\scripts\New-SguBootstrapPackages.ps1 ` .\scripts\New-SguBootstrapPackages.ps1 `
-Version 0.1.0 ` -Version 0.1.1 `
-ServerContentPath C:\Preparacion\Packages -ServerContentPath C:\Preparacion\Packages
``` ```
+29 -3
View File
@@ -36,6 +36,9 @@ param(
[int]$NtlmTimeoutSeconds = 20, [int]$NtlmTimeoutSeconds = 20,
[ValidateRange(2, 90)] [ValidateRange(2, 90)]
[int]$ProfileTimeoutSeconds = 90, [int]$ProfileTimeoutSeconds = 90,
[ValidateNotNullOrEmpty()]
[string[]]$FirewallRemoteAddress = @('LocalSubnet'),
[ipaddress]$FirewallLocalAddress,
[switch]$CreateMissingOus, [switch]$CreateMissingOus,
[switch]$DisableCertificateRevocationCheckForLab [switch]$DisableCertificateRevocationCheckForLab
) )
@@ -215,9 +218,32 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install the SGU Authentication Broker
throw 'Could not enable recovery for non-crash SGUAuthBroker failures.' throw 'Could not enable recovery for non-crash SGUAuthBroker failures.'
} }
if (-not (Get-NetFirewallRule -DisplayName 'SGU Authentication Broker (mTLS)' -ErrorAction SilentlyContinue)) { $firewallRule = Get-NetFirewallRule `
New-NetFirewallRule -DisplayName 'SGU Authentication Broker (mTLS)' ` -DisplayName 'SGU Authentication Broker (mTLS)' `
-Direction Inbound -Action Allow -Protocol TCP -LocalPort 8443 -Profile Domain | Out-Null -ErrorAction SilentlyContinue
if (-not $firewallRule) {
$firewallParameters = @{
DisplayName = 'SGU Authentication Broker (mTLS)'
Direction = 'Inbound'
Action = 'Allow'
Protocol = 'TCP'
LocalPort = 8443
Profile = 'Any'
RemoteAddress = $FirewallRemoteAddress
}
if ($FirewallLocalAddress) {
$firewallParameters.LocalAddress = $FirewallLocalAddress.IPAddressToString
}
$firewallRule = New-NetFirewallRule @firewallParameters
}
else {
$firewallRule | Set-NetFirewallRule -Enabled True -Profile Any
$addressParameters = @{ RemoteAddress = $FirewallRemoteAddress }
if ($FirewallLocalAddress) {
$addressParameters.LocalAddress = $FirewallLocalAddress.IPAddressToString
}
$firewallRule | Get-NetFirewallAddressFilter |
Set-NetFirewallAddressFilter @addressParameters | Out-Null
} }
Start-Service -Name $serviceName Start-Service -Name $serviceName
+16 -8
View File
@@ -20,6 +20,20 @@ $remoteDesktopUsersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1] $remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesktopPrincipal access")) { if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesktopPrincipal access")) {
function Invoke-PowerCfgBestEffort {
param([Parameter(Mandatory)][string[]]$Arguments)
# Start-Process keeps powercfg's policy-override diagnostic on its own
# stderr stream. In PowerShell 7, directly invoking that native command
# turns stderr into a terminating ErrorRecord under $ErrorActionPreference
# = 'Stop', which previously aborted this unrelated remediation work.
$process = Start-Process -FilePath "$env:SystemRoot\System32\powercfg.exe" `
-ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -ne 0) {
Write-Warning "powercfg $($Arguments -join ' ') returned exit code $($process.ExitCode); continuing enrollment repair."
}
}
foreach ($powerChange in @( foreach ($powerChange in @(
@('monitor-timeout-ac', '0'), @('monitor-timeout-ac', '0'),
@('monitor-timeout-dc', '0'), @('monitor-timeout-dc', '0'),
@@ -27,15 +41,9 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesk
@('standby-timeout-dc', '0'), @('standby-timeout-dc', '0'),
@('hibernate-timeout-ac', '0'), @('hibernate-timeout-ac', '0'),
@('hibernate-timeout-dc', '0'))) { @('hibernate-timeout-dc', '0'))) {
& powercfg.exe /change $powerChange[0] $powerChange[1] Invoke-PowerCfgBestEffort -Arguments @('/change', $powerChange[0], $powerChange[1])
if ($LASTEXITCODE -ne 0) {
throw "powercfg /change $($powerChange[0]) failed with exit code $LASTEXITCODE."
}
}
& powercfg.exe /hibernate off
if ($LASTEXITCODE -ne 0) {
throw "powercfg /hibernate off failed with exit code $LASTEXITCODE."
} }
Invoke-PowerCfgBestEffort -Arguments @('/hibernate', 'off')
Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' ` Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
-Name fDenyTSConnections -Type DWord -Value 0 -Name fDenyTSConnections -Type DWord -Value 0
+23 -10
View File
@@ -1,5 +1,8 @@
[CmdletBinding(SupportsShouldProcess)] [CmdletBinding(SupportsShouldProcess)]
param() param(
[ValidateNotNullOrEmpty()]
[string[]]$AllowedRemoteAddress = @('LocalSubnet')
)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent() $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
@@ -38,9 +41,12 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable secure administrative RDP
Set-Service -Name TermService -StartupType Automatic Set-Service -Name TermService -StartupType Automatic
Start-Service -Name TermService Start-Service -Name TermService
Get-NetFirewallRule -Name 'RemoteDesktop-UserMode-In-TCP','RemoteDesktop-UserMode-In-UDP' ` $remoteDesktopRules = @(Get-NetFirewallRule `
-ErrorAction SilentlyContinue | -Name 'RemoteDesktop-UserMode-In-TCP','RemoteDesktop-UserMode-In-UDP' `
Set-NetFirewallRule -Enabled True -Profile Domain -ErrorAction SilentlyContinue)
$remoteDesktopRules | Set-NetFirewallRule -Enabled True -Profile Any
$remoteDesktopRules | Get-NetFirewallAddressFilter |
Set-NetFirewallAddressFilter -RemoteAddress $AllowedRemoteAddress | Out-Null
$enableRemoting = Start-Process ` $enableRemoting = Start-Process `
-FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" ` -FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
@@ -57,9 +63,12 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable secure administrative RDP
Set-Service -Name WinRM -StartupType Automatic Set-Service -Name WinRM -StartupType Automatic
Start-Service -Name WinRM Start-Service -Name WinRM
Get-NetFirewallRule -Name 'WINRM-HTTP-In-TCP','WINRM-HTTP-In-TCP-NoScope' ` $winRmRules = @(Get-NetFirewallRule `
-ErrorAction SilentlyContinue | -Name 'WINRM-HTTP-In-TCP','WINRM-HTTP-In-TCP-NoScope' `
Set-NetFirewallRule -Enabled True -Profile Domain -ErrorAction SilentlyContinue)
$winRmRules | Set-NetFirewallRule -Enabled True -Profile Any
$winRmRules | Get-NetFirewallAddressFilter |
Set-NetFirewallAddressFilter -RemoteAddress $AllowedRemoteAddress | Out-Null
Get-NetFirewallRule -Name 'WINRM-HTTP-In-TCP-PUBLIC' -ErrorAction SilentlyContinue | Get-NetFirewallRule -Name 'WINRM-HTTP-In-TCP-PUBLIC' -ErrorAction SilentlyContinue |
Disable-NetFirewallRule Disable-NetFirewallRule
@@ -74,8 +83,11 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable secure administrative RDP
'WMI-WINMGMT-In-TCP', 'WMI-WINMGMT-In-TCP',
'WMI-ASYNC-In-TCP' 'WMI-ASYNC-In-TCP'
) )
Get-NetFirewallRule -Name $administrativeRules -ErrorAction SilentlyContinue | $enabledAdministrativeRules = @(Get-NetFirewallRule `
Set-NetFirewallRule -Enabled True -Profile Domain -Name $administrativeRules -ErrorAction SilentlyContinue)
$enabledAdministrativeRules | Set-NetFirewallRule -Enabled True -Profile Any
$enabledAdministrativeRules | Get-NetFirewallAddressFilter |
Set-NetFirewallAddressFilter -RemoteAddress $AllowedRemoteAddress | Out-Null
} }
[pscustomobject]@{ [pscustomobject]@{
@@ -88,7 +100,8 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable secure administrative RDP
-Name UserAuthentication) -eq 1 -Name UserAuthentication) -eq 1
TermService = (Get-Service TermService).Status TermService = (Get-Service TermService).Status
WinRM = (Get-Service WinRM).Status WinRM = (Get-Service WinRM).Status
FirewallProfile = 'Domain' FirewallProfile = 'Any'
AllowedRemoteAddress = $AllowedRemoteAddress
AdministrativeAccessOnly = $true AdministrativeAccessOnly = $true
AlwaysOnPowerPolicyApplied = $true AlwaysOnPowerPolicyApplied = $true
} }
+157 -12
View File
@@ -171,8 +171,10 @@ function Ensure-OrganizationalUnit {
) )
$distinguishedName = "OU=$Name,$Path" $distinguishedName = "OU=$Name,$Path"
$existing = Get-ADOrganizationalUnit -Identity $distinguishedName -Server $Server ` $escapedName = $Name.Replace('\', '\5c').Replace('*', '\2a').Replace('(', '\28').Replace(')', '\29')
-ErrorAction SilentlyContinue $existing = Get-ADOrganizationalUnit -LDAPFilter "(ou=$escapedName)" `
-SearchBase $Path -SearchScope OneLevel -Server $Server -ErrorAction Stop |
Select-Object -First 1
if (-not $existing) { if (-not $existing) {
New-ADOrganizationalUnit -Name $Name -Path $Path ` New-ADOrganizationalUnit -Name $Name -Path $Path `
-ProtectedFromAccidentalDeletion $true -Server $Server | Out-Null -ProtectedFromAccidentalDeletion $true -Server $Server | Out-Null
@@ -180,6 +182,57 @@ function Ensure-OrganizationalUnit {
return $distinguishedName 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 { function Set-PackageShare {
param( param(
[Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$Path,
@@ -234,6 +287,10 @@ function Set-PackageShare {
} }
Assert-Administrator Assert-Administrator
trap {
Write-BootstrapLog ("ERROR: " + $_.Exception.Message)
throw
}
$operatingSystem = Get-CimInstance Win32_OperatingSystem $operatingSystem = Get-CimInstance Win32_OperatingSystem
if ([int]$operatingSystem.ProductType -eq 1) { if ([int]$operatingSystem.ProductType -eq 1) {
throw 'The domain controller bootstrap requires Windows Server, not a Windows client edition.' throw 'The domain controller bootstrap requires Windows Server, not a Windows client edition.'
@@ -379,24 +436,75 @@ if (-not $computer.Domain.Equals($DomainName, [StringComparison]::OrdinalIgnoreC
} }
Write-BootstrapLog 'Finalizing Active Directory, DNS, policies, broker, shares, and remote management.' 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 # Once the machine is a DC, every active adapter must query the local DNS
$usersOuDn = Ensure-OrganizationalUnit -Name 'Usuarios-SGU' -Path $baseDn -Server $env:COMPUTERNAME # 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')) { foreach ($ouName in @('Docentes', 'Alumnos', 'Administrativos')) {
Ensure-OrganizationalUnit -Name $ouName -Path $usersOuDn -Server $env:COMPUTERNAME | Out-Null Ensure-OrganizationalUnit -Name $ouName -Path $usersOuDn -Server $adServer | Out-Null
} }
$remoteDesktopGroupName = 'SG-Laboratorio-Usuarios-RDP' $remoteDesktopGroupName = 'SG-Laboratorio-Usuarios-RDP'
$remoteDesktopGroup = Get-ADGroup -Identity $remoteDesktopGroupName -Server $env:COMPUTERNAME ` $remoteDesktopGroup = Get-ADGroup -LDAPFilter "(sAMAccountName=$remoteDesktopGroupName)" `
-SearchBase $baseDn -SearchScope Subtree -Server $adServer `
-ErrorAction SilentlyContinue -ErrorAction SilentlyContinue
if (-not $remoteDesktopGroup) { if (-not $remoteDesktopGroup) {
New-ADGroup -Name $remoteDesktopGroupName -SamAccountName $remoteDesktopGroupName ` New-ADGroup -Name $remoteDesktopGroupName -SamAccountName $remoteDesktopGroupName `
-GroupCategory Security -GroupScope Global -Path $laboratoryOuDn ` -GroupCategory Security -GroupScope Global -Path $laboratoryOuDn `
-Description 'SGU users permitted to use Remote Desktop on laboratory clients.' ` -Description 'SGU users permitted to use Remote Desktop on laboratory clients.' `
-Server $env:COMPUTERNAME | Out-Null -Server $adServer | Out-Null
$remoteDesktopGroup = Get-ADGroup -Identity $remoteDesktopGroupName -Server $env:COMPUTERNAME $remoteDesktopGroup = Get-ADGroup -LDAPFilter "(sAMAccountName=$remoteDesktopGroupName)" `
-SearchBase $laboratoryOuDn -SearchScope OneLevel -Server $adServer
} }
& (Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1') ` & (Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1') `
@@ -444,16 +552,30 @@ if (Test-Path -LiteralPath $brokerConfigurationPath -PathType Leaf) {
-PublishPath $brokerPublishPath ` -PublishPath $brokerPublishPath `
-ServerCertificateSubject $brokerDnsName ` -ServerCertificateSubject $brokerDnsName `
-AllowedClientThumbprints $allowedClientThumbprints ` -AllowedClientThumbprints $allowedClientThumbprints `
-LdapHost $env:COMPUTERNAME ` -LdapHost $adServer `
-BaseDn $baseDn ` -BaseDn $baseDn `
-DomainNetbios $DomainNetbios ` -DomainNetbios $DomainNetbios `
-UpnSuffix $DomainName ` -UpnSuffix $DomainName `
-RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName ` -RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName `
-DefaultCompany 'La Salle' ` -DefaultCompany 'La Salle' `
-FirewallLocalAddress $ServerIPv4Address `
-FirewallRemoteAddress "$($ServerIPv4Address.IPAddressToString)/$PrefixLength" `
-CreateMissingOus ` -CreateMissingOus `
-DisableCertificateRevocationCheckForLab | Out-Null -DisableCertificateRevocationCheckForLab | Out-Null
& (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') | 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' $contentPath = Join-Path $bootstrapRoot 'payload\server-content\Packages'
if (Test-Path -LiteralPath $contentPath -PathType Container) { if (Test-Path -LiteralPath $contentPath -PathType Container) {
@@ -463,6 +585,27 @@ if (Test-Path -LiteralPath $contentPath -PathType Container) {
Set-PackageShare -Path $PackageSharePath -NetbiosName $DomainNetbios ` Set-PackageShare -Path $PackageSharePath -NetbiosName $DomainNetbios `
-DomainSid $domain.DomainSID.Value -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
}
& (Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1') ` & (Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1') `
-TargetOuDn $laboratoryOuDn -DomainController $env:COMPUTERNAME | Out-Null -TargetOuDn $laboratoryOuDn -DomainController $env:COMPUTERNAME | Out-Null
$userPolicyParameters = @{ $userPolicyParameters = @{
@@ -493,6 +636,8 @@ $validation = [ordered]@{
LaboratoryOu = $laboratoryOuDn LaboratoryOu = $laboratoryOuDn
UsersOu = $usersOuDn UsersOu = $usersOuDn
RemoteDesktopGroup = $remoteDesktopGroup.DistinguishedName RemoteDesktopGroup = $remoteDesktopGroup.DistinguishedName
DomainNetworkProfile = [string](Get-NetConnectionProfile `
-InterfaceAlias $NetworkInterfaceAlias -ErrorAction SilentlyContinue).NetworkCategory
} }
if ($validation.BrokerService -ne 'Running' -or if ($validation.BrokerService -ne 'Running' -or
+6 -2
View File
@@ -179,10 +179,14 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
$acl = Get-Acl -LiteralPath (Split-Path $settingsPath -Parent) $acl = Get-Acl -LiteralPath (Split-Path $settingsPath -Parent)
$acl.SetAccessRuleProtection($true, $false) $acl.SetAccessRuleProtection($true, $false)
# Resolve built-in identities by SID instead of localized display names.
# "BUILTIN\Administrators" is not resolvable on every non-English client.
$systemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18')
$administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
'SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')) $systemSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
'BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')) $administratorsSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
Set-Acl -LiteralPath (Split-Path $settingsPath -Parent) -AclObject $acl Set-Acl -LiteralPath (Split-Path $settingsPath -Parent) -AclObject $acl
New-Item -Path $classRegistryPath -Force | Out-Null New-Item -Path $classRegistryPath -Force | Out-Null
+10 -3
View File
@@ -71,7 +71,11 @@ if ($PSCmdlet.ShouldProcess($enrollmentRoot, 'Install the SGU enrollment repair
$runtimeDirectory = Join-Path $enrollmentRoot 'prerequisites' $runtimeDirectory = Join-Path $enrollmentRoot 'prerequisites'
New-Item -ItemType Directory -Path $runtimeDirectory -Force | Out-Null New-Item -ItemType Directory -Path $runtimeDirectory -Force | Out-Null
$guardRuntimeInstaller = Join-Path $runtimeDirectory (Split-Path $DotNetRuntimeInstallerPath -Leaf) $guardRuntimeInstaller = Join-Path $runtimeDirectory (Split-Path $DotNetRuntimeInstallerPath -Leaf)
Copy-Item -LiteralPath $DotNetRuntimeInstallerPath -Destination $guardRuntimeInstaller -Force $sourceRuntimeInstaller = [IO.Path]::GetFullPath($DotNetRuntimeInstallerPath)
$destinationRuntimeInstaller = [IO.Path]::GetFullPath($guardRuntimeInstaller)
if (-not $sourceRuntimeInstaller.Equals($destinationRuntimeInstaller, [StringComparison]::OrdinalIgnoreCase)) {
Copy-Item -LiteralPath $DotNetRuntimeInstallerPath -Destination $guardRuntimeInstaller -Force
}
} }
$guardConfiguration = [ordered]@{ $guardConfiguration = [ordered]@{
@@ -92,10 +96,13 @@ if ($PSCmdlet.ShouldProcess($enrollmentRoot, 'Install the SGU enrollment repair
$acl = Get-Acl -LiteralPath $enrollmentRoot $acl = Get-Acl -LiteralPath $enrollmentRoot
$acl.SetAccessRuleProtection($true, $false) $acl.SetAccessRuleProtection($true, $false)
# Well-known SIDs are invariant across localized Windows installations.
$systemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18')
$administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
'SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')) $systemSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
'BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')) $administratorsSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
Set-Acl -LiteralPath $enrollmentRoot -AclObject $acl Set-Acl -LiteralPath $enrollmentRoot -AclObject $acl
$repairScript = Join-Path $enrollmentRoot 'Repair-SguClientEnrollment.ps1' $repairScript = Join-Path $enrollmentRoot 'Repair-SguClientEnrollment.ps1'
+1
View File
@@ -97,6 +97,7 @@ try {
$releaseNotes = @" $releaseNotes = @"
Bootstrap reproducible para el laboratorio SGU. Bootstrap reproducible para el laboratorio SGU.
- **Advertencia:** el bootstrap de servidor crea un bosque nuevo. No restaura los SID, contraseñas ni relaciones de confianza del bosque anterior; para conservarlos se requiere una recuperación de bosque desde una copia de estado del sistema.
- `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-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-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.
- Windows Home se detecta y se rechaza con una explicación, ya que no admite unión a Active Directory ni RDP host. - Windows Home se detecta y se rechaza con una explicación, ya que no admite unión a Active Directory ni RDP host.
+57 -10
View File
@@ -8,18 +8,65 @@ param(
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$existing = Get-DnsServerResourceRecord -ZoneName $ZoneName -Name $RecordName -RRType A -ErrorAction SilentlyContinue $dnsReady = $false
if ($existing) { for ($attempt = 1; $attempt -le 30; $attempt++) {
$current = @($existing.RecordData.IPv4Address.IPAddressToString) try {
if ($current.Count -ne 1 -or $current[0] -ne $IPv4Address.IPAddressToString) { $soa = @(Resolve-DnsName $ZoneName -Type SOA -DnsOnly -Server localhost `
# The fixed lab address is an explicit bootstrap input and may change -ErrorAction Stop | Where-Object Type -eq SOA)
# when the server is rebuilt. Replace only this exact A record set. if ($soa.Count -gt 0) {
$existing | Remove-DnsServerResourceRecord -ZoneName $ZoneName -Force $dnsReady = $true
Add-DnsServerResourceRecordA -ZoneName $ZoneName -Name $RecordName -IPv4Address $IPv4Address break
}
} }
catch {
# An AD-integrated zone can take a few seconds to load after DNS starts.
}
Start-Sleep -Seconds 2
} }
else { if (-not $dnsReady) {
Add-DnsServerResourceRecordA -ZoneName $ZoneName -Name $RecordName -IPv4Address $IPv4Address throw "DNS did not load the $ZoneName zone before the readiness timeout."
}
$recordReady = $false
for ($attempt = 1; $attempt -le 5; $attempt++) {
$existing = @(Get-DnsServerResourceRecord -ZoneName $ZoneName -Name $RecordName `
-RRType A -ErrorAction SilentlyContinue)
$unwanted = @($existing | Where-Object {
$_.RecordData.IPv4Address.IPAddressToString -ne $IPv4Address.IPAddressToString
})
foreach ($record in $unwanted) {
Remove-DnsServerResourceRecord -ZoneName $ZoneName -InputObject $record -Force
}
$desired = @($existing | Where-Object {
$_.RecordData.IPv4Address.IPAddressToString -eq $IPv4Address.IPAddressToString
})
if ($desired.Count -eq 0) {
try {
Add-DnsServerResourceRecordA -ZoneName $ZoneName -Name $RecordName `
-IPv4Address $IPv4Address -ErrorAction Stop
}
catch {
# A record that becomes visible while an AD-integrated zone is
# finishing its load is harmless; the verified read below decides.
}
}
Start-Sleep -Milliseconds 250
$final = @(Get-DnsServerResourceRecord -ZoneName $ZoneName -Name $RecordName `
-RRType A -ErrorAction SilentlyContinue)
$finalAddresses = @($final | ForEach-Object {
$_.RecordData.IPv4Address.IPAddressToString
})
if ($finalAddresses.Count -eq 1 -and
$finalAddresses[0] -eq $IPv4Address.IPAddressToString) {
$recordReady = $true
break
}
Start-Sleep -Seconds 1
}
if (-not $recordReady) {
throw "The $RecordName.$ZoneName A record could not be set exclusively to $IPv4Address."
} }
if ($ExternalForwarders.Count -gt 0) { if ($ExternalForwarders.Count -gt 0) {
+27
View File
@@ -10,6 +10,7 @@ $ErrorActionPreference = 'Stop'
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System' $policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
$policyValueName = 'DisableLockWorkstation' $policyValueName = 'DisableLockWorkstation'
$desktopPolicyKey = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop' $desktopPolicyKey = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
$themeKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent() $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity) $principal = [Security.Principal.WindowsPrincipal]::new($identity)
@@ -83,6 +84,19 @@ if ($PSCmdlet.ShouldProcess($GpoName, 'Prevent SGU users from manually locking w
-Type String ` -Type String `
-Value '0' | Out-Null -Value '0' | Out-Null
# Apply the native Windows dark theme at user logon. Both values are required:
# one controls the shell and the other controls supported applications.
foreach ($themeValueName in 'AppsUseLightTheme', 'SystemUsesLightTheme') {
Set-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $themeKey `
-ValueName $themeValueName `
-Type DWord `
-Value 0 | Out-Null
}
if ($WallpaperPath) { if ($WallpaperPath) {
Set-GPRegistryValue ` Set-GPRegistryValue `
-Name $GpoName ` -Name $GpoName `
@@ -115,6 +129,18 @@ $screenSaverValue = Get-GPRegistryValue `
-Server $DomainController ` -Server $DomainController `
-Key $desktopPolicyKey ` -Key $desktopPolicyKey `
-ValueName 'ScreenSaveActive' -ValueName 'ScreenSaveActive'
$appsThemeValue = Get-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $themeKey `
-ValueName 'AppsUseLightTheme'
$systemThemeValue = Get-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $themeKey `
-ValueName 'SystemUsesLightTheme'
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks | $link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $GpoName | Where-Object DisplayName -eq $GpoName |
Select-Object -First 1 Select-Object -First 1
@@ -138,5 +164,6 @@ if ($WallpaperPath) {
LinkEnabled = [bool]$linkEnabled LinkEnabled = [bool]$linkEnabled
DisableLockWorkstation = [int]$configuredValue.Value DisableLockWorkstation = [int]$configuredValue.Value
ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0' ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0'
DarkMode = ([int]$appsThemeValue.Value -eq 0) -and ([int]$systemThemeValue.Value -eq 0)
Wallpaper = $configuredWallpaper Wallpaper = $configuredWallpaper
} }