Compare commits

...
2 Commits
Author SHA1 Message Date
alexrg bda8b354b7 Enable public Azure Files roaming deployment 2026-09-18 09:55:39 -06:00
alexrg 7986b76e35 Disable Fast User Switching on domain clients 2026-09-18 09:06:43 -06:00
9 changed files with 135 additions and 20 deletions
+4
View File
@@ -36,6 +36,10 @@ contrario, el contenedor de equipos configurado en AD. Los parámetros
explícitamente. Para otra cuenta, editar el usuario sugerido como explícitamente. Para otra cuenta, editar el usuario sugerido como
`DOMINIO\usuario` o `usuario@dominio`. No se guardan contraseñas. `DOMINIO\usuario` o `usuario@dominio`. No se guardan contraseñas.
El enrolamiento también oculta las entradas de Cambio rápido de usuario mediante
una política de equipo. Docentes, administrativos y alumnos conservan la opción
de cerrar sesión, pero no pueden dejar una sesión abierta para cambiar a otra.
Si la IPv4 proporcionada es pública, el mismo flujo configura automáticamente Si la IPv4 proporcionada es pública, el mismo flujo configura automáticamente
la conectividad directa. Después de autenticar WinRM, el servidor crea o reutiliza la conectividad directa. Después de autenticar WinRM, el servidor crea o reutiliza
un certificado DoH, publica DNS cifrado en TCP 443 y devuelve únicamente su un certificado DoH, publica DNS cifrado en TCP 443 y devuelve únicamente su
+39
View File
@@ -0,0 +1,39 @@
# SGU Credential Provider 0.6.1
Continúa la serie publicada después de `0.6.0`.
## Cambios
- El configurador de roaming admite explícitamente Azure Files mediante endpoint
público con `-EndpointAccess Public`; el modo privado sigue siendo el valor
predeterminado.
- Se corrige la generación del nombre de la identidad de almacenamiento para
respetar el límite de 15 caracteres de una cuenta de equipo de Active
Directory.
- Las ACL de `profiles` y `redirected` aplican mínimo privilegio en la raíz: los
grupos autorizados pueden crear su directorio, pero no modificar ni eliminar
directorios ajenos; `CREATOR OWNER` conserva el control dentro del directorio
propio.
- Se documenta la secuencia de despliegue con firewall de Azure Storage en modo
`Deny`, autorizando finalmente sólo la red institucional `200.13.89.0/24`.
## Validación del despliegue
- Azure Files usa autenticación AD DS con Kerberos AES-256.
- `AF-02` y `AF-03` validaron ticket Kerberos, TCP 445 y lectura/escritura en los
shares `profiles` y `redirected`.
- FSLogix 26.08 (`3.26.826.17182`) quedó instalado y activo en ambos clientes.
- La GPO de FSLogix mantiene una base deshabilitada y se habilita sólo para los
SID de `SGU-Docentes` y `SGU-Administrativos`.
- La GPO de Alumnos redirige únicamente Escritorio y Documentos.
## Descargas
- `sgu-windows-client-bootstrap-0.6.1.zip`
- `sgu-server-bootstrap-0.6.1.zip`
- `sgu-linux-client-bootstrap-0.6.1.zip`
- `sgu-azure-infrastructure-0.6.1.zip`
- `SHA256SUMS-0.6.1.txt`
Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen
credenciales, claves de almacenamiento ni artefactos temporales del despliegue.
+15
View File
@@ -26,6 +26,16 @@ VNet. El modo de enrolamiento público directo no puede usar estos recursos y el
script de despliegue exige `-DeployUserRoaming $false` cuando no se despliega el script de despliegue exige `-DeployUserRoaming $false` cuando no se despliega el
gateway P2S. gateway P2S.
Para una cuenta SMB ya existente cuyo endpoint público se limite mediante el
firewall de Storage a las redes institucionales, el configurador admite
`-EndpointAccess Public`. En ese modo no exige una dirección RFC1918, pero sigue
comprobando resolución DNS y TCP 445. La creación de los shares y las reglas de
red de la cuenta existente se realiza antes de ejecutar el configurador. El host
que aplica la integración AD/ACL debe conservar acceso temporal a SMB durante
esa ejecución; después se establece `defaultAction=Deny` y se deja únicamente la
red institucional autorizada. En el despliegue LCI actual esa regla final es
`200.13.89.0/24`.
Azure Files se integra con el AD DS SGU, no con cuentas o claves guardadas en 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 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 privilegio con `CREATOR OWNER`, administradores y los grupos del rol, y permiso
@@ -33,6 +43,11 @@ SMB predeterminado para identidades autenticadas. La ACL NTFS mantiene aislado
el contenido de cada usuario. La clave de la cuenta se usa en memoria sólo para el contenido de cada usuario. La clave de la cuenta se usa en memoria sólo para
crear las ACL iniciales y no se escribe en el dominio, en GPO ni en disco. crear las ACL iniciales y no se escribe en el dominio, en GPO ni en disco.
Los grupos de rol sólo pueden enumerar la raíz y crear su propio directorio; no
reciben `Modify` en la raíz compartida. El directorio creado hereda `CREATOR
OWNER`, de modo que un usuario no puede modificar ni eliminar el contenedor de
otro usuario.
## 1. Desplegar la infraestructura ## 1. Desplegar la infraestructura
El soporte está activado por omisión para despliegues P2S: El soporte está activado por omisión para despliegues P2S:
+25 -16
View File
@@ -21,6 +21,8 @@ param(
[string]$AdministrativeGroupName = 'SGU-Administrativos', [string]$AdministrativeGroupName = 'SGU-Administrativos',
[string]$StudentGpoName = 'SGU - AL redirected folders', [string]$StudentGpoName = 'SGU - AL redirected folders',
[string]$StaffGpoName = 'SGU - AD-DO FSLogix profiles', [string]$StaffGpoName = 'SGU - AD-DO FSLogix profiles',
[ValidateSet('Private', 'Public')]
[string]$EndpointAccess = 'Private',
[ValidateRange(1024, 1048576)] [ValidateRange(1024, 1048576)]
[int]$FsLogixProfileSizeMiB = 30000, [int]$FsLogixProfileSizeMiB = 30000,
[string]$AzFilesHybridModulePath, [string]$AzFilesHybridModulePath,
@@ -76,13 +78,13 @@ function Import-SguAzFilesHybrid {
function Get-SguStorageSamAccountName { function Get-SguStorageSamAccountName {
param([Parameter(Mandatory)][string]$StorageName) param([Parameter(Mandatory)][string]$StorageName)
if ($StorageName.Length -le 20) { if ($StorageName.Length -le 15) {
return $StorageName return $StorageName
} }
$sha256 = [Security.Cryptography.SHA256]::Create() $sha256 = [Security.Cryptography.SHA256]::Create()
try { try {
$hash = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($StorageName)) $hash = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($StorageName))
$suffix = ([BitConverter]::ToString($hash) -replace '-', '').Substring(0, 15).ToLowerInvariant() $suffix = ([BitConverter]::ToString($hash) -replace '-', '').Substring(0, 10).ToLowerInvariant()
return "sgufs$suffix" return "sgufs$suffix"
} }
finally { finally {
@@ -322,15 +324,19 @@ if ($PSCmdlet.ShouldProcess($StorageAccountName, 'Grant authenticated AD identit
-DefaultSharePermission StorageFileDataSmbShareContributor -DefaultSharePermission StorageFileDataSmbShareContributor
} }
$privateAddresses = @(Resolve-DnsName -Name $fileEndpointHost -Type A -ErrorAction Stop | $endpointAddresses = @(Resolve-DnsName -Name $fileEndpointHost -Type A -ErrorAction Stop |
Where-Object IPAddress | Select-Object -ExpandProperty IPAddress) Where-Object IPAddress | Select-Object -ExpandProperty IPAddress)
if ($privateAddresses.Count -eq 0 -or @($privateAddresses | Where-Object { if ($endpointAddresses.Count -eq 0) {
$_ -match '^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)' throw "$fileEndpointHost did not resolve to an IPv4 address."
}).Count -eq 0) { }
$privateAddresses = @($endpointAddresses | Where-Object {
$_ -match '^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)'
})
if ($EndpointAccess -eq 'Private' -and $privateAddresses.Count -eq 0) {
throw "$fileEndpointHost did not resolve to a private endpoint. Verify the privatelink.file.core.windows.net VNet link and the DC Azure DNS forwarder." throw "$fileEndpointHost did not resolve to a private endpoint. Verify the privatelink.file.core.windows.net VNet link and the DC Azure DNS forwarder."
} }
if (-not (Test-NetConnection -ComputerName $fileEndpointHost -Port 445 -InformationLevel Quiet)) { if (-not (Test-NetConnection -ComputerName $fileEndpointHost -Port 445 -InformationLevel Quiet)) {
throw "The domain controller cannot reach $fileEndpointHost on TCP 445 through the private endpoint." throw "The domain controller cannot reach $fileEndpointHost on TCP 445 using the selected $EndpointAccess endpoint."
} }
$storageKey = @(Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName ` $storageKey = @(Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName `
@@ -344,23 +350,24 @@ $storageCredential = [PSCredential]::new(
(ConvertTo-SecureString -String $storageKey -AsPlainText -Force)) (ConvertTo-SecureString -String $storageKey -AsPlainText -Force))
$profilesSharePath = "\\$fileEndpointHost\$FsLogixProfilesShareName" $profilesSharePath = "\\$fileEndpointHost\$FsLogixProfilesShareName"
$redirectedFoldersSharePath = "\\$fileEndpointHost\$RedirectedFoldersShareName" $redirectedFoldersSharePath = "\\$fileEndpointHost\$RedirectedFoldersShareName"
$perUserRootRights = [Security.AccessControl.FileSystemRights]::CreateDirectories -bor
[Security.AccessControl.FileSystemRights]::ListDirectory -bor
[Security.AccessControl.FileSystemRights]::ReadAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadExtendedAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadPermissions -bor
[Security.AccessControl.FileSystemRights]::Traverse -bor
[Security.AccessControl.FileSystemRights]::Synchronize
try { try {
if ($PSCmdlet.ShouldProcess($profilesSharePath, 'Apply isolated FSLogix root ACLs')) { if ($PSCmdlet.ShouldProcess($profilesSharePath, 'Apply isolated FSLogix root ACLs')) {
Set-SguAzureFileRootAcl -UncPath $profilesSharePath -Credential $storageCredential ` Set-SguAzureFileRootAcl -UncPath $profilesSharePath -Credential $storageCredential `
-DomainAdminsSid $domainAdminsSid ` -DomainAdminsSid $domainAdminsSid `
-ContributorSids @($professorGroup.SID, $administrativeGroup.SID) -ContributorSids @($professorGroup.SID, $administrativeGroup.SID) `
-ContributorRights $perUserRootRights
} }
if ($PSCmdlet.ShouldProcess($redirectedFoldersSharePath, 'Apply isolated student-folder root ACLs')) { if ($PSCmdlet.ShouldProcess($redirectedFoldersSharePath, 'Apply isolated student-folder root ACLs')) {
$studentRootRights = [Security.AccessControl.FileSystemRights]::CreateDirectories -bor
[Security.AccessControl.FileSystemRights]::ListDirectory -bor
[Security.AccessControl.FileSystemRights]::ReadAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadExtendedAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadPermissions -bor
[Security.AccessControl.FileSystemRights]::Traverse -bor
[Security.AccessControl.FileSystemRights]::Synchronize
Set-SguAzureFileRootAcl -UncPath $redirectedFoldersSharePath -Credential $storageCredential ` Set-SguAzureFileRootAcl -UncPath $redirectedFoldersSharePath -Credential $storageCredential `
-DomainAdminsSid $domainAdminsSid -ContributorSids @($studentGroup.SID) ` -DomainAdminsSid $domainAdminsSid -ContributorSids @($studentGroup.SID) `
-ContributorRights $studentRootRights -ContributorRights $perUserRootRights
} }
} }
finally { finally {
@@ -415,6 +422,8 @@ foreach ($staffGroup in @($professorGroup, $administrativeGroup)) {
[pscustomobject]@{ [pscustomobject]@{
StorageAccountName = $StorageAccountName StorageAccountName = $StorageAccountName
FileEndpoint = $fileEndpointHost FileEndpoint = $fileEndpointHost
EndpointAccess = $EndpointAccess
EndpointAddresses = $endpointAddresses
PrivateEndpointAddresses = $privateAddresses PrivateEndpointAddresses = $privateAddresses
DirectoryService = $directoryService DirectoryService = $directoryService
KerberosEncryption = 'AES256' KerberosEncryption = 'AES256'
+8
View File
@@ -314,6 +314,11 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
-Value 1 ` -Value 1 `
-PropertyType DWord ` -PropertyType DWord `
-Force | Out-Null -Force | Out-Null
New-ItemProperty -Path $interactiveLogonPolicyPath `
-Name HideFastUserSwitching `
-Value 1 `
-PropertyType DWord `
-Force | Out-Null
if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) { if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
} }
@@ -344,6 +349,9 @@ catch {
LastSignedInUserHidden = (Get-ItemPropertyValue ` LastSignedInUserHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath ` -LiteralPath $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName) -eq 1 -Name DontDisplayLastUserName) -eq 1
FastUserSwitchingHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name HideFastUserSwitching) -eq 1
LocalUserEnumerationDisabled = (Get-ItemPropertyValue ` LocalUserEnumerationDisabled = (Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath ` -LiteralPath $defaultProviderPolicyPath `
-Name EnumerateLocalUsers) -eq 0 -Name EnumerateLocalUsers) -eq 0
@@ -99,6 +99,7 @@ $policies = @(
@{ Key = $credentialProviderPolicyKey; Name = 'DefaultCredentialProvider'; Type = 'String'; Value = $providerClassId }, @{ Key = $credentialProviderPolicyKey; Name = 'DefaultCredentialProvider'; Type = 'String'; Value = $providerClassId },
@{ Key = $credentialProviderPolicyKey; Name = 'EnumerateLocalUsers'; Type = 'DWord'; Value = 0 }, @{ Key = $credentialProviderPolicyKey; Name = 'EnumerateLocalUsers'; Type = 'DWord'; Value = 0 },
@{ Key = $interactiveLogonPolicyKey; Name = 'DontDisplayLastUserName'; Type = 'DWord'; Value = 1 }, @{ Key = $interactiveLogonPolicyKey; Name = 'DontDisplayLastUserName'; Type = 'DWord'; Value = 1 },
@{ Key = $interactiveLogonPolicyKey; Name = 'HideFastUserSwitching'; Type = 'DWord'; Value = 1 },
# Use Windows' native default account image for named user tiles. LogonUI # Use Windows' native default account image for named user tiles. LogonUI
# retains ownership of the anonymous Other user tile and its circular mask. # retains ownership of the anonymous Other user tile and its circular mask.
+15
View File
@@ -92,6 +92,20 @@ if (-not $lastSignedInUserHidden) {
$issues.Add('The last signed-in user is not hidden from LogonUI.') $issues.Add('The last signed-in user is not hidden from LogonUI.')
} }
$fastUserSwitchingHidden = $false
try {
$fastUserSwitchingHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name HideFastUserSwitching `
-ErrorAction Stop) -eq 1
}
catch {
# Report the missing or unreadable policy as a failed enrollment check.
}
if (-not $fastUserSwitchingHidden) {
$issues.Add('Fast User Switching entry points are not hidden.')
}
$localUserEnumerationDisabled = $false $localUserEnumerationDisabled = $false
try { try {
$localUserEnumerationDisabled = (Get-ItemPropertyValue ` $localUserEnumerationDisabled = (Get-ItemPropertyValue `
@@ -279,6 +293,7 @@ $result = [pscustomobject]@{
ProviderBinaryPresent = [bool]$providerBinaryPresent ProviderBinaryPresent = [bool]$providerBinaryPresent
DefaultProviderConfigured = $defaultProviderConfigured DefaultProviderConfigured = $defaultProviderConfigured
LastSignedInUserHidden = $lastSignedInUserHidden LastSignedInUserHidden = $lastSignedInUserHidden
FastUserSwitchingHidden = $fastUserSwitchingHidden
LocalUserEnumerationDisabled = $localUserEnumerationDisabled LocalUserEnumerationDisabled = $localUserEnumerationDisabled
PasswordProviderPreserved = $passwordProviderPreserved PasswordProviderPreserved = $passwordProviderPreserved
StandardLocalUserPresent = $standardLocalUserPresent StandardLocalUserPresent = $standardLocalUserPresent
+11
View File
@@ -4,6 +4,8 @@ $enrollmentTestScriptPath = Join-Path $repositoryRoot 'scripts\Test-SguClientEnr
$packageScriptPath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1' $packageScriptPath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1'
$releaseScriptPath = Join-Path $repositoryRoot 'scripts\Publish-GiteaRelease.ps1' $releaseScriptPath = Join-Path $repositoryRoot 'scripts\Publish-GiteaRelease.ps1'
$azureLauncherPath = Join-Path $repositoryRoot 'scripts\Start-SguAzureClientEnrollment.cmd' $azureLauncherPath = Join-Path $repositoryRoot 'scripts\Start-SguAzureClientEnrollment.cmd'
$credentialProviderInstallerPath = Join-Path $repositoryRoot 'scripts\Install-CredentialProvider.ps1'
$computerPolicyScriptPath = Join-Path $repositoryRoot 'scripts\Set-SguDomainComputerPolicies.ps1'
$tokens = $null $tokens = $null
$parseErrors = $null $parseErrors = $null
@@ -72,4 +74,13 @@ Describe 'SGU Windows client enrollment scripts' {
Should Be $true Should Be $true
$azureLauncher | Should Match '-PauseOnError' $azureLauncher | Should Match '-PauseOnError'
} }
It 'hides Fast User Switching during enrollment and through computer policy' {
(Get-Content -LiteralPath $credentialProviderInstallerPath -Raw) |
Should Match 'HideFastUserSwitching'
(Get-Content -LiteralPath $enrollmentTestScriptPath -Raw) |
Should Match 'FastUserSwitchingHidden'
(Get-Content -LiteralPath $computerPolicyScriptPath -Raw) |
Should Match "Name = 'HideFastUserSwitching'; Type = 'DWord'; Value = 1"
}
} }
+17 -4
View File
@@ -77,6 +77,16 @@ Describe 'SGU Azure user-roaming infrastructure' {
} }
Describe 'SGU role-specific roaming policies' { Describe 'SGU role-specific roaming policies' {
It 'supports an explicitly selected public SMB endpoint while keeping private as the default' {
(((Get-Command $configurationPath).Parameters['EndpointAccess'].Attributes |
Where-Object { $_ -is [Management.Automation.ValidateSetAttribute] }).ValidValues -join ',') |
Should Be 'Private,Public'
$source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match ([regex]::Escape("[string]`$EndpointAccess = 'Private'"))
$source | Should Match ([regex]::Escape("`$EndpointAccess -eq 'Private'"))
$source | Should Match 'selected \$EndpointAccess endpoint'
}
It 'uses an AD computer identity with AES-256 Kerberos' { It 'uses an AD computer identity with AES-256 Kerberos' {
$source = Get-Content -LiteralPath $configurationPath -Raw $source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match "DomainAccountType = 'ComputerAccount'" $source | Should Match "DomainAccountType = 'ComputerAccount'"
@@ -90,6 +100,9 @@ Describe 'SGU role-specific roaming policies' {
$source | Should Match "SecurityIdentifier\]::new\('S-1-3-0'\)" $source | Should Match "SecurityIdentifier\]::new\('S-1-3-0'\)"
$source | Should Match 'PropagationFlags\]::InheritOnly' $source | Should Match 'PropagationFlags\]::InheritOnly'
$source | Should Match 'SetAccessRuleProtection\(\$true, \$false\)' $source | Should Match 'SetAccessRuleProtection\(\$true, \$false\)'
$source | Should Match '\$perUserRootRights'
$source | Should Match 'CreateDirectories'
$source | Should Match 'ContributorRights \$perUserRootRights'
} }
It 'redirects only the student Desktop and Documents folders' { It 'redirects only the student Desktop and Documents folders' {
@@ -118,11 +131,11 @@ Describe 'SGU role-specific roaming policies' {
It 'derives valid deterministic AD names for long storage account names' { It 'derives valid deterministic AD names for long storage account names' {
$name = Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx' $name = Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx'
$name.Length | Should Be 20 $name.Length | Should Be 15
$name | Should Match '^sgufs[0-9a-f]{15}$' $name | Should Match '^sgufs[0-9a-f]{10}$'
(Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx') | Should Be $name (Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx') | Should Be $name
(Get-SguStorageSamAccountName -StorageName 'sguroam1234567890123') | (Get-SguStorageSamAccountName -StorageName 'sguroam1234567') |
Should Be 'sguroam1234567890123' Should Be 'sguroam1234567'
} }
} }