Compare commits

..
2 Commits
Author SHA1 Message Date
alexrg 20dae3caa9 Hide password changes for managed SGU users 2026-09-18 16:58:05 -06:00
alexrg 7d78a1f515 Make Auth Broker authoritative for managed passwords 2026-09-18 16:46:56 -06:00
10 changed files with 200 additions and 1 deletions
+9
View File
@@ -62,6 +62,15 @@ plus six digits. It searches `BaseDn` by `sAMAccountName`, creates the user when
absent, moves it to the mapped OU when required, sets `userPrincipalName`, and
passes the submitted password directly to ADSI `SetPassword`.
Before an account becomes usable, the broker applies explicit deny ACEs for the
Active Directory `Change Password` extended right to SELF and Everyone. Users
beneath `OU=Usuarios-SGU` therefore cannot replace the synchronized password
from Windows, Ctrl+Alt+Delete, LDAP or another client. The broker's
administrative `SetPassword` operation uses the separate `Reset Password` right
and remains able to synchronize the current institutional credential after each
successful SGU authentication. Repeated synchronizations detect the existing
ACEs and do not duplicate them.
When the authenticated HTML exposes recognized stable IDs, the broker also
updates the applicable `displayName`, `givenName`, `sn`, `mail`, `title`,
`department`, `employeeType`, `employeeID`, `streetAddress`, `l`, `st`, and
+17
View File
@@ -0,0 +1,17 @@
# SGU Credential Provider 0.6.8
Esta versión declara al Auth Broker como la única autoridad de contraseñas para
todas las cuentas administradas bajo `OU=Usuarios-SGU`.
- Cada creación, movimiento o actualización de una cuenta agrega de forma
idempotente las denegaciones SELF y Everyone para el derecho extendido de
Active Directory `Change Password`.
- El usuario no puede cambiar la contraseña desde Windows, Ctrl+Alt+Delete,
LDAP ni herramientas equivalentes.
- El broker conserva el derecho administrativo separado `Reset Password` y
continúa sincronizando la contraseña institucional exacta mediante
`SetPassword` después de una autenticación SGU válida.
- El despliegue del broker recorre todas las cuentas existentes en
`Usuarios-SGU` y corrige aquellas que todavía permiten cambios directos.
No se modifica la contraseña institucional ni se almacena una copia adicional.
+16
View File
@@ -0,0 +1,16 @@
# SGU Credential Provider 0.6.9
Esta versión completa la autoridad de contraseñas del Auth Broker en la
interfaz de Windows.
- La GPO `SGU - User session restrictions` habilita la directiva **Remove
Change Password** para todas las cuentas bajo `OU=Usuarios-SGU`.
- La opción **Cambiar una contraseña** deja de aparecer en la pantalla de
seguridad de Ctrl+Alt+Supr.
- La protección real continúa en Active Directory mediante las denegaciones
del derecho extendido `Change Password`; la GPO únicamente evita mostrar una
acción que esas cuentas no pueden completar.
El Auth Broker conserva el derecho administrativo separado `Reset Password`
para sincronizar la contraseña institucional después de una autenticación SGU
válida.
+7
View File
@@ -18,6 +18,13 @@ the same source list. RDP uses a separate allowlist. See
`Marshal.ZeroFreeGlobalAllocUnicode`; managed references are released as soon
as each request completes.
- The broker uses the exact received value for both NTLM and AD `SetPassword`.
- Managed `Usuarios-SGU` accounts deny the SELF and Everyone `Change Password`
extended right. Only an administrator or the broker through the separate
`Reset Password` right can replace the AD password.
- The `SGU - User session restrictions` GPO also hides the **Change a
password** command from the Windows Ctrl+Alt+Delete security screen for
managed users. This is a user-interface complement to the directory ACL,
not a substitute for it.
- There is no HMAC password, pepper, local password cache, Supabase password, or
other derived credential in this Windows path.
- Neither application logs request bodies or passwords. Deployment configuration
+15 -1
View File
@@ -201,6 +201,19 @@ if ($RemoteDesktopGroupDn) {
}
}
$managedUsersPasswordChangeCorrected = 0
if ($PSCmdlet.ShouldProcess($usersOuDn, 'Deny direct password changes for every managed SGU user')) {
$managedUsers = @(Get-ADUser -Filter * -SearchBase $usersOuDn -SearchScope Subtree `
-Properties CannotChangePassword -Server $LdapHost -ErrorAction Stop)
foreach ($managedUser in $managedUsers) {
if (-not $managedUser.CannotChangePassword) {
Set-ADAccountControl -Identity $managedUser.DistinguishedName `
-CannotChangePassword $true -Server $LdapHost -Confirm:$false
$managedUsersPasswordChangeCorrected++
}
}
}
foreach ($file in @('SGU.AuthBroker.exe', 'SGU.AuthBroker.dll', 'appsettings.json')) {
if (-not (Test-Path -LiteralPath (Join-Path $PublishPath $file))) {
throw "PublishPath is missing $file."
@@ -356,4 +369,5 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install the SGU Authentication Broker
}
Get-Service -Name $serviceName | Select-Object Name, Status, StartType,
@{ Name = 'EventLog'; Expression = { $brokerEventLogName } }
@{ Name = 'EventLog'; Expression = { $brokerEventLogName } },
@{ Name = 'ExistingUsersPasswordChangeDenied'; Expression = { $managedUsersPasswordChangeCorrected } }
+19
View File
@@ -10,6 +10,7 @@ param(
$ErrorActionPreference = 'Stop'
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
$policyValueName = 'DisableLockWorkstation'
$disableChangePasswordValueName = 'DisableChangePassword'
$desktopPolicyKey = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
$themeKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
@@ -76,6 +77,17 @@ if ($PSCmdlet.ShouldProcess($GpoName, 'Prevent SGU users from manually locking w
-Type DWord `
-Value 1 | Out-Null
# The directory ACL remains the security boundary. This user policy also
# removes the unusable Change a password action from Ctrl+Alt+Delete.
Set-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $policyKey `
-ValueName $disableChangePasswordValueName `
-Type DWord `
-Value 1 | Out-Null
Set-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
@@ -135,6 +147,12 @@ $configuredValue = Get-GPRegistryValue `
-Server $DomainController `
-Key $policyKey `
-ValueName $policyValueName
$disableChangePasswordValue = Get-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $policyKey `
-ValueName $disableChangePasswordValueName
$screenSaverValue = Get-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
@@ -175,6 +193,7 @@ if ($WallpaperPath) {
TargetOu = $TargetOuDn
LinkEnabled = [bool]$linkEnabled
DisableLockWorkstation = [int]$configuredValue.Value
DisableChangePassword = [int]$disableChangePasswordValue.Value
ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0'
DarkMode = ([int]$appsThemeValue.Value -eq 0) -and ([int]$systemThemeValue.Value -eq 0)
Wallpaper = $configuredWallpaper
+1
View File
@@ -22,4 +22,5 @@ internal static class BrokerEventIds
internal static readonly EventId DirectoryOptionalMetadataFailure = new(1301, nameof(DirectoryOptionalMetadataFailure));
internal static readonly EventId DirectoryGroupMembershipFailure = new(1302, nameof(DirectoryGroupMembershipFailure));
internal static readonly EventId DirectoryRoleGroupMembershipAdded = new(1303, nameof(DirectoryRoleGroupMembershipAdded));
internal static readonly EventId DirectoryPasswordChangeDenied = new(1304, nameof(DirectoryPasswordChangeDenied));
}
@@ -1,5 +1,7 @@
using System.Collections.Concurrent;
using System.DirectoryServices;
using System.Security.AccessControl;
using System.Security.Principal;
using SGU.AuthBroker.Core.Directory;
using SGU.AuthBroker.Core.Identity;
using SGU.AuthBroker.Core.Profiles;
@@ -15,6 +17,12 @@ public sealed class ActiveDirectorySynchronizer(
private const int InfoAttributeMaximumLength = 1024;
private const int AccountDisabled = 0x0002;
private const int NormalAccount = 0x0200;
private static readonly Guid ChangePasswordExtendedRight =
new("AB721A53-1E2F-11D0-9819-00AA0040529B");
private static readonly SecurityIdentifier EveryoneSid =
new(WellKnownSidType.WorldSid, null);
private static readonly SecurityIdentifier SelfSid =
new(WellKnownSidType.SelfSid, null);
private static readonly AuthenticationTypes BindFlags =
AuthenticationTypes.Secure | AuthenticationTypes.Signing | AuthenticationTypes.Sealing;
@@ -117,6 +125,12 @@ public sealed class ActiveDirectorySynchronizer(
// account without its required classification.
EnsureRoleGroupMembership(user, identity);
// SGU remains the password authority. Deny the user's Change
// Password extended right before making the account usable. This
// does not deny the broker's administrative Reset Password right,
// which ADSI SetPassword uses for each successful authentication.
EnsureCannotChangePassword(user, identity.UserName);
// The exact institutional password received by the broker is passed to AD.
// It is not derived, transformed, written to disk, or included in logs.
user.Invoke("SetPassword", [password]);
@@ -143,6 +157,52 @@ public sealed class ActiveDirectorySynchronizer(
}
}
private void EnsureCannotChangePassword(DirectoryEntry user, string institutionalUser)
{
user.Options!.SecurityMasks = SecurityMasks.Dacl;
ActiveDirectorySecurity security = user.ObjectSecurity;
if (!EnsureCannotChangePassword(security))
{
return;
}
user.ObjectSecurity = security;
user.CommitChanges();
logger.LogInformation(
BrokerEventIds.DirectoryPasswordChangeDenied,
"Denied direct password changes for managed Active Directory user {InstitutionalUser}; SGU Auth Broker remains the password authority.",
institutionalUser);
}
internal static bool EnsureCannotChangePassword(ActiveDirectorySecurity security)
{
bool changed = false;
foreach (SecurityIdentifier identity in new[] { EveryoneSid, SelfSid })
{
bool exists = security
.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier))
.OfType<ActiveDirectoryAccessRule>()
.Any(rule =>
rule.AccessControlType == AccessControlType.Deny &&
rule.IdentityReference.Equals(identity) &&
rule.ObjectType == ChangePasswordExtendedRight &&
(rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) != 0);
if (exists)
{
continue;
}
security.AddAccessRule(new ActiveDirectoryAccessRule(
identity,
ActiveDirectoryRights.ExtendedRight,
AccessControlType.Deny,
ChangePasswordExtendedRight));
changed = true;
}
return changed;
}
private static void TryApplyProfile(
DirectoryEntry user,
UserIdentity identity,
@@ -0,0 +1,28 @@
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$synchronizerPath = Join-Path $repositoryRoot 'src\SGU.AuthBroker\Services\ActiveDirectorySynchronizer.cs'
$deployPath = Join-Path $repositoryRoot 'scripts\Deploy-AuthBroker.ps1'
$userPolicyPath = Join-Path $repositoryRoot 'scripts\Set-SguDomainUserPolicies.ps1'
$synchronizer = Get-Content -LiteralPath $synchronizerPath -Raw
$deploy = Get-Content -LiteralPath $deployPath -Raw
$userPolicy = Get-Content -LiteralPath $userPolicyPath -Raw
Describe 'SGU Auth Broker password authority' {
It 'denies the Change Password extended right to SELF and Everyone before SetPassword' {
$synchronizer | Should Match 'AB721A53-1E2F-11D0-9819-00AA0040529B'
$synchronizer | Should Match 'WellKnownSidType\.WorldSid'
$synchronizer | Should Match 'WellKnownSidType\.SelfSid'
$synchronizer | Should Match 'AccessControlType\.Deny'
$synchronizer.IndexOf('EnsureCannotChangePassword(user', [StringComparison]::Ordinal) |
Should BeLessThan $synchronizer.IndexOf('user.Invoke("SetPassword"', [StringComparison]::Ordinal)
}
It 'repairs every existing account below Usuarios-SGU during broker deployment' {
$deploy | Should Match 'Get-ADUser.*-SearchBase \$usersOuDn.*-SearchScope Subtree'
$deploy | Should Match '(?s)Set-ADAccountControl.*-CannotChangePassword \$true'
}
It 'removes Change Password from the Windows security screen for managed users' {
$userPolicy | Should Match "disableChangePasswordValueName = 'DisableChangePassword'"
$userPolicy | Should Match '(?s)-ValueName \$disableChangePasswordValueName.*-Type DWord.*-Value 1'
}
}
@@ -1,3 +1,6 @@
using System.DirectoryServices;
using System.Security.AccessControl;
using System.Security.Principal;
using SGU.AuthBroker.Core.Profiles;
using SGU.AuthBroker.Services;
using Xunit;
@@ -31,4 +34,29 @@ public sealed class ActiveDirectorySynchronizerTests
Assert.Null(updated);
}
[Fact]
public void CannotChangePasswordRulesAreCompleteAndIdempotent()
{
ActiveDirectorySecurity security = new();
Assert.True(ActiveDirectorySynchronizer.EnsureCannotChangePassword(security));
Assert.False(ActiveDirectorySynchronizer.EnsureCannotChangePassword(security));
Guid changePasswordRight = new("AB721A53-1E2F-11D0-9819-00AA0040529B");
ActiveDirectoryAccessRule[] rules = security
.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier))
.OfType<ActiveDirectoryAccessRule>()
.Where(rule =>
rule.AccessControlType == AccessControlType.Deny &&
rule.ObjectType == changePasswordRight &&
(rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) != 0)
.ToArray();
Assert.Equal(2, rules.Length);
Assert.Contains(rules, rule => rule.IdentityReference.Equals(
new SecurityIdentifier(WellKnownSidType.WorldSid, null)));
Assert.Contains(rules, rule => rule.IdentityReference.Equals(
new SecurityIdentifier(WellKnownSidType.SelfSid, null)));
}
}