Enforce SGU sign-in and session policies

This commit is contained in:
2026-09-01 15:48:26 -06:00
parent 0391320a3e
commit a166193b66
9 changed files with 227 additions and 29 deletions
+12 -8
View File
@@ -73,16 +73,20 @@ Every synchronized user also receives `Universidad La Salle` in the AD
The generic SGU credential is rendered as a dedicated branded tile instead of The generic SGU credential is rendered as a dedicated branded tile instead of
being grouped below the anonymous **Other user** tile. Machine policy assigns being grouped below the anonymous **Other user** tile. Machine policy assigns
the SGU CLSID as the default provider while retaining the built-in Microsoft the SGU CLSID as the default provider, hides the last signed-in identity, and
password provider. It enumerates one `CPFT_TILE_IMAGE` and places the disables local-user enumeration while retaining the built-in Microsoft password
`CPFT_LARGE_TEXT` heading immediately after it with `CPFS_DISPLAY_IN_BOTH`, provider and its **Other user** recovery path. It enumerates one
matching Microsoft's supported field ordering. LogonUI owns field typography: `CPFT_TILE_IMAGE` and places the `CPFT_LARGE_TEXT` heading immediately after it
on Windows 10 and 11, the account-name title used by **Other user** is shell UI, with `CPFS_DISPLAY_IN_SELECTED_TILE`. LogonUI owns field typography and vertical
not a style that a generic Credential Provider can request. Do not add a second tile order: on Windows 10 and 11, the account-name title used by **Other user**
tile image or a synthetic Windows account to imitate that title. is shell UI, not a style that a generic Credential Provider can request. Do not
add a second tile image, filter the system password provider, or create a
synthetic Windows account to imitate that title or ordering.
The managed hierarchy is rooted at `OU=Usuarios-SGU`: `Docentes`, `Alumnos`, The managed hierarchy is rooted at `OU=Usuarios-SGU`: `Docentes`, `Alumnos`,
and `Administrativos` are direct child OUs beneath it. and `Administrativos` are direct child OUs beneath it. The domain GPO
`SGU - User session restrictions` is linked to this root and enables the
per-user `DisableLockWorkstation` policy for the complete hierarchy.
Per-user synchronization is serialized inside the broker to prevent concurrent Per-user synchronization is serialized inside the broker to prevent concurrent
create/reset races. Production deployments should run the broker as a gMSA with create/reset races. Production deployments should run the broker as a gMSA with
+38 -10
View File
@@ -57,7 +57,8 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass `
Orden de la transacción: Orden de la transacción:
1. instala .NET y los binarios versionados; 1. instala .NET y los binarios versionados;
2. registra COM y configura SGU como proveedor predeterminado; 2. registra COM, configura SGU como proveedor predeterminado y oculta el
último usuario que cerró sesión;
3. instala el guard de autorreparación; 3. instala el guard de autorreparación;
4. exige health mTLS del broker y ejecuta las comprobaciones locales; 4. exige health mTLS del broker y ejecuta las comprobaciones locales;
5. configura DNS del dominio; 5. configura DNS del dominio;
@@ -65,8 +66,25 @@ Orden de la transacción:
7. al arrancar, el guard habilita RDP, NLA y WinRM y comprueba el estado final. 7. al arrancar, el guard habilita RDP, NLA y WinRM y comprueba el estado final.
La directiva de Windows **Assign a default credential provider** selecciona SGU La directiva de Windows **Assign a default credential provider** selecciona SGU
por defecto. No se instala ningún filtro: el proveedor de contraseña de por defecto. El instalador también habilita **Interactive logon: Don't display
Microsoft permanece disponible para cuentas creadas manualmente y recuperación. last signed-in** y deshabilita **Enumerate local users on domain-joined
computers**. De este modo un usuario SGU desaparece después de cerrar sesión y
permanece el mosaico genérico **Other user**. No se instala ningún filtro: el
proveedor de contraseña de Microsoft permanece disponible para cuentas creadas
manualmente y recuperación.
En el controlador de dominio, aplicar una vez la directiva de usuario al árbol
completo `Usuarios-SGU`:
```powershell
.\Set-SguDomainUserPolicies.ps1
```
El script crea y vincula de forma idempotente el GPO
`SGU - User session restrictions` y configura `DisableLockWorkstation=1` para
Administrativos, Alumnos y Docentes. La directiva elimina la acción manual de
bloqueo; no sustituye otras directivas independientes de protector de pantalla,
inactividad o desconexión de RDP.
## 3. Verificación y reparación ## 3. Verificación y reparación
@@ -82,21 +100,31 @@ Start-ScheduledTask -TaskName SGU-CredentialProvider-EnrollmentGuard
``` ```
Un resultado válido exige simultáneamente binario y registro COM, configuración, Un resultado válido exige simultáneamente binario y registro COM, configuración,
certificados, .NET 10, proveedor SGU predeterminado y proveedor de contraseña de certificados, .NET 10, proveedor SGU predeterminado, último usuario oculto,
Microsoft preservado. El script de reparación se encuentra en enumeración local deshabilitada y proveedor de contraseña de Microsoft
preservado. El script de reparación se encuentra en
`C:\ProgramData\SGU\Enrollment` con ACL exclusiva para `SYSTEM` y `C:\ProgramData\SGU\Enrollment` con ACL exclusiva para `SYSTEM` y
administradores. administradores.
## 4. Experiencia de LogonUI ## 4. Experiencia de LogonUI
El proveedor usa un mosaico genérico dedicado, no una credencial asociada al SID El proveedor usa un mosaico genérico dedicado, no una credencial asociada al SID
anónimo de **Other user**. Windows puede mostrar ahora la imagen institucional y anónimo de **Other user**. Windows muestra la imagen institucional y la etiqueta
la etiqueta `Universidad La Salle · Acceso SGU`, además del encabezado `Universidad La Salle · Acceso SGU`; el encabezado `Acceso institucional SGU`
`Acceso institucional SGU`. El orden visual exacto de los iconos de opciones lo se publica como `CPFT_LARGE_TEXT` solamente en el mosaico seleccionado. LogonUI
decide Windows; lo administrable y exigido es cuál aparece seleccionado por elige la familia, el peso y el tamaño final y no expone opciones de tipografía a
defecto. un Credential Provider.
La configuración soportada garantiza que SGU queda seleccionado por defecto y
que no se conserva el último usuario. El orden vertical exacto de mosaicos lo
decide LogonUI y no tiene una directiva pública. Por seguridad no se filtra ni
se elimina **Other user**: Microsoft recomienda conservar al menos un proveedor
del sistema como ruta de recuperación.
Referencias de Microsoft: Referencias de Microsoft:
- <https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-credentialproviders#defaultcredentialprovider> - <https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-credentialproviders#defaultcredentialprovider>
- <https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-windowslogon#enumeratelocalusersondomainjoinedcomputers>
- <https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-ctrlaltdel#disablelockcomputer>
- <https://learn.microsoft.com/en-us/windows/win32/api/credentialprovider/ne-credentialprovider-credential_provider_field_type>
- <https://learn.microsoft.com/en-us/windows/win32/api/credentialprovider/nf-credentialprovider-icredentialprovidercredential2-getusersid> - <https://learn.microsoft.com/en-us/windows/win32/api/credentialprovider/nf-credentialprovider-icredentialprovidercredential2-getusersid>
+12 -3
View File
@@ -129,9 +129,18 @@ On Windows 10:
-DotNetRuntimeInstallerPath C:\SGUDeploy\prerequisites\dotnet-runtime-10.0.11-win-x64.exe -DotNetRuntimeInstallerPath C:\SGUDeploy\prerequisites\dotnet-runtime-10.0.11-win-x64.exe
``` ```
Use Lithnet's `Invoke-CredUI` test utility when available, or lock the VM and Use Lithnet's `Invoke-CredUI` test utility when available, or sign out/restart
select **Acceso institucional SGU** under sign-in options. Keep the built-in the VM and select **Acceso institucional SGU**. Keep the built-in Windows
Windows password tile visible. password provider available through **Other user**.
On the domain controller, apply the SGU user policy once:
```powershell
.\Set-SguDomainUserPolicies.ps1
```
It links `SGU - User session restrictions` to `OU=Usuarios-SGU` and prevents
managed users from manually locking the workstation.
Before testing through Hyper-V Enhanced Session/RDP, enable the dedicated lab Before testing through Hyper-V Enhanced Session/RDP, enable the dedicated lab
group and Windows PowerShell Remoting: group and Windows PowerShell Remoting:
+6 -5
View File
@@ -90,12 +90,13 @@ contenido. De esta forma se puede preparar una actualización aunque
`LogonUI.exe` todavía tenga cargada la DLL anterior; el reinicio obligatorio `LogonUI.exe` todavía tenga cargada la DLL anterior; el reinicio obligatorio
activa la nueva versión. activa la nueva versión.
El instalador también asigna SGU como proveedor predeterminado mediante la El instalador también asigna SGU como proveedor predeterminado, oculta el último
directiva de equipo de Windows. No modifica ni oculta el proveedor de contraseña usuario que cerró sesión y deshabilita la enumeración de usuarios locales en el
de Microsoft. equipo unido al dominio. No modifica ni oculta el proveedor de contraseña de
Microsoft; **Other user** permanece como ruta de recuperación.
Bloquear el equipo, abrir **Sign-in options** y elegir el icono azul con llave Cerrar sesión o reiniciar el equipo. El acceso SGU debe quedar seleccionado por
del acceso SGU. El proveedor de contraseña de Microsoft debe permanecer visible. defecto y el proveedor de contraseña de Microsoft debe permanecer disponible.
## 5. Verificación ## 5. Verificación
+29 -1
View File
@@ -34,6 +34,7 @@ $settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId" $providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32" $classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' $defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
$interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent() $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity) $principal = [Security.Principal.WindowsPrincipal]::new($identity)
@@ -181,13 +182,34 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
Set-Item -Path $providerRegistryPath -Value 'Universidad La Salle · Acceso SGU' Set-Item -Path $providerRegistryPath -Value 'Universidad La Salle · Acceso SGU'
if (-not $DoNotSetAsDefaultCredentialProvider) { if (-not $DoNotSetAsDefaultCredentialProvider) {
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
}
New-ItemProperty -Path $defaultProviderPolicyPath ` New-ItemProperty -Path $defaultProviderPolicyPath `
-Name DefaultCredentialProvider ` -Name DefaultCredentialProvider `
-Value $providerClassId ` -Value $providerClassId `
-PropertyType String ` -PropertyType String `
-Force | Out-Null -Force | Out-Null
} }
# Do not leave a signed-out SGU identity exposed as a persistent user tile.
# The Microsoft password provider remains registered and supplies Other user.
if (-not (Test-Path -LiteralPath $interactiveLogonPolicyPath)) {
New-Item -Path $interactiveLogonPolicyPath -Force | Out-Null
}
New-ItemProperty -Path $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName `
-Value 1 `
-PropertyType DWord `
-Force | Out-Null
if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
}
New-ItemProperty -Path $defaultProviderPolicyPath `
-Name EnumerateLocalUsers `
-Value 0 `
-PropertyType DWord `
-Force | Out-Null
} }
$defaultProviderConfigured = $false $defaultProviderConfigured = $false
@@ -207,5 +229,11 @@ catch {
SettingsPath = $settingsPath SettingsPath = $settingsPath
Registered = Test-Path -LiteralPath $providerRegistryPath Registered = Test-Path -LiteralPath $providerRegistryPath
DefaultProviderConfigured = $defaultProviderConfigured DefaultProviderConfigured = $defaultProviderConfigured
LastSignedInUserHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName) -eq 1
LocalUserEnumerationDisabled = (Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath `
-Name EnumerateLocalUsers) -eq 0
SystemPasswordProviderPreserved = $true SystemPasswordProviderPreserved = $true
} }
+95
View File
@@ -0,0 +1,95 @@
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$TargetOuDn = 'OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx',
[string]$GpoName = 'SGU - User session restrictions',
[string]$DomainController = $env:COMPUTERNAME
)
$ErrorActionPreference = 'Stop'
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
$policyValueName = 'DisableLockWorkstation'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated Windows PowerShell session on a domain controller or management host.'
}
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
$targetOu = Get-ADOrganizationalUnit `
-Identity $TargetOuDn `
-Server $DomainController `
-ErrorAction Stop
$domainDn = ($targetOu.DistinguishedName -split ',DC=', 2)[1]
if (-not $domainDn) {
throw 'TargetOuDn does not contain a domain distinguished name.'
}
$domainName = ($domainDn -replace ',DC=', '.')
$gpo = Get-GPO -Name $GpoName -Domain $domainName -Server $DomainController -ErrorAction SilentlyContinue
if (-not $gpo -and $PSCmdlet.ShouldProcess($GpoName, 'Create the SGU user policy GPO')) {
$gpo = New-GPO -Name $GpoName -Domain $domainName -Server $DomainController
}
if (-not $gpo) {
throw "The GPO '$GpoName' does not exist and was not created."
}
$existingLink = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $GpoName |
Select-Object -First 1
$existingLinkEnabled = $existingLink -and (
$existingLink.Enabled -eq $true -or
[string]$existingLink.Enabled -eq 'Yes')
if (-not $existingLink) {
if ($PSCmdlet.ShouldProcess($TargetOuDn, "Link and enable '$GpoName'")) {
New-GPLink `
-Name $GpoName `
-Target $TargetOuDn `
-Domain $domainName `
-Server $DomainController `
-LinkEnabled Yes | Out-Null
}
}
elseif (-not $existingLinkEnabled -and
$PSCmdlet.ShouldProcess($TargetOuDn, "Enable the '$GpoName' link")) {
Set-GPLink `
-Name $GpoName `
-Target $TargetOuDn `
-Domain $domainName `
-Server $DomainController `
-LinkEnabled Yes | Out-Null
}
if ($PSCmdlet.ShouldProcess($GpoName, 'Prevent SGU users from manually locking workstations')) {
Set-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $policyKey `
-ValueName $policyValueName `
-Type DWord `
-Value 1 | Out-Null
}
$configuredValue = Get-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $policyKey `
-ValueName $policyValueName
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $GpoName |
Select-Object -First 1
$linkEnabled = $link -and (
$link.Enabled -eq $true -or
[string]$link.Enabled -eq 'Yes')
[pscustomobject]@{
GpoName = $GpoName
GpoId = $gpo.Id
TargetOu = $TargetOuDn
LinkEnabled = [bool]$linkEnabled
DisableLockWorkstation = [int]$configuredValue.Value
}
+31
View File
@@ -14,6 +14,7 @@ $providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authent
$passwordProviderRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$passwordProviderClassId" $passwordProviderRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$passwordProviderClassId"
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32" $classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' $defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
$interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
$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()
@@ -51,6 +52,34 @@ if (-not $defaultProviderConfigured) {
$issues.Add('The SGU provider is not assigned as the machine default credential provider.') $issues.Add('The SGU provider is not assigned as the machine default credential provider.')
} }
$lastSignedInUserHidden = $false
try {
$lastSignedInUserHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName `
-ErrorAction Stop) -eq 1
}
catch {
# Report the missing or unreadable policy as a failed enrollment check.
}
if (-not $lastSignedInUserHidden) {
$issues.Add('The last signed-in user is not hidden from LogonUI.')
}
$localUserEnumerationDisabled = $false
try {
$localUserEnumerationDisabled = (Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath `
-Name EnumerateLocalUsers `
-ErrorAction Stop) -eq 0
}
catch {
# Report the missing or unreadable policy as a failed enrollment check.
}
if (-not $localUserEnumerationDisabled) {
$issues.Add('Local user enumeration is not explicitly disabled for the domain client.')
}
$passwordProviderPreserved = Test-Path -LiteralPath $passwordProviderRegistryPath $passwordProviderPreserved = Test-Path -LiteralPath $passwordProviderRegistryPath
if (-not $passwordProviderPreserved) { if (-not $passwordProviderPreserved) {
$issues.Add('The built-in Microsoft password provider registration is missing.') $issues.Add('The built-in Microsoft password provider registration is missing.')
@@ -137,6 +166,8 @@ $result = [pscustomobject]@{
ProviderBinary = $registeredDll ProviderBinary = $registeredDll
ProviderBinaryPresent = [bool]$providerBinaryPresent ProviderBinaryPresent = [bool]$providerBinaryPresent
DefaultProviderConfigured = $defaultProviderConfigured DefaultProviderConfigured = $defaultProviderConfigured
LastSignedInUserHidden = $lastSignedInUserHidden
LocalUserEnumerationDisabled = $localUserEnumerationDisabled
PasswordProviderPreserved = $passwordProviderPreserved PasswordProviderPreserved = $passwordProviderPreserved
SettingsPresent = [bool]$settings SettingsPresent = [bool]$settings
ClientCertificatePresent = [bool]$clientCertificatePresent ClientCertificatePresent = [bool]$clientCertificatePresent
@@ -23,7 +23,9 @@ public sealed class SguCredentialProvider : CredentialProviderBase
ProviderTileIcon.Create()); ProviderTileIcon.Create());
yield return new LargeLabelControl(ControlKeys.Heading, "Acceso institucional SGU") yield return new LargeLabelControl(ControlKeys.Heading, "Acceso institucional SGU")
{ {
State = FieldState.DisplayInBoth // Isolate the CPFT_LARGE_TEXT heading from the compact provider tile
// for the selected-layout variant. LogonUI still owns the typography.
State = FieldState.DisplayInSelectedTile
}; };
yield return new SmallLabelControl( yield return new SmallLabelControl(
ControlKeys.InformationLabel, ControlKeys.InformationLabel,
@@ -44,7 +44,7 @@ public sealed class ProviderTileIconTests
SguCredentialTile tile = Assert.IsType<SguCredentialTile>(provider.CreateGenericTile()); SguCredentialTile tile = Assert.IsType<SguCredentialTile>(provider.CreateGenericTile());
Assert.Equal("Acceso institucional SGU", heading.Label); Assert.Equal("Acceso institucional SGU", heading.Label);
Assert.Equal(FieldState.DisplayInBoth, heading.State); Assert.Equal(FieldState.DisplayInSelectedTile, heading.State);
Assert.Equal(GenericTileDisplayMode.DisplayAsDedicatedTile, tile.GenericTileDisplayMode); Assert.Equal(GenericTileDisplayMode.DisplayAsDedicatedTile, tile.GenericTileDisplayMode);
} }
} }