diff --git a/docs/release-0.6.2.md b/docs/release-0.6.2.md new file mode 100644 index 0000000..9c6e0d2 --- /dev/null +++ b/docs/release-0.6.2.md @@ -0,0 +1,36 @@ +# SGU Credential Provider 0.6.2 + +Continúa la serie publicada después de `0.6.1`. + +## Cambios + +- Se agrega despliegue automático e idempotente de FSLogix 26.08 mediante una + GPO de inicio de equipo ligada a `OU=Laboratorio`. +- El publicador verifica la firma Authenticode de Microsoft y el SHA-256 del + instalador antes de copiarlo al SYSVOL. +- Los equipos que estaban apagados reciben e instalan FSLogix en su siguiente + arranque; no se emplean tareas programadas. +- El script queda incluido en el paquete de servidor y la guía de roaming + documenta su publicación y comprobación. + +## Validación del despliegue + +- La GPO `SGU - FSLogix client deployment` está enlazada y sus versiones de AD + y SYSVOL coinciden. +- El instalador publicado conserva firma válida de Microsoft y SHA-256 + `0BAF7FE8195571060F0361B351E58FE6CD422AE521A96BAB3E66D900544E8A30`. +- `AF-02` y `AF-03` aplicaron la GPO y ejecutan `frxsvc` con FSLogix + `3.26.826.17182`. +- Azure Storage permanece en `defaultAction=Deny` y sólo autoriza + `200.13.89.0/24`. + +## Descargas + +- `sgu-windows-client-bootstrap-0.6.2.zip` +- `sgu-server-bootstrap-0.6.2.zip` +- `sgu-linux-client-bootstrap-0.6.2.zip` +- `sgu-azure-infrastructure-0.6.2.zip` +- `SHA256SUMS-0.6.2.txt` + +Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen +credenciales, claves de almacenamiento ni artefactos temporales del despliegue. diff --git a/docs/user-roaming.md b/docs/user-roaming.md index f758362..a756a23 100644 --- a/docs/user-roaming.md +++ b/docs/user-roaming.md @@ -115,7 +115,7 @@ dedicada para evitar que una cuenta de equipo sin agente pierda Kerberos a los 30 días. Si la política exige rotación, use periódicamente el procedimiento oficial `Update-AzStorageAccountADObjectPassword` y no cambie sólo un lado. -## 3. Instalar FSLogix en las imágenes Windows +## 3. Instalar FSLogix en los clientes Windows FSLogix no forma parte de Windows y su licencia debe validarse para las ediciones y suscripciones usadas. Descargue `FSLogixAppsSetup.exe` directamente de @@ -132,6 +132,22 @@ imagen ya contiene FSLogix, el instalador puede omitirse. Microsoft recomienda mantener la versión más reciente; FSLogix no se actualiza mediante Windows Update. +Para cubrir también equipos ya unidos al dominio —incluidos los que estaban +apagados durante el despliegue— publique el instalador como script de inicio de +equipo: + +```powershell +.\payload\scripts\Publish-SguFsLogixClientDeployment.ps1 +``` + +El script descarga el paquete oficial FSLogix 26.08, valida la firma +Authenticode de Microsoft y el SHA-256 esperado, y crea la GPO +`SGU - FSLogix client deployment` ligada a `OU=Laboratorio`. El ejecutable queda +en el SYSVOL de esa GPO y se instala de forma idempotente durante el arranque; +no usa tareas programadas ni requiere que el equipo estuviera encendido al +publicarla. Si `frxsvc` y `frx.exe` ya existen, el script de inicio termina sin +reinstalar. + ## 4. Validar un piloto Después de `gpupdate /force` y reiniciar un cliente: @@ -141,6 +157,7 @@ Resolve-DnsName "$($azure.UserRoamingStorageAccountName).file.core.windows.net" Test-NetConnection "$($azure.UserRoamingStorageAccountName).file.core.windows.net" -Port 445 Get-Service frxsvc Get-ItemProperty 'HKLM:\SOFTWARE\FSLogix\Profiles' +Get-ItemProperty 'HKLM:\SOFTWARE\SGU\FSLogixDeployment' & 'C:\Program Files\FSLogix\Apps\frx.exe' list-redirects ``` diff --git a/scripts/New-SguBootstrapPackages.ps1 b/scripts/New-SguBootstrapPackages.ps1 index 1a0e050..dde2468 100644 --- a/scripts/New-SguBootstrapPackages.ps1 +++ b/scripts/New-SguBootstrapPackages.ps1 @@ -201,6 +201,7 @@ $serverScripts = @( 'Get-SguUsageReport.ps1', 'Get-SguBrokerLog.ps1', 'Install-SguDomainMonitoring.ps1', + 'Publish-SguFsLogixClientDeployment.ps1', 'Install-SguRustDeskClient.ps1', 'Install-SguRustDeskLinuxEnrollment.ps1', 'Install-SguRustDeskServer.ps1', diff --git a/scripts/Publish-SguFsLogixClientDeployment.ps1 b/scripts/Publish-SguFsLogixClientDeployment.ps1 new file mode 100644 index 0000000..f41ee34 --- /dev/null +++ b/scripts/Publish-SguFsLogixClientDeployment.ps1 @@ -0,0 +1,140 @@ +#Requires -Version 5.1 +[CmdletBinding(SupportsShouldProcess)] +param( + [string]$GpoName = 'SGU - FSLogix client deployment', + [string]$LaboratoryOuDn, + [uri]$InstallerZipUri = 'https://download.microsoft.com/download/ae6d2014-e692-45fa-a88b-ee552567cdc1/FSLogix_26.08.zip', + [string]$ExpectedInstallerSha256 = '0BAF7FE8195571060F0361B351E58FE6CD422AE521A96BAB3E66D900544E8A30', + [string]$CacheRoot = "$env:ProgramData\SGU\FSLogixDeployment\26.08" +) + +$ErrorActionPreference = 'Stop' +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$principal = [Security.Principal.WindowsPrincipal]::new($identity) +if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw 'Run the FSLogix GPO deployment from an elevated Windows PowerShell session.' +} +Import-Module ActiveDirectory -ErrorAction Stop +Import-Module GroupPolicy -ErrorAction Stop + +$domain = Get-ADDomain +$domainName = $domain.DNSRoot +$domainDn = $domain.DistinguishedName +if (-not $LaboratoryOuDn) { $LaboratoryOuDn = "OU=Laboratorio,$domainDn" } +Get-ADOrganizationalUnit -Identity $LaboratoryOuDn -ErrorAction Stop | Out-Null + +$zipPath = Join-Path $CacheRoot 'FSLogix_26.08.zip' +$extractRoot = Join-Path $CacheRoot 'expanded' +$installerPath = Join-Path $extractRoot 'x64\Release\FSLogixAppsSetup.exe' +if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) { + if (-not $PSCmdlet.ShouldProcess($InstallerZipUri.AbsoluteUri, "Download official FSLogix package to $zipPath")) { return } + New-Item -ItemType Directory -Path $CacheRoot,$extractRoot -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri $InstallerZipUri -OutFile $zipPath -UseBasicParsing + Expand-Archive -LiteralPath $zipPath -DestinationPath $extractRoot -Force +} +if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) { + throw "The FSLogix x64 installer is missing after extraction: $installerPath" +} +$signature = Get-AuthenticodeSignature -LiteralPath $installerPath +if ($signature.Status -ne [Management.Automation.SignatureStatus]::Valid -or + -not $signature.SignerCertificate -or + $signature.SignerCertificate.Subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)') { + throw 'FSLogixAppsSetup.exe does not have a valid Microsoft Corporation signature.' +} +$installerHash = (Get-FileHash -LiteralPath $installerPath -Algorithm SHA256).Hash +if ($installerHash -ne $ExpectedInstallerSha256) { + throw "FSLogixAppsSetup.exe SHA-256 mismatch. Expected $ExpectedInstallerSha256; received $installerHash." +} + +$gpo = Get-GPO -Name $GpoName -Domain $domainName -ErrorAction SilentlyContinue +if (-not $gpo) { $gpo = New-GPO -Name $GpoName -Domain $domainName } +$links = @(Get-GPInheritance -Target $LaboratoryOuDn -Domain $domainName).GpoLinks +if (-not ($links | Where-Object DisplayName -eq $GpoName)) { + New-GPLink -Name $GpoName -Target $LaboratoryOuDn -Domain $domainName -LinkEnabled Yes | Out-Null +} + +# Set one ordinary machine value through the supported cmdlet. Besides providing +# a deployment marker, this initializes the computer half of the GPO and its +# Registry client-side extension before the Scripts extension is added below. +Set-GPRegistryValue -Name $GpoName -Domain $domainName ` + -Key 'HKLM\SOFTWARE\SGU\FSLogixDeployment' -ValueName 'Version' ` + -Type String -Value '26.08-3.26.826.17182' | Out-Null +$gpo = Get-GPO -Name $GpoName -Domain $domainName +$guid = $gpo.Id.ToString('B').ToUpperInvariant() +$gpoRoot = "\\$domainName\SYSVOL\$domainName\Policies\$guid" +$scriptsRoot = Join-Path $gpoRoot 'Machine\Scripts' +$startupRoot = Join-Path $scriptsRoot 'Startup' +New-Item -ItemType Directory -Path $startupRoot -Force | Out-Null +Copy-Item -LiteralPath $installerPath -Destination (Join-Path $startupRoot 'FSLogixAppsSetup.exe') -Force + +$startupPowerShell = @' +$ErrorActionPreference = 'Stop' +$appsRoot = Join-Path $env:ProgramFiles 'FSLogix\Apps' +$frx = Join-Path $appsRoot 'frx.exe' +if ((Get-Service frxsvc -ErrorAction SilentlyContinue) -and (Test-Path -LiteralPath $frx)) { exit 0 } +$source = Join-Path $PSScriptRoot 'FSLogixAppsSetup.exe' +$signature = Get-AuthenticodeSignature -LiteralPath $source +if ($signature.Status -ne 'Valid' -or -not $signature.SignerCertificate -or + $signature.SignerCertificate.Subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)') { exit 10 } +$targetRoot = Join-Path $env:ProgramData 'SGU\FSLogix' +New-Item -ItemType Directory -Path $targetRoot -Force | Out-Null +$target = Join-Path $targetRoot 'FSLogixAppsSetup.exe' +Copy-Item -LiteralPath $source -Destination $target -Force +$log = Join-Path $targetRoot 'install.log' +$process = Start-Process -FilePath $target -ArgumentList @('/install','/quiet','/norestart','/log',"`"$log`"") -Wait -PassThru +if ($process.ExitCode -notin @(0,1641,3010)) { exit $process.ExitCode } +if (-not (Get-Service frxsvc -ErrorAction SilentlyContinue) -or -not (Test-Path -LiteralPath $frx)) { exit 11 } +exit 0 +'@ +[IO.File]::WriteAllText( + (Join-Path $startupRoot 'Install-SguFsLogix-Startup.ps1'), + $startupPowerShell, + [Text.UTF8Encoding]::new($true)) +$startupCommand = '@echo off' + [Environment]::NewLine + + 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0Install-SguFsLogix-Startup.ps1"' + [Environment]::NewLine + + 'exit /b %ERRORLEVEL%' + [Environment]::NewLine +[IO.File]::WriteAllText( + (Join-Path $startupRoot 'Install-SguFsLogix.cmd'), + $startupCommand, + [Text.Encoding]::ASCII) +$scriptsIni = "[Startup]`r`n0CmdLine=Install-SguFsLogix.cmd`r`n0Parameters=`r`n" +[IO.File]::WriteAllText( + (Join-Path $scriptsRoot 'scripts.ini'), + $scriptsIni, + [Text.Encoding]::Unicode) + +$policyDn = "CN=$guid,CN=Policies,CN=System,$domainDn" +$policy = Get-ADObject -Identity $policyDn -Properties versionNumber,gPCMachineExtensionNames +$scriptExtension = '[{42B5FAAE-6536-11D2-AE5A-0000F87571E3}{40B6664F-4972-11D1-A7CA-0000F87571E3}]' +$extensions = @([regex]::Matches([string]$policy.gPCMachineExtensionNames, '\[\{[0-9A-Fa-f-]{36}\}\{[0-9A-Fa-f-]{36}\}\]') | + ForEach-Object Value) +if ($extensions -notcontains $scriptExtension) { $extensions += $scriptExtension } +$extensionValue = ($extensions | Sort-Object { $_.Substring(1, 38) }) -join '' +$currentVersion = [int64]$policy.versionNumber +$machineVersion = $currentVersion -band 0xFFFF +if ($machineVersion -ge 65535) { throw 'The computer GPO version cannot be incremented further.' } +$newVersion = ($currentVersion -band 0xFFFF0000) -bor ($machineVersion + 1) +$gptIniPath = Join-Path $gpoRoot 'gpt.ini' +$gptLines = @(Get-Content -LiteralPath $gptIniPath) +$versionFound = $false +$gptLines = @($gptLines | ForEach-Object { + if ($_ -match '^Version=') { $versionFound = $true; "Version=$newVersion" } else { $_ } +}) +if (-not $versionFound) { $gptLines += "Version=$newVersion" } +[IO.File]::WriteAllLines($gptIniPath, $gptLines, [Text.Encoding]::ASCII) +Set-ADObject -Identity $policy -Replace @{ + gPCMachineExtensionNames = $extensionValue + versionNumber = [int]$newVersion +} + +$verifiedGpo = Get-GPO -Name $GpoName -Domain $domainName +[pscustomobject]@{ + GpoName = $verifiedGpo.DisplayName + GpoId = $verifiedGpo.Id + LaboratoryOuDn = $LaboratoryOuDn + MachineVersion = $verifiedGpo.Computer.DSVersion + InstallerVersion = '3.26.826.17182' + InstallerSha256 = $installerHash + StartupScript = Join-Path $startupRoot 'Install-SguFsLogix.cmd' +} diff --git a/tests/UserRoaming.Tests.ps1 b/tests/UserRoaming.Tests.ps1 index c6e8f9c..0f4e5e6 100644 --- a/tests/UserRoaming.Tests.ps1 +++ b/tests/UserRoaming.Tests.ps1 @@ -3,6 +3,7 @@ $bicepPath = Join-Path $repositoryRoot 'infra\azure\main.bicep' $deploymentPath = Join-Path $repositoryRoot 'scripts\Deploy-SguAzureInfrastructure.ps1' $configurationPath = Join-Path $repositoryRoot 'scripts\Enable-SguAzureUserRoaming.ps1' $installerPath = Join-Path $repositoryRoot 'scripts\Install-SguFsLogix.ps1' +$gpoDeploymentPath = Join-Path $repositoryRoot 'scripts\Publish-SguFsLogixClientDeployment.ps1' $clientBootstrapPath = Join-Path $repositoryRoot 'scripts\Invoke-SguClientBootstrap.ps1' $azureLauncherPath = Join-Path $repositoryRoot 'scripts\Start-SguAzureClientEnrollment.cmd' $packagePath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1' @@ -11,6 +12,7 @@ foreach ($scriptPath in @( $deploymentPath, $configurationPath, $installerPath, + $gpoDeploymentPath, $clientBootstrapPath, $packagePath)) { $tokens = $null @@ -162,5 +164,17 @@ Describe 'SGU FSLogix image enrollment' { $source = Get-Content -LiteralPath $packagePath -Raw $source | Should Match "'Install-SguFsLogix\.ps1'" $source | Should Match "'Enable-SguAzureUserRoaming\.ps1'" + $source | Should Match "'Publish-SguFsLogixClientDeployment\.ps1'" + } + + It 'publishes a signed idempotent FSLogix computer startup deployment' { + $source = Get-Content -LiteralPath $gpoDeploymentPath -Raw + $source | Should Match 'Get-AuthenticodeSignature' + $source | Should Match 'ExpectedInstallerSha256' + $source | Should Match "'Machine\\Scripts'" + $source | Should Match "'Startup'" + $source | Should Match 'scripts\.ini' + $source | Should Match '42B5FAAE-6536-11D2-AE5A-0000F87571E3' + $source | Should Match 'Install-SguFsLogix-Startup\.ps1' } }