Compare commits

...
3 Commits
12 changed files with 251 additions and 31 deletions
+11 -1
View File
@@ -111,7 +111,13 @@ ports remain private even though the VM owns a public IP.
Never disable the built-in Microsoft password Credential Provider. It is the Never disable the built-in Microsoft password Credential Provider. It is the
supported recovery path if a third-party provider fails to load. supported recovery path if a third-party provider fails to load.
For a clean machine, the supported entry points are the release packages: For a clean machine, choose the release package that matches the workstation:
- `sgu-windows10-legacy-client-bootstrap-VERSION.zip` for Windows 10;
- `sgu-windows11-client-bootstrap-VERSION.zip` for Windows 11, including the
modern Azure P2S/pre-logon flow.
Both use the same direct-lab entry point:
```bat ```bat
Start-SguServerBootstrap.cmd 192.168.50.10 Start-SguServerBootstrap.cmd 192.168.50.10
@@ -123,6 +129,10 @@ omite y ese adaptador todavía usa una dirección `169.254.x.x`, el enrolador la
solicita de forma interactiva. En equipos con dos NIC selecciona el adaptador solicita de forma interactiva. En equipos con dos NIC selecciona el adaptador
sin puerta de enlace y conserva el `Default Switch` para Internet. sin puerta de enlace y conserva el `Default Switch` para Internet.
El manifiesto identifica el perfil `Windows10Legacy` o `Windows11Modern` y el
bootstrap valida el build antes de hacer cambios. Las correcciones comunes se
mantienen en ambos; Windows 11 conserva además sus puntos de entrada modernos.
Linux clients are enrolled through their native PAM/SSSD stack instead of the Linux clients are enrolled through their native PAM/SSSD stack instead of the
Windows Credential Provider: Windows Credential Provider:
+1 -1
View File
@@ -122,7 +122,7 @@ $w11 = .\scripts\New-SguAzureP2sCertificates.ps1 -ClientName 'Windows11'
Copie a la VM Windows 11 de Hyper-V: Copie a la VM Windows 11 de Hyper-V:
- `sgu-client-bootstrap-VERSION.zip` extraído; - `sgu-windows11-client-bootstrap-VERSION.zip` extraído;
- `$vpn.PackagePath`; - `$vpn.PackagePath`;
- `$w11.ClientCertificatePath`; - `$w11.ClientCertificatePath`;
- `sgu-azure-p2s-root.cer`. - `sgu-azure-p2s-root.cer`.
+9 -1
View File
@@ -96,7 +96,9 @@ Se admiten Pro, Enterprise y Education. Windows Home no puede unirse a Active
Directory local ni actuar como host RDP; el bootstrap lo detecta antes de cambiar Directory local ni actuar como host RDP; el bootstrap lo detecta antes de cambiar
el equipo y explica que se debe actualizar la edición. el equipo y explica que se debe actualizar la edición.
1. Descargar y extraer `sgu-client-bootstrap-VERSION.zip`. 1. Descargar y extraer el paquete correspondiente:
`sgu-windows10-legacy-client-bootstrap-VERSION.zip` o
`sgu-windows11-client-bootstrap-VERSION.zip`.
2. Ejecutar con la IP fija actual del controlador de dominio: 2. Ejecutar con la IP fija actual del controlador de dominio:
```bat ```bat
@@ -111,6 +113,12 @@ del cliente, el bootstrap la solicita cuando el adaptador sólo tiene APIPA
ruta predeterminada. Si falla, la ventana elevada permanece abierta y el mismo ruta predeterminada. Si falla, la ventana elevada permanece abierta y el mismo
error queda en `C:\ProgramData\SGU\Bootstrap\Client\latest-error.log`. error queda en `C:\ProgramData\SGU\Bootstrap\Client\latest-error.log`.
Cada manifiesto fija su perfil y evita cruzar paquetes: Windows 10 utiliza
`Windows10Legacy` (build menor a 22000) y Windows 11 `Windows11Modern` (build
22000 o posterior). El ZIP moderno conserva tanto el enrolamiento directo como
Azure P2S/pre-logon; el ZIP legado contiene el flujo directo. El código común y
las garantías de seguridad son idénticos.
Después de UAC, se solicita interactivamente la credencial autorizada para unir Después de UAC, se solicita interactivamente la credencial autorizada para unir
equipos. La contraseña existe sólo en memoria. El bootstrap: equipos. La contraseña existe sólo en memoria. El bootstrap:
+13 -2
View File
@@ -1,7 +1,11 @@
# Enrolamiento obligatorio de clientes SGU # Enrolamiento obligatorio de clientes SGU
Para una instalación limpia de Windows se prefiere el único punto de entrada Para una instalación limpia se selecciona primero el ZIP correspondiente:
empaquetado:
- `sgu-windows10-legacy-client-bootstrap-VERSION.zip` para Windows 10;
- `sgu-windows11-client-bootstrap-VERSION.zip` para Windows 11.
Ambos conservan el punto de entrada directo:
```bat ```bat
Start-SguClientEnrollment.cmd 192.168.50.10 192.168.50.11 Start-SguClientEnrollment.cmd 192.168.50.10 192.168.50.11
@@ -19,6 +23,13 @@ la única NIC activa sin puerta de enlace para no reemplazar el adaptador de
Internet. Ante cualquier error conserva la ventana y escribe el diagnóstico en Internet. Ante cualquier error conserva la ventana y escribe el diagnóstico en
`C:\ProgramData\SGU\Bootstrap\Client\latest-error.log`. `C:\ProgramData\SGU\Bootstrap\Client\latest-error.log`.
El manifiesto contiene el perfil `Windows10Legacy` o `Windows11Modern` y el
bootstrap rechaza un ZIP que no corresponda al build instalado. El paquete de
Windows 11 conserva además `Start-SguAzureClientEnrollment.cmd` y el instalador
P2S de equipo; el legado de Windows 10 se limita al transporte directo del
laboratorio. Credential Provider, mTLS, cuenta `alumno`, RustDesk, monitorización
y autorreparación siguen saliendo de la misma base de código.
El flujo administrado instala y valida el Credential Provider **antes** de El flujo administrado instala y valida el Credential Provider **antes** de
ejecutar `Add-Computer`. La pertenencia al dominio es el último cambio; si falta ejecutar `Add-Computer`. La pertenencia al dominio es el último cambio; si falta
el runtime, un certificado, el registro COM, la directiva predeterminada o la el runtime, un certificado, el registro COM, la directiva predeterminada o la
+38 -2
View File
@@ -11,6 +11,8 @@ param(
[string]$DomainNetbios = 'LCI', [string]$DomainNetbios = 'LCI',
[string]$ComputerOuDn, [string]$ComputerOuDn,
[string]$NewComputerName, [string]$NewComputerName,
[ValidateSet('Auto', 'Windows10Legacy', 'Windows11Modern')]
[string]$CompatibilityProfile = 'Auto',
[ValidateSet('Direct', 'AzureP2S')] [ValidateSet('Direct', 'AzureP2S')]
[string]$ConnectivityMode = 'Direct', [string]$ConnectivityMode = 'Direct',
[string]$VpnConnectionName = 'SGU Azure P2S', [string]$VpnConnectionName = 'SGU Azure P2S',
@@ -85,6 +87,7 @@ function Assert-PackageManifest {
throw "Bootstrap package integrity check failed: $($entry.Path)" throw "Bootstrap package integrity check failed: $($entry.Path)"
} }
} }
return $manifest
} }
function Resolve-ClientInterfaceAlias { function Resolve-ClientInterfaceAlias {
@@ -309,7 +312,38 @@ if (-not $ComputerOuDn) {
} }
$packageRoot = $PSScriptRoot $packageRoot = $PSScriptRoot
Assert-PackageManifest -PackageRoot $packageRoot $packageManifest = Assert-PackageManifest -PackageRoot $packageRoot
$manifestProfile = if ($packageManifest.PSObject.Properties['CompatibilityProfile']) {
[string]$packageManifest.CompatibilityProfile
}
else {
'Auto'
}
if ($CompatibilityProfile -ne 'Auto' -and $manifestProfile -ne 'Auto' -and
$CompatibilityProfile -ne $manifestProfile) {
throw "The requested compatibility profile '$CompatibilityProfile' does not match package profile '$manifestProfile'."
}
if ($CompatibilityProfile -eq 'Auto') {
$CompatibilityProfile = $manifestProfile
}
$windowsBuild = [int]$operatingSystem.BuildNumber
if ($CompatibilityProfile -eq 'Auto') {
$CompatibilityProfile = if ($windowsBuild -lt 22000) {
'Windows10Legacy'
}
else {
'Windows11Modern'
}
}
if ($CompatibilityProfile -eq 'Windows10Legacy' -and $windowsBuild -ge 22000) {
throw "The Windows 10 legacy package cannot enroll Windows build $windowsBuild. Use the Windows 11 modern client package."
}
if ($CompatibilityProfile -eq 'Windows11Modern' -and $windowsBuild -lt 22000) {
throw "The Windows 11 modern package cannot enroll Windows build $windowsBuild. Use the Windows 10 legacy client package."
}
if ($CompatibilityProfile -eq 'Windows10Legacy' -and $ConnectivityMode -eq 'AzureP2S') {
throw 'Azure P2S pre-logon enrollment belongs to the Windows 11 modern package. Use Direct connectivity for the Windows 10 legacy package.'
}
$scriptsRoot = Join-Path $packageRoot 'payload\scripts' $scriptsRoot = Join-Path $packageRoot 'payload\scripts'
$providerPublishPath = Join-Path $packageRoot 'payload\credential-provider' $providerPublishPath = Join-Path $packageRoot 'payload\credential-provider'
$runtimeInstaller = Get-ChildItem (Join-Path $packageRoot 'payload\prerequisites') ` $runtimeInstaller = Get-ChildItem (Join-Path $packageRoot 'payload\prerequisites') `
@@ -593,7 +627,8 @@ finally {
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $priorTrustedHosts -Force Set-Item WSMan:\localhost\Client\TrustedHosts -Value $priorTrustedHosts -Force
} }
if (-not $winRmWasRunning) { if (-not $winRmWasRunning) {
Stop-Service WinRM -Force -ErrorAction SilentlyContinue Stop-Service WinRM -Force -NoWait -WarningAction SilentlyContinue `
-ErrorAction SilentlyContinue
} }
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
$DomainCredential = $null $DomainCredential = $null
@@ -608,6 +643,7 @@ if ($SkipRestart) {
ClientCertificateRegistered = $true ClientCertificateRegistered = $true
BrokerEndpoint = $brokerEndpoint BrokerEndpoint = $brokerEndpoint
ConnectivityMode = $ConnectivityMode ConnectivityMode = $ConnectivityMode
CompatibilityProfile = $CompatibilityProfile
VpnConnectionName = if ($ConnectivityMode -eq 'AzureP2S') { $VpnConnectionName } else { $null } VpnConnectionName = if ($ConnectivityMode -eq 'AzureP2S') { $VpnConnectionName } else { $null }
RestartRequired = $true RestartRequired = $true
RustDesk = if ($result) { $result.RustDesk } else { $null } RustDesk = if ($result) { $result.RustDesk } else { $null }
+48 -15
View File
@@ -33,7 +33,10 @@ function Write-PackageManifest {
param( param(
[Parameter(Mandatory)][string]$PackageRoot, [Parameter(Mandatory)][string]$PackageRoot,
[Parameter(Mandatory)][string]$PackageVersion, [Parameter(Mandatory)][string]$PackageVersion,
[Parameter(Mandatory)][string]$PackageKind [Parameter(Mandatory)][string]$PackageKind,
[ValidateSet('Windows10Legacy', 'Windows11Modern')]
[string]$CompatibilityProfile,
[string]$TargetOperatingSystem
) )
$resolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path.TrimEnd('\') $resolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path.TrimEnd('\')
@@ -48,13 +51,19 @@ function Write-PackageManifest {
} }
}) })
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 1 SchemaVersion = 2
Product = 'SGU Credential Provider' Product = 'SGU Credential Provider'
PackageKind = $PackageKind PackageKind = $PackageKind
Version = $PackageVersion Version = $PackageVersion
CreatedAt = (Get-Date).ToUniversalTime().ToString('o') CreatedAt = (Get-Date).ToUniversalTime().ToString('o')
Files = $files Files = $files
} }
if ($CompatibilityProfile) {
$manifest['CompatibilityProfile'] = $CompatibilityProfile
}
if ($TargetOperatingSystem) {
$manifest['TargetOperatingSystem'] = $TargetOperatingSystem
}
[IO.File]::WriteAllText( [IO.File]::WriteAllText(
(Join-Path $resolvedPackageRoot 'package-manifest.json'), (Join-Path $resolvedPackageRoot 'package-manifest.json'),
($manifest | ConvertTo-Json -Depth 6), ($manifest | ConvertTo-Json -Depth 6),
@@ -78,21 +87,28 @@ if (-not $runtimeInstaller) {
} }
New-Item -ItemType Directory -Path $resolvedOutputRoot -Force | Out-Null New-Item -ItemType Directory -Path $resolvedOutputRoot -Force | Out-Null
$clientRoot = Join-Path $resolvedOutputRoot "sgu-client-bootstrap-$Version" $windows11ClientRoot = Join-Path $resolvedOutputRoot "sgu-windows11-client-bootstrap-$Version"
$windows10ClientRoot = Join-Path $resolvedOutputRoot "sgu-windows10-legacy-client-bootstrap-$Version"
$clientRoot = $windows11ClientRoot
$serverRoot = Join-Path $resolvedOutputRoot "sgu-server-bootstrap-$Version" $serverRoot = Join-Path $resolvedOutputRoot "sgu-server-bootstrap-$Version"
$linuxClientRoot = Join-Path $resolvedOutputRoot "sgu-linux-client-bootstrap-$Version" $linuxClientRoot = Join-Path $resolvedOutputRoot "sgu-linux-client-bootstrap-$Version"
$azureRoot = Join-Path $resolvedOutputRoot "sgu-azure-infrastructure-$Version" $azureRoot = Join-Path $resolvedOutputRoot "sgu-azure-infrastructure-$Version"
$clientZip = "$clientRoot.zip" $windows11ClientZip = "$windows11ClientRoot.zip"
$windows10ClientZip = "$windows10ClientRoot.zip"
$serverZip = "$serverRoot.zip" $serverZip = "$serverRoot.zip"
$linuxClientZip = "$linuxClientRoot.zip" $linuxClientZip = "$linuxClientRoot.zip"
$azureZip = "$azureRoot.zip" $azureZip = "$azureRoot.zip"
foreach ($target in @($clientRoot,$serverRoot,$linuxClientRoot,$azureRoot,$clientZip,$serverZip,$linuxClientZip,$azureZip)) { foreach ($target in @(
$windows11ClientRoot,$windows10ClientRoot,$serverRoot,$linuxClientRoot,$azureRoot,
$windows11ClientZip,$windows10ClientZip,$serverZip,$linuxClientZip,$azureZip)) {
if (Test-Path -LiteralPath $target) { if (Test-Path -LiteralPath $target) {
throw "Release target already exists: $target" throw "Release target already exists: $target"
} }
} }
New-Item -ItemType Directory -Path $clientRoot,$serverRoot,$linuxClientRoot,$azureRoot -Force | Out-Null New-Item -ItemType Directory `
-Path $windows11ClientRoot,$windows10ClientRoot,$serverRoot,$linuxClientRoot,$azureRoot `
-Force | Out-Null
$welcomeFontNames = @( $welcomeFontNames = @(
'IndivisaTextSans-Regular.otf', 'IndivisaTextSans-Regular.otf',
'IndivisaTextSans-Bold.otf', 'IndivisaTextSans-Bold.otf',
@@ -105,10 +121,6 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Invoke-SguClientBootstrap.ps
-Destination (Join-Path $clientRoot 'Invoke-SguClientBootstrap.ps1') -Destination (Join-Path $clientRoot 'Invoke-SguClientBootstrap.ps1')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguClientEnrollment.cmd') ` Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguClientEnrollment.cmd') `
-Destination (Join-Path $clientRoot '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 = @( $clientScripts = @(
'Enable-LabRemoteAccess.ps1', 'Enable-LabRemoteAccess.ps1',
'Enable-SguClientMonitoring.ps1', 'Enable-SguClientMonitoring.ps1',
@@ -141,8 +153,26 @@ foreach ($fontName in $welcomeFontNames) {
} }
Copy-RequiredFile -Source $runtimeInstaller.FullName ` Copy-RequiredFile -Source $runtimeInstaller.FullName `
-Destination (Join-Path $clientRoot "payload\prerequisites\$($runtimeInstaller.Name)") -Destination (Join-Path $clientRoot "payload\prerequisites\$($runtimeInstaller.Name)")
Write-PackageManifest -PackageRoot $clientRoot -PackageVersion $Version -PackageKind Client
Compress-Archive -Path (Join-Path $clientRoot '*') -DestinationPath $clientZip ` # Both Windows packages share the provider and enrollment implementation. The
# Windows 10 artifact freezes the direct-network compatibility surface, while
# the Windows 11 artifact adds the modern Azure P2S/pre-logon entry point.
Copy-Item -Path (Join-Path $windows11ClientRoot '*') `
-Destination $windows10ClientRoot -Recurse -Force
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureClientEnrollment.cmd') `
-Destination (Join-Path $windows11ClientRoot 'Start-SguAzureClientEnrollment.cmd')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Install-SguAzureP2sClient.ps1') `
-Destination (Join-Path $windows11ClientRoot 'Install-SguAzureP2sClient.ps1')
Write-PackageManifest -PackageRoot $windows10ClientRoot -PackageVersion $Version `
-PackageKind WindowsClient -CompatibilityProfile Windows10Legacy `
-TargetOperatingSystem 'Windows 10 Pro, Enterprise, or Education (build below 22000)'
Write-PackageManifest -PackageRoot $windows11ClientRoot -PackageVersion $Version `
-PackageKind WindowsClient -CompatibilityProfile Windows11Modern `
-TargetOperatingSystem 'Windows 11 Pro, Enterprise, or Education (build 22000 or later)'
Compress-Archive -Path (Join-Path $windows10ClientRoot '*') -DestinationPath $windows10ClientZip `
-CompressionLevel Optimal
Compress-Archive -Path (Join-Path $windows11ClientRoot '*') -DestinationPath $windows11ClientZip `
-CompressionLevel Optimal -CompressionLevel Optimal
# Linux clients use their native PAM/SSSD sign-in stack rather than the Windows # Linux clients use their native PAM/SSSD sign-in stack rather than the Windows
@@ -242,7 +272,8 @@ Compress-Archive -Path (Join-Path $azureRoot '*') -DestinationPath $azureZip `
-CompressionLevel Optimal -CompressionLevel Optimal
$checksums = @( $checksums = @(
("{0} {1}" -f (Get-FileHash -LiteralPath $clientZip -Algorithm SHA256).Hash, (Split-Path $clientZip -Leaf)) ("{0} {1}" -f (Get-FileHash -LiteralPath $windows10ClientZip -Algorithm SHA256).Hash, (Split-Path $windows10ClientZip -Leaf))
("{0} {1}" -f (Get-FileHash -LiteralPath $windows11ClientZip -Algorithm SHA256).Hash, (Split-Path $windows11ClientZip -Leaf))
("{0} {1}" -f (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash, (Split-Path $serverZip -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 $linuxClientZip -Algorithm SHA256).Hash, (Split-Path $linuxClientZip -Leaf))
("{0} {1}" -f (Get-FileHash -LiteralPath $azureZip -Algorithm SHA256).Hash, (Split-Path $azureZip -Leaf)) ("{0} {1}" -f (Get-FileHash -LiteralPath $azureZip -Algorithm SHA256).Hash, (Split-Path $azureZip -Leaf))
@@ -252,8 +283,10 @@ $checksumsPath = Join-Path $resolvedOutputRoot "SHA256SUMS-$Version.txt"
[pscustomobject]@{ [pscustomobject]@{
Version = $Version Version = $Version
ClientPackage = $clientZip Windows10LegacyClientPackage = $windows10ClientZip
ClientSha256 = (Get-FileHash -LiteralPath $clientZip -Algorithm SHA256).Hash Windows10LegacyClientSha256 = (Get-FileHash -LiteralPath $windows10ClientZip -Algorithm SHA256).Hash
Windows11ClientPackage = $windows11ClientZip
Windows11ClientSha256 = (Get-FileHash -LiteralPath $windows11ClientZip -Algorithm SHA256).Hash
LinuxClientPackage = $linuxClientZip LinuxClientPackage = $linuxClientZip
LinuxClientSha256 = (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash LinuxClientSha256 = (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash
ServerPackage = $serverZip ServerPackage = $serverZip
+7 -2
View File
@@ -15,7 +15,8 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$tagName = "v$Version" $tagName = "v$Version"
$assetPaths = @( $assetPaths = @(
(Join-Path $ReleaseDirectory "sgu-client-bootstrap-$Version.zip"), (Join-Path $ReleaseDirectory "sgu-windows10-legacy-client-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-windows11-client-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-server-bootstrap-$Version.zip"), (Join-Path $ReleaseDirectory "sgu-server-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-linux-client-bootstrap-$Version.zip"), (Join-Path $ReleaseDirectory "sgu-linux-client-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-azure-infrastructure-$Version.zip"), (Join-Path $ReleaseDirectory "sgu-azure-infrastructure-$Version.zip"),
@@ -111,7 +112,9 @@ 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. - **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-windows10-legacy-client-bootstrap-$Version.zip`: perfil directo para Windows 10 de laboratorio, con las correcciones de NIC privada, límites de cuentas locales y compatibilidad de sus APIs heredadas.
- `sgu-windows11-client-bootstrap-$Version.zip`: perfil completo para Windows 11; conserva el enrolamiento directo y añade Azure P2S con certificado de máquina y entrada previa al inicio de sesión.
- Ambos clientes comparten los mismos binarios, seguridad mTLS, Credential Provider, cuenta estándar, RustDesk, supervisión y autorreparación; el manifiesto impide ejecutar accidentalmente el paquete de la otra versión de Windows.
- En clientes Hyper-V con dos NIC, el bootstrap selecciona la red privada sin puerta de enlace, solicita o acepta la IP fija del cliente, espera a que la dirección y WinRM estén disponibles y conserva en pantalla y archivo cualquier error de enrolamiento. - En clientes Hyper-V con dos NIC, el bootstrap selecciona la red privada sin puerta de enlace, solicita o acepta la IP fija del cliente, espera a que la dirección y WinRM estén disponibles y conserva en pantalla y archivo cualquier error de enrolamiento.
- `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-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. - `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.
@@ -120,6 +123,8 @@ Bootstrap reproducible para el laboratorio SGU.
- 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 cada grupo dentro de la OU de su rol y migra idempotentemente cualquier grupo heredado sin cambiar su SID. - 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 cada grupo dentro de la OU de su rol y migra idempotentemente cualquier grupo heredado sin cambiar su SID.
- El Auth Broker resuelve la dirección guardada de administrativos y docentes mediante `GetDireccion`, `GetLocalidadListado` y `GetColoniasListado`, evitando conservar los valores transitorios `Seleccione...` de los controles dinámicos de SGU. - El Auth Broker resuelve la dirección guardada de administrativos y docentes mediante `GetDireccion`, `GetLocalidadListado` y `GetColoniasListado`, evitando conservar los valores transitorios `Seleccione...` de los controles dinámicos de SGU.
- El enrolamiento y la reparación de clientes Windows crean y verifican idempotentemente la cuenta local estándar `alumno`, sin pertenencia al grupo de administradores. - El enrolamiento y la reparación de clientes Windows crean y verifican idempotentemente la cuenta local estándar `alumno`, sin pertenencia al grupo de administradores.
- La descripción de la cuenta local administrada respeta el límite de 48 caracteres de Windows 10 Enterprise.
- La validación de expiración de contraseña usa el indicador de cuenta compatible con Windows 10 y 11, en lugar de una propiedad que Windows 10 no expone.
- 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 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. - 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.
- 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.
+25 -2
View File
@@ -5,7 +5,15 @@ param()
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$userName = 'alumno' $userName = 'alumno'
$plainTextPassword = 'ingenieria' $plainTextPassword = 'ingenieria'
$description = 'Cuenta local estandar de recuperacion para equipos SGU' $description = 'Cuenta local estandar SGU para recuperacion'
$passwordNeverExpiresFlag = 0x10000
function Get-LocalUserFlags {
param([Parameter(Mandatory)][string]$Name)
$directoryEntry = [ADSI]("WinNT://$env:COMPUTERNAME/$Name,user")
return [int]$directoryEntry.InvokeGet('UserFlags')
}
$identity = [Security.Principal.WindowsIdentity]::GetCurrent() $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity) $principal = [Security.Principal.WindowsPrincipal]::new($identity)
@@ -40,6 +48,16 @@ try {
-Description $description | Out-Null -Description $description | Out-Null
} }
# Windows 10's Get-LocalUser object has PasswordExpires but does not expose
# PasswordNeverExpires. Enforce and verify the underlying UF_DONT_EXPIRE_PASSWD
# flag so the result is consistent across Windows 10 and Windows 11.
$directoryEntry = [ADSI]("WinNT://$env:COMPUTERNAME/$userName,user")
$userFlags = [int]$directoryEntry.InvokeGet('UserFlags')
if (($userFlags -band $passwordNeverExpiresFlag) -eq 0) {
$directoryEntry.InvokeSet('UserFlags', ($userFlags -bor $passwordNeverExpiresFlag))
$directoryEntry.CommitChanges()
}
$user = Get-LocalUser -Name $userName -ErrorAction Stop $user = Get-LocalUser -Name $userName -ErrorAction Stop
$administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544') $administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
$usersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-545') $usersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-545')
@@ -74,11 +92,16 @@ if (@($verifiedAdministrators).SID.Value -contains $verifiedUser.SID.Value) {
if ($verifiedUsers.SID.Value -notcontains $verifiedUser.SID.Value) { if ($verifiedUsers.SID.Value -notcontains $verifiedUser.SID.Value) {
throw "The local account '$userName' does not belong to the local Users group." throw "The local account '$userName' does not belong to the local Users group."
} }
$verifiedPasswordNeverExpires =
((Get-LocalUserFlags -Name $userName) -band $passwordNeverExpiresFlag) -ne 0
if (-not $verifiedPasswordNeverExpires) {
throw "The local account '$userName' password is not configured to never expire."
}
[pscustomobject]@{ [pscustomobject]@{
UserName = $verifiedUser.Name UserName = $verifiedUser.Name
Enabled = $verifiedUser.Enabled Enabled = $verifiedUser.Enabled
IsAdministrator = $false IsAdministrator = $false
IsStandardUser = $true IsStandardUser = $true
PasswordNeverExpires = $verifiedUser.PasswordNeverExpires PasswordNeverExpires = $verifiedPasswordNeverExpires
} }
+9 -2
View File
@@ -4,5 +4,12 @@ set "SGU_BOOTSTRAP_IP=%~1"
set "SGU_VPN_PACKAGE=%~2" set "SGU_VPN_PACKAGE=%~2"
set "SGU_VPN_PFX=%~3" set "SGU_VPN_PFX=%~3"
set "SGU_VPN_ROOT=%~4" 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" powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Invoke-SguClientBootstrap.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-PauseOnError','-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% set "SGU_EXIT_CODE=%errorlevel%"
if not "%SGU_EXIT_CODE%"=="0" (
echo.
echo SGU Windows 11 Azure enrollment did not complete. Review:
echo C:\ProgramData\SGU\Bootstrap\Client\latest-error.log
pause
)
exit /b %SGU_EXIT_CODE%
+13 -3
View File
@@ -20,6 +20,7 @@ $interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\P
$settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json' $settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json'
$issues = [Collections.Generic.List[string]]::new() $issues = [Collections.Generic.List[string]]::new()
$standardLocalUserName = 'alumno' $standardLocalUserName = 'alumno'
$passwordNeverExpiresFlag = 0x10000
$computer = Get-CimInstance Win32_ComputerSystem $computer = Get-CimInstance Win32_ComputerSystem
if ($RequireDomainJoined -and -not $computer.PartOfDomain) { if ($RequireDomainJoined -and -not $computer.PartOfDomain) {
@@ -93,8 +94,7 @@ $standardLocalUserPresent = [bool]$standardLocalUser
$standardLocalUserEnabled = $standardLocalUserPresent -and $standardLocalUser.Enabled $standardLocalUserEnabled = $standardLocalUserPresent -and $standardLocalUser.Enabled
$standardLocalUserIsAdministrator = $false $standardLocalUserIsAdministrator = $false
$standardLocalUserInUsersGroup = $false $standardLocalUserInUsersGroup = $false
$standardLocalUserPasswordNeverExpires = $standardLocalUserPasswordNeverExpires = $false
$standardLocalUserPresent -and $standardLocalUser.PasswordNeverExpires
if ($standardLocalUserPresent) { if ($standardLocalUserPresent) {
$administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544') $administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
$usersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-545') $usersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-545')
@@ -106,6 +106,16 @@ if ($standardLocalUserPresent) {
$administratorMembers.SID.Value -contains $standardLocalUser.SID.Value $administratorMembers.SID.Value -contains $standardLocalUser.SID.Value
$standardLocalUserInUsersGroup = $standardLocalUserInUsersGroup =
$standardMembers.SID.Value -contains $standardLocalUser.SID.Value $standardMembers.SID.Value -contains $standardLocalUser.SID.Value
try {
$directoryEntry = [ADSI]("WinNT://$env:COMPUTERNAME/$standardLocalUserName,user")
$userFlags = [int]$directoryEntry.InvokeGet('UserFlags')
$standardLocalUserPasswordNeverExpires =
($userFlags -band $passwordNeverExpiresFlag) -ne 0
}
catch {
# Report the account as invalid when Windows cannot read its flags.
$standardLocalUserPasswordNeverExpires = $false
}
} }
if (-not $standardLocalUserPresent) { if (-not $standardLocalUserPresent) {
$issues.Add("The required standard local user '$standardLocalUserName' is missing.") $issues.Add("The required standard local user '$standardLocalUserName' is missing.")
@@ -120,7 +130,7 @@ elseif (-not $standardLocalUserInUsersGroup) {
$issues.Add("The required standard local user '$standardLocalUserName' does not belong to the local Users group.") $issues.Add("The required standard local user '$standardLocalUserName' does not belong to the local Users group.")
} }
elseif (-not $standardLocalUserPasswordNeverExpires) { elseif (-not $standardLocalUserPasswordNeverExpires) {
$issues.Add("The required standard local user '$standardLocalUserName' does not retain its enrollment password.") $issues.Add("The required standard local user '$standardLocalUserName' password is not configured to never expire.")
} }
$settings = $null $settings = $null
+12
View File
@@ -72,6 +72,8 @@ Describe 'SGU public-cloud network safety' {
'ConnectivityMode') | Should Be $true 'ConnectivityMode') | Should Be $true
((Get-Command $clientBootstrapPath).Parameters.Keys -contains ((Get-Command $clientBootstrapPath).Parameters.Keys -contains
'VpnProfilePackagePath') | Should Be $true 'VpnProfilePackagePath') | Should Be $true
((Get-Command $clientBootstrapPath).Parameters.Keys -contains
'CompatibilityProfile') | Should Be $true
} }
It 'accepts an explicit static IPv4 address for a private Windows adapter' { It 'accepts an explicit static IPv4 address for a private Windows adapter' {
@@ -106,6 +108,16 @@ Describe 'SGU public-cloud network safety' {
$source | Should Match 'Wait-TcpPort -Address \$DomainControllerIPv4Address -Port 5985' $source | Should Match 'Wait-TcpPort -Address \$DomainControllerIPv4Address -Port 5985'
} }
It 'keeps legacy and modern Windows package profiles isolated by build' {
$source = Get-Content -LiteralPath $clientBootstrapPath -Raw
$source.Contains("if (`$CompatibilityProfile -eq 'Windows10Legacy' -and `$windowsBuild -ge 22000)") |
Should Be $true
$source.Contains("if (`$CompatibilityProfile -eq 'Windows11Modern' -and `$windowsBuild -lt 22000)") |
Should Be $true
$source.Contains("if (`$CompatibilityProfile -eq 'Windows10Legacy' -and `$ConnectivityMode -eq 'AzureP2S')") |
Should Be $true
}
It 'uses an all-user machine-certificate VPN profile' { It 'uses an all-user machine-certificate VPN profile' {
$source = Get-Content -LiteralPath $azureClientPath -Raw $source = Get-Content -LiteralPath $azureClientPath -Raw
$source | Should Match '-AuthenticationMethod MachineCertificate' $source | Should Match '-AuthenticationMethod MachineCertificate'
+65
View File
@@ -0,0 +1,65 @@
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$localUserScriptPath = Join-Path $repositoryRoot 'scripts\Set-SguStandardLocalUser.ps1'
$enrollmentTestScriptPath = Join-Path $repositoryRoot 'scripts\Test-SguClientEnrollment.ps1'
$packageScriptPath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1'
$releaseScriptPath = Join-Path $repositoryRoot 'scripts\Publish-GiteaRelease.ps1'
$azureLauncherPath = Join-Path $repositoryRoot 'scripts\Start-SguAzureClientEnrollment.cmd'
$tokens = $null
$parseErrors = $null
$scriptAst = [Management.Automation.Language.Parser]::ParseFile(
$localUserScriptPath,
[ref]$tokens,
[ref]$parseErrors)
if ($parseErrors.Count -gt 0) {
throw ($parseErrors -join [Environment]::NewLine)
}
$descriptionAssignment = $scriptAst.Find({
param($node)
$node -is [Management.Automation.Language.AssignmentStatementAst] -and
$node.Left.Extent.Text -eq '$description'
}, $true)
$description = $descriptionAssignment.Right.Extent.Text.Trim("'")
Describe 'SGU Windows client enrollment scripts' {
It 'keeps the local-user description within the Windows 10 limit' {
($description.Length -le 48) | Should Be $true
}
It 'declares the managed local student account' {
$source = Get-Content -LiteralPath $localUserScriptPath -Raw
$source | Should Match "\$userName = 'alumno'"
$source | Should Match "\$plainTextPassword = 'ingenieria'"
}
It 'uses the cross-version Windows account flag for password expiration' {
$localUserSource = Get-Content -LiteralPath $localUserScriptPath -Raw
$enrollmentTestSource = Get-Content -LiteralPath $enrollmentTestScriptPath -Raw
$localUserSource | Should Match '\$passwordNeverExpiresFlag = 0x10000'
$enrollmentTestSource | Should Match '\$passwordNeverExpiresFlag = 0x10000'
$localUserSource | Should Not Match '\$verifiedUser\.PasswordNeverExpires'
$enrollmentTestSource | Should Not Match '\$standardLocalUser\.PasswordNeverExpires'
}
It 'publishes separate legacy Windows 10 and modern Windows 11 artifacts' {
$packageSource = Get-Content -LiteralPath $packageScriptPath -Raw
$releaseSource = Get-Content -LiteralPath $releaseScriptPath -Raw
$packageSource | Should Match 'sgu-windows10-legacy-client-bootstrap-\$Version'
$packageSource | Should Match 'sgu-windows11-client-bootstrap-\$Version'
$packageSource | Should Match '-CompatibilityProfile Windows10Legacy'
$packageSource | Should Match '-CompatibilityProfile Windows11Modern'
$releaseSource | Should Match 'sgu-windows10-legacy-client-bootstrap-\$Version\.zip'
$releaseSource | Should Match 'sgu-windows11-client-bootstrap-\$Version\.zip'
}
It 'keeps Azure P2S in the modern Windows 11 artifact' {
$packageSource = Get-Content -LiteralPath $packageScriptPath -Raw
$azureLauncher = Get-Content -LiteralPath $azureLauncherPath -Raw
$packageSource.Contains("Join-Path `$windows11ClientRoot 'Start-SguAzureClientEnrollment.cmd'") |
Should Be $true
$packageSource.Contains("Join-Path `$windows10ClientRoot 'Start-SguAzureClientEnrollment.cmd'") |
Should Be $false
$azureLauncher | Should Match '-PauseOnError'
}
}