From c47913e81d2e3fc762bbbddded74250d7a6fcb0a Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Fri, 18 Sep 2026 11:09:44 -0600 Subject: [PATCH] Use FSLogix Cloud Cache for public profile storage --- README.md | 4 +- docs/architecture.md | 4 +- docs/release-0.6.3.md | 42 +++++++++++++ docs/user-roaming.md | 39 ++++++++++-- scripts/Enable-SguAzureUserRoaming.ps1 | 83 +++++++++++++++++++++++++- tests/UserRoaming.Tests.ps1 | 15 +++++ 6 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 docs/release-0.6.3.md diff --git a/README.md b/README.md index c5d1f8f..5415285 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,9 @@ For a public Azure VM, use optional Azure P2S gateway or direct enrollment restricted to explicit public source CIDRs. P2S deployments also provision private Azure Files storage for role-based roaming: `AL` redirects only Documents/Desktop, while `AD` and `DO` -can mount an FSLogix VHDX profile after the post-domain setup step. +use an FSLogix VHDX profile after the post-domain setup step. Existing public +Azure Files deployments automatically use FSLogix Cloud Cache locally instead +of performing all profile I/O across WAN SMB. Never disable the built-in Microsoft password Credential Provider. It is the supported recovery path if a third-party provider fails to load. diff --git a/docs/architecture.md b/docs/architecture.md index 4a3c700..56be2d7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -84,7 +84,9 @@ Azure P2S deployments can add a dedicated, private Azure Files account. Student accounts (`AL`) keep a non-authoritative local profile and redirect only Documents/Desktop. Administrative and professor accounts (`AD`/`DO`) receive an FSLogix VHDX profile container through object-specific settings keyed to their -AD security-group SIDs. The storage account uses AD DS Kerberos with AES-256; +AD security-group SIDs. Private endpoints mount the VHDX directly; public WAN +endpoints use a local FSLogix Cloud Cache synchronized to Azure Files so profile +I/O does not block on every SMB round trip. The storage account uses AD DS Kerberos with AES-256; share-root ACLs use `CREATOR OWNER` so users cannot browse one another's data. The bootstrap staging account is deliberately separate. See [user-roaming.md](user-roaming.md) for rollout and recovery constraints. diff --git a/docs/release-0.6.3.md b/docs/release-0.6.3.md new file mode 100644 index 0000000..6415d1c --- /dev/null +++ b/docs/release-0.6.3.md @@ -0,0 +1,42 @@ +# SGU Credential Provider 0.6.3 + +Corrige el primer inicio de sesión de perfiles completos `AD`/`DO` cuando Azure +Files se consume mediante su endpoint público. + +## Cambios + +- `Enable-SguAzureUserRoaming.ps1` selecciona automáticamente FSLogix Cloud + Cache para endpoints públicos y conserva el montaje directo para endpoints + privados. +- Cloud Cache realiza el I/O del perfil sobre el VHDX local y sincroniza con + Azure Files, evitando que la latencia WAN de SMB bloquee cada operación. +- La GPO elimina de forma efectiva `VHDLocations` al activar `CCDLocations`, + incluyendo equipos que habían aplicado la política directa anterior. +- Se impide el retorno silencioso a un perfil local o temporal cuando FSLogix + no puede registrar el contenedor. +- La documentación agrega capacidad de disco, validación del registro efectivo + y rutas de diagnóstico para ambos modos. + +## Validación del despliegue + +- Se eliminó únicamente el perfil piloto fallido de `AD017045` en `AF-03`; la + cuenta de Active Directory se conservó. +- La GPO `SGU - AD-DO FSLogix profiles` quedó en Cloud Cache para los SID de + `SGU-Administrativos` y `SGU-Docentes`; `VHDLocations` ya no aparece en el + registro efectivo de `AF-03`. +- El nuevo inicio de `AD017045` creó la caché y el proxy locales, agregó la + redirección del perfil al VHDX y completó `LoadProfile` en 4.649 segundos, sin + error 258, perfil temporal ni retorno local. +- Pasaron 70 pruebas Pester, 41 pruebas de `SGU.AuthBroker.Core`, 16 de + `SGU.AuthBroker` y 6 del Credential Provider. La solución compiló sin errores. + +## Descargas + +- `sgu-windows-client-bootstrap-0.6.3.zip` +- `sgu-server-bootstrap-0.6.3.zip` +- `sgu-linux-client-bootstrap-0.6.3.zip` +- `sgu-azure-infrastructure-0.6.3.zip` +- `SHA256SUMS-0.6.3.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 a756a23..c74132b 100644 --- a/docs/user-roaming.md +++ b/docs/user-roaming.md @@ -6,7 +6,7 @@ cada tipo de cuenta SGU: | Rol | Datos conservados | Implementación | | --- | --- | --- | | `AL` | `Desktop` y `Documents` | Redirección de carpetas a Azure Files; el resto del perfil local no es autoritativo y no se copia al iniciar o cerrar sesión. | -| `AD` / `DO` | Perfil completo | Un contenedor VHDX único de FSLogix montado desde Azure Files. | +| `AD` / `DO` | Perfil completo | FSLogix: montaje directo del VHDX con endpoint privado o Cloud Cache local sincronizada con Azure Files cuando el endpoint es público. | Linux conserva su solución nativa de directorios personales; no intenta montar un perfil de Windows. @@ -36,6 +36,22 @@ esa ejecución; después se establece `defaultAction=Deny` y se deja únicamente red institucional autorizada. En el despliegue LCI actual esa regla final es `200.13.89.0/24`. +El modo `-StaffProfileStorageMode Auto` es el predeterminado. Selecciona +`Direct` con un endpoint privado y `CloudCache` con uno público. Cloud Cache +mantiene el VHDX de trabajo en `C:\ProgramData\FSLogix\Cache` y sincroniza sus +cambios con Azure Files, evitando que cada operación del perfil dependa de la +latencia WAN de SMB. `CCDLocations` y `VHDLocations` son mutuamente excluyentes: +el configurador elimina de forma efectiva el valor obsoleto al cambiar de modo, +incluso en clientes que ya habían aplicado la GPO anterior. Puede forzarse un +modo con `-StaffProfileStorageMode Direct` o `CloudCache`, pero `Direct` sobre +un endpoint público sólo debe usarse para diagnóstico. + +Cloud Cache necesita espacio local suficiente para el perfil (30 GB de máximo +por omisión), conserva la caché tras cerrar sesión y exige al menos un proveedor +remoto sano al registrar o desmontar el perfil. La GPO también impide el inicio +con perfil temporal o local cuando FSLogix falla, para que una incidencia no +produzca dos perfiles divergentes silenciosamente. + Azure Files se integra con el AD DS SGU, no con cuentas o claves guardadas en cada cliente. El script configura AES-256 para Kerberos, una ACL de mínimo privilegio con `CREATOR OWNER`, administradores y los grupos del rol, y permiso @@ -97,6 +113,14 @@ $parameters = @{ .\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters ``` +Para reutilizar una cuenta con endpoint público: + +```powershell +$parameters.EndpointAccess = 'Public' +$parameters.StaffProfileStorageMode = 'Auto' # resuelve a CloudCache +.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters +``` + La operación es idempotente. Crea `OU=AzureFilesConfig`, une la cuenta de almacenamiento al bosque y publica dos GPO: @@ -156,15 +180,21 @@ Después de `gpupdate /force` y reiniciar un cliente: 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\FSLogix\Profiles\ObjectSpecific\' Get-ItemProperty 'HKLM:\SOFTWARE\SGU\FSLogixDeployment' & 'C:\Program Files\FSLogix\Apps\frx.exe' list-redirects ``` -La resolución debe devolver la IP privada del endpoint. Inicie primero con un +En modo privado, la resolución debe devolver la IP privada del endpoint; en +modo público debe devolver una dirección pública y TCP 445 debe estar permitido +desde la red institucional. En Cloud Cache compruebe además que existe +`C:\ProgramData\FSLogix\Cache` y que el registro efectivo contiene +`CCDLocations`, pero no `VHDLocations`. Inicie primero con un `AL` nuevo y confirme las ubicaciones de Escritorio/Documentos; luego pruebe un `AD` o `DO` sin perfil local previo y confirme la creación de un VHDX en -`profiles`. No active el borrado de perfiles locales ni despliegue a todos los +`profiles`. Revise `%ProgramData%\FSLogix\Logs\Profile` para confirmar que el +perfil se registró sin `ERROR: 258`, perfil temporal ni retorno a `C:\Users`. +No active el borrado de perfiles locales ni despliegue a todos los usuarios hasta probar inicio, cierre, desconexión de VPN, cuota y restauración. Referencias de Microsoft: @@ -173,4 +203,5 @@ Referencias de Microsoft: - [Permisos SMB para FSLogix](https://learn.microsoft.com/fslogix/how-to-configure-storage-permissions) - [Contenedores de perfil FSLogix](https://learn.microsoft.com/fslogix/how-to-configure-profile-containers) - [Configuración ObjectSpecific de FSLogix](https://learn.microsoft.com/fslogix/how-to-configure-object-specific-settings) +- [Cloud Cache de FSLogix](https://learn.microsoft.com/fslogix/concepts-fslogix-cloud-cache) - [Redirección de carpetas](https://learn.microsoft.com/windows-server/storage/folder-redirection/deploy-folder-redirection) diff --git a/scripts/Enable-SguAzureUserRoaming.ps1 b/scripts/Enable-SguAzureUserRoaming.ps1 index e5440cf..24e9678 100644 --- a/scripts/Enable-SguAzureUserRoaming.ps1 +++ b/scripts/Enable-SguAzureUserRoaming.ps1 @@ -23,6 +23,8 @@ param( [string]$StaffGpoName = 'SGU - AD-DO FSLogix profiles', [ValidateSet('Private', 'Public')] [string]$EndpointAccess = 'Private', + [ValidateSet('Auto', 'Direct', 'CloudCache')] + [string]$StaffProfileStorageMode = 'Auto', [ValidateRange(1024, 1048576)] [int]$FsLogixProfileSizeMiB = 30000, [string]$AzFilesHybridModulePath, @@ -161,6 +163,36 @@ function Set-SguGpoRegistryValue { } } +function Remove-SguGpoRegistryValue { + param( + [Parameter(Mandatory)][string]$GpoName, + [Parameter(Mandatory)][string]$DomainName, + [Parameter(Mandatory)][string]$Server, + [Parameter(Mandatory)][string]$Key, + [Parameter(Mandatory)][string]$ValueName + ) + + if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\$ValueName", 'Remove obsolete policy value')) { + Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server ` + -Key $Key -ValueName $ValueName -ErrorAction SilentlyContinue + } +} + +function Set-SguGpoRegistryValueDeletion { + param( + [Parameter(Mandatory)][string]$GpoName, + [Parameter(Mandatory)][string]$DomainName, + [Parameter(Mandatory)][string]$Server, + [Parameter(Mandatory)][string]$Key, + [Parameter(Mandatory)][string]$ValueName + ) + + Remove-SguGpoRegistryValue -GpoName $GpoName -DomainName $DomainName -Server $Server ` + -Key $Key -ValueName $ValueName + Set-SguGpoRegistryValue -GpoName $GpoName -DomainName $DomainName -Server $Server ` + -Key $Key -ValueName "**del.$ValueName" -Type String -Value '' +} + function Get-SguUnusedDriveName { $used = @(Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Name) foreach ($name in @('Z', 'Y', 'X', 'W', 'V')) { @@ -350,6 +382,12 @@ $storageCredential = [PSCredential]::new( (ConvertTo-SecureString -String $storageKey -AsPlainText -Force)) $profilesSharePath = "\\$fileEndpointHost\$FsLogixProfilesShareName" $redirectedFoldersSharePath = "\\$fileEndpointHost\$RedirectedFoldersShareName" +$resolvedStaffProfileStorageMode = if ($StaffProfileStorageMode -eq 'Auto') { + if ($EndpointAccess -eq 'Public') { 'CloudCache' } else { 'Direct' } +} +else { + $StaffProfileStorageMode +} $perUserRootRights = [Security.AccessControl.FileSystemRights]::CreateDirectories -bor [Security.AccessControl.FileSystemRights]::ListDirectory -bor [Security.AccessControl.FileSystemRights]::ReadAttributes -bor @@ -407,12 +445,47 @@ $fsLogixValues = [ordered]@{ ReAttachIntervalSeconds = @{ Type = 'DWord'; Value = 15 } ReAttachRetryCount = @{ Type = 'DWord'; Value = 3 } SizeInMBs = @{ Type = 'DWord'; Value = $FsLogixProfileSizeMiB } - VHDLocations = @{ Type = 'String'; Value = $profilesSharePath } VolumeType = @{ Type = 'String'; Value = 'VHDX' } } +$cloudCacheOnlyValues = @( + 'CCDLocations', + 'ClearCacheOnLogoff', + 'HealthyProvidersRequiredForRegister', + 'HealthyProvidersRequiredForUnregister', + 'PreventLoginWithFailure', + 'PreventLoginWithTempProfile', + 'VHDCompactDisk' +) +if ($resolvedStaffProfileStorageMode -eq 'CloudCache') { + $fsLogixValues.CCDLocations = @{ + Type = 'String' + Value = "type=smb,name=`"SGU Azure Files`",connectionString=$profilesSharePath" + } + $fsLogixValues.ClearCacheOnLogoff = @{ Type = 'DWord'; Value = 0 } + $fsLogixValues.HealthyProvidersRequiredForRegister = @{ Type = 'DWord'; Value = 1 } + $fsLogixValues.HealthyProvidersRequiredForUnregister = @{ Type = 'DWord'; Value = 1 } + $fsLogixValues.PreventLoginWithFailure = @{ Type = 'DWord'; Value = 1 } + $fsLogixValues.PreventLoginWithTempProfile = @{ Type = 'DWord'; Value = 1 } + $fsLogixValues.VHDCompactDisk = @{ Type = 'DWord'; Value = 0 } +} +else { + $fsLogixValues.VHDLocations = @{ Type = 'String'; Value = $profilesSharePath } +} foreach ($staffGroup in @($professorGroup, $administrativeGroup)) { $objectSpecificKey = "$fsLogixRoot\ObjectSpecific\$($staffGroup.SID.Value)" + if ($resolvedStaffProfileStorageMode -eq 'CloudCache') { + Set-SguGpoRegistryValueDeletion -GpoName $staffGpo.DisplayName -DomainName $domainName ` + -Server $DomainController -Key $objectSpecificKey -ValueName 'VHDLocations' + } + else { + foreach ($obsoleteValue in $cloudCacheOnlyValues) { + Set-SguGpoRegistryValueDeletion -GpoName $staffGpo.DisplayName -DomainName $domainName ` + -Server $DomainController -Key $objectSpecificKey -ValueName $obsoleteValue + } + } foreach ($setting in $fsLogixValues.GetEnumerator()) { + Remove-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName ` + -Server $DomainController -Key $objectSpecificKey -ValueName "**del.$($setting.Key)" Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName ` -Server $DomainController -Key $objectSpecificKey -ValueName $setting.Key ` -Type $setting.Value.Type -Value $setting.Value.Value @@ -423,6 +496,7 @@ foreach ($staffGroup in @($professorGroup, $administrativeGroup)) { StorageAccountName = $StorageAccountName FileEndpoint = $fileEndpointHost EndpointAccess = $EndpointAccess + StaffProfileStorageMode = $resolvedStaffProfileStorageMode EndpointAddresses = $endpointAddresses PrivateEndpointAddresses = $privateAddresses DirectoryService = $directoryService @@ -434,6 +508,11 @@ foreach ($staffGroup in @($professorGroup, $administrativeGroup)) { StudentPolicy = $studentGpo.DisplayName StaffPolicy = $staffGpo.DisplayName StudentBehavior = 'Local non-authoritative profile; Documents and Desktop redirected without Offline Files pinning.' - StaffBehavior = 'FSLogix VHDX profile container for SGU-Docentes and SGU-Administrativos only.' + StaffBehavior = if ($resolvedStaffProfileStorageMode -eq 'CloudCache') { + 'FSLogix Cloud Cache profile container for SGU-Docentes and SGU-Administrativos only; local I/O is synchronized to Azure Files.' + } + else { + 'Direct FSLogix VHDX profile container for SGU-Docentes and SGU-Administrativos only.' + } ExistingStaffLocalProfilesDeleted = [bool]$DeleteExistingStaffLocalProfiles } diff --git a/tests/UserRoaming.Tests.ps1 b/tests/UserRoaming.Tests.ps1 index 0f4e5e6..946d6b2 100644 --- a/tests/UserRoaming.Tests.ps1 +++ b/tests/UserRoaming.Tests.ps1 @@ -89,6 +89,21 @@ Describe 'SGU role-specific roaming policies' { $source | Should Match 'selected \$EndpointAccess endpoint' } + It 'uses local Cloud Cache automatically for public WAN profile storage' { + (((Get-Command $configurationPath).Parameters['StaffProfileStorageMode'].Attributes | + Where-Object { $_ -is [Management.Automation.ValidateSetAttribute] }).ValidValues -join ',') | + Should Be 'Auto,Direct,CloudCache' + $source = Get-Content -LiteralPath $configurationPath -Raw + $source | Should Match "EndpointAccess -eq 'Public'\) \{ 'CloudCache' \}" + $source | Should Match 'CCDLocations' + $source | Should Match 'type=smb,name=' + $source | Should Match "ValueName 'VHDLocations'" + $source | Should Match '\*\*del\.\$ValueName' + $source | Should Match 'HealthyProvidersRequiredForRegister' + $source | Should Match 'PreventLoginWithFailure' + $source | Should Match 'VHDCompactDisk' + } + It 'uses an AD computer identity with AES-256 Kerberos' { $source = Get-Content -LiteralPath $configurationPath -Raw $source | Should Match "DomainAccountType = 'ComputerAccount'"