Enforcing Active Directory Password Policies When Resetting Passwords via Service Accounts


8 views

When a domain admin or service account performs a password reset in Active Directory, the operation bypasses standard password policy enforcement (password history, complexity requirements). This creates a security loophole where users could theoretically reuse old passwords indefinitely.

Active Directory handles password changes differently based on the context:

1. User-initiated change (Ctrl+Alt+Del): Full policy enforcement
2. Admin-forced reset: Bypasses history/complexity checks
3. LDAP modify operation: Behaves like admin reset unless configured

To enforce policies during service account resets, we need to use the DSACLS method combined with proper LDAP controls:

// PowerShell example enforcing policy during reset
$userDN = "CN=John Doe,OU=Users,DC=domain,DC=com"
$newPassword = ConvertTo-SecureString "P@ssw0rd123!" -AsPlainText -Force

# This approach respects password policies
Set-ADAccountPassword -Identity $userDN -NewPassword $newPassword -Reset -Confirm:$false
Set-ADUser -Identity $userDN -ChangePasswordAtLogon $true

For web applications performing resets, configure these AD settings:

  • Enable "Require SSL" for password reset operations
  • Set msDS-PasswordSettingsPrecedence on Fine-Grained Password Policies
  • Implement custom password filters via PasswordFilter.dll

Here's a C# approach using System.DirectoryServices.Protocols that respects policies:

using System.DirectoryServices.Protocols;

public bool SecurePasswordReset(string username, string newPassword)
{
    try {
        using (var conn = new LdapConnection("domainController"))
        {
            conn.AuthType = AuthType.Negotiate;
            
            var request = new PasswordModifyRequest(
                username, 
                null, 
                newPassword);
            
            // Critical: This flag enforces policy checks
            request.Controls.Add(new PermissiveModifyControl());
            
            var response = (PasswordModifyResponse)conn.SendRequest(request);
            return response.ResultCode == ResultCode.Success;
        }
    }
    catch (LdapException ex) {
        // Handle policy violation errors (e.g., 19=constraint violation)
        return false; 
    }
}

After implementation, verify policy enforcement with:

repadmin /showattr "DC=domain,DC=com" /subtree /filter:"(objectClass=user)" /attrs:pwdLastSet,unicodePwd

When implementing self-service password reset functionality through service accounts in Active Directory, we encounter a critical gap in policy enforcement. The standard password policies (complexity requirements, password history, minimum age) are bypassed when changes are made via privileged service accounts. This creates security vulnerabilities where users can effectively recycle old passwords.

Active Directory evaluates password policies differently based on the context:

// Standard user-initiated change (policy enforced)
User -> AD: ChangePassword(oldPwd, newPwd) 

// Admin/service account change (policy bypass)
ServiceAccount -> AD: SetPassword(userDN, newPwd)

The key distinction lies in the underlying LDAP operation. SetPassword (used by admins) doesn't trigger the same policy checks as ChangePassword (used by users).

Here are three approaches to enforce password policies consistently:

1. Using AD's Password Settings Objects (PSO)

Create a fine-grained password policy that applies specifically to service accounts:

New-ADFineGrainedPasswordPolicy -Name "ServiceResetPolicy" 
    -Precedence 100 
    -PasswordHistoryCount 10 
    -MinPasswordLength 12 
    -ComplexityEnabled $true 
    -MinPasswordAge "1.00:00:00"
    
Add-ADFineGrainedPasswordPolicySubject -Identity "ServiceResetPolicy" 
    -Subjects "CN=PwdResetSvc,OU=ServiceAccounts,DC=domain,DC=com"

Note that this still won't enforce history checks for target user accounts.

2. Implementing Pre-Validation in Code

Before executing the reset, validate against policies using the DSGetPasswordPolicy API:

// C# example using System.DirectoryServices.AccountManagement
using (var context = new PrincipalContext(ContextType.Domain))
{
    var user = UserPrincipal.FindByIdentity(context, username);
    
    // Check if password meets complexity requirements
    if (!context.ValidateCredentials(username, newPassword))
    {
        throw new PasswordException("Password doesn't meet complexity requirements");
    }
    
    // Check against last 5 passwords (requires extended schema)
    var lastPasswords = ((DirectoryEntry)user.GetUnderlyingObject())
        .Properties["attributeNames"].Value as string[];
    
    if (lastPasswords?.Contains(newPassword) == true)
    {
        throw new PasswordException("Password was used recently");
    }
}

3. Hybrid Approach with Temporary Credentials

A more robust solution involves impersonating the user for the password change:

// PowerShell example using RunAs
$cred = New-Object System.Management.Automation.PSCredential($username, $securePwd)
Start-Process "powershell.exe" -Credential $cred -ArgumentList {
    $user = [ADSI]"LDAP://$($args[0])"
    $user.ChangePassword($args[1], $args[2])
} -ArgumentList $userDN, $currentPwd, $newPwd

This triggers standard policy enforcement but requires knowing/rotating the current password.

For a web service scenario, consider this architecture:

// Web API endpoint pseudocode
[HttpPost("/api/resetpassword")]
public async Task<IActionResult> ResetPassword([FromBody] ResetRequest request)
{
    // 1. Verify user identity (MFA, security questions, etc.)
    if (!await VerifyUserIdentity(request.UserId, request.ChallengeAnswers))
        return Unauthorized();
    
    // 2. Validate new password against policies
    var policyResult = await _passwordValidator.ValidateAsync(
        request.NewPassword, 
        request.UserId);
    
    if (!policyResult.IsValid)
        return BadRequest(policyResult.Errors);
    
    // 3. Perform privileged reset
    using (var impersonation = new WindowsIdentityImpersonator(_svcAccount))
    {
        var user = UserPrincipal.FindByIdentity(
            ContextType.Domain,
            request.UserId);
        
        // This will still bypass some policies!
        user.SetPassword(request.NewPassword);
    }
    
    return Ok();
}

When implementing these solutions, remember:

  • Service accounts need "Reset Password" permission, not full admin rights
  • Log all password reset activities with full audit details
  • Implement rate limiting to prevent brute force attacks
  • Consider using Azure AD Password Protection for additional checks