Introduction

Introducing a new password expiration policy for Active Directory users can cause unexpected problems. If passwords did not previously expire, many users will require assistance. When all passwords expire at once, your support personnel can become swamped. This article describes how to make the transition easier.



Relevant AD Attributes and Group Policy Setting

The Active Directory attributes relevant to password expiration and account lockout are documented in the table below. Also included are the corresponding PowerShell property name and the Group Policy Setting.

AD Attribute PowerShell Property Group Policy Setting
lockoutThreshold LockoutThreshold Account lockout threshold
lockoutDuration LockoutDuration Account lockout duration
lockoutObservationWindow LockoutObservationWindow Reset account lockout counter after
pwdHistoryLength PasswordHistoryCount Enforce password history
lockoutTime AccountLockoutTime <none>
logonCount <none> <none>
pwdLastSet PasswordLastSet <none>
pwdProperties ComplexityEnabled Password must meet complexity requirements
badPwdCount BadLogonCount <none>
badPasswordTime LastBadPasswordAttempt <none>

The first four attributes in the table only apply to the domain object in Active Directory. This is the default Domain Password and Account Lockout Policy. Similar attributes apply to Password Setting Objects (PSO's). The corresponding PSO attribute names are the same, but start with the string "msDS-". The only exception is the PSO attribute corresponding to pwdHistoryLength, which is msDS-PasswordHistoryLength. The last six attributes apply to users. The attributes logonCount, badPwdCount, and badPasswordTime are not replicated, so each domain controller maintains its own values for each user.

The relevant group policy settings are shown in this image. The values in the image were used for testing.

↑ Return to Top


Implementing Password Expiration for the First Time

If a password expiration policy is being implemented for the first time, almost all users will either never have changed their password, or will have changed it long ago. Most passwords will be immediately expired. And users will have little or no experience changing their password. They may not know the rules for passwords, like minimum length, complexity, and password history. And users are more likely to forget their password later.

Communication is important. Users need to be informed of the change and all of the password requirements. It is recommended that the new policy be implemented on groups of users in stages over time, to make support easier. At the very least, steps should be taken so that passwords do not all expire at once. The scripts provided later can assist in this effort.

↑ Return to Top


Changing the Maximum Password Age

If you now have a policy, but are increasing the maximum password age, there should be no problems.

But if you are decreasing the maximum password age, the situation is similar to the case where you are implementing a policy for the first time. Many people will have their password expired immediately. The scripts provided can help.

↑ Return to Top


The pwdLastSet Attribute

The pwdLastSet Active Directory attribute of users is syntax LargeInteger. It is a 64-bit integer that represents DateTime values as the number of 100-nanosecond intervals (also called ticks) since 12:00 am January 1, 1601. That is the zero date for LargeInteger DateTime values. The value is also in Coordinated Universal Time (UTC, for the French term), which used to be called GMT. The system updates the value whenever the password is changed.

The only values that administrators are allowed to assign to pwdLastSet are 0 and -1. The value 0 corresponds to the long ago zero date, so the user password is expired at once. When you select "User must change password at next logon" on the "Account" tab of ADUC, the GUI assigns 0 to the pwdLastSet attribute.

The only other value administrators can assign is -1. Because of the way 64-bit integers are saved in AD, this becomes the largest value that can be saved in a 64-bit register. This huge number corresponds to September 14 in the year 30828. But if pwdLastSet has this value, the system will change it to the value corresponding to the current DateTime the next time the user logs on. Then the password will expire after the maximum password age has passed from that point in time. A quirk is that you cannot assign -1 until you first assign 0.

You can take advantage of these features of pwdLastSet to stagger when user passwords will expire. The scripts that follow help with this.

↑ Return to Top


Script to Change the pwdLastSet Attribute of all Users in a CSV File to -1

The PowerShell script that follows imports a CSV file of user sAMAccountNames (or distinguishedNames), assigns 0 to each of the user's pwdLastSet attribute, then assigns -1. A nearby domain controller is specified in the script, so that both updates are done on the same DC. This ensures that there are no problems due to replication between DC's. The idea is that you would have a series of CSV files. You can run the script on a different CSV file of users, perhaps one file per week. If you have your users split among 5 such files, then their passwords will expire in groups over a 5 week period in the future. The script follows.

# StaggerPWExp.ps1
# PowerShell script to use when a new password expiration policy is to be
# first applied to a domain. All users in the CSV file will have their
# passwords expire X days after they next logon, where X is the new password
# expiration policy in days. This script should be run during off hours.

# Author: Richard L. Mueller # Version 1.0 - November 23, 2018
# Specify the DNS name of a nearby Domain Controller, so all updates are # performed on the same DC. This avoids possible problems due to the # time it takes updates to synchronize between DCs. $DC = "MyDC.MyDomain.com"
# Read user sAMAccountNames from the CSV file. # The header line should define this field as "ID". # If you have a series of CSV files, the filename should be updated each time # this script is run. $Users = Import-Csv .\Users1.csv
# Assign 0 to the pwdLastSet attribute for all users in the CSV. # This expires the password immediately. ForEach ($User In $Users) {     $ID = $User.ID     # $ID is in quotes because the sAMAccountName can include spaces.     Set-ADUser -Server $DC -Identity "$ID" -Replace @{pwdLastSet=0} }
# Assign -1 to the pwdLastSet attribute for all users in the CSV. Because of # the way 64-bit integers are saved, this is the largest possible value that # can be saved in a LargeInteger attribute. It corresponds to a date far in # the future. But the system will assign a value corresponding to the current # DateTime the next time the user logs on. The password will then expire # according to the maximum password age policy that applies to the user. ForEach ($User In $Users) {     $ID = $User.ID     # $ID is in quotes because the sAMAccountName can include spaces.     Set-ADUser -server $DC -Identity "$ID" -Replace @{pwdLastSet=-1} }

You must modify the value assigned to the $DC variable in the above script for your situation. It should be the DNS name of a nearby domain controller. You should also modify the CSV file name (and possibly the path) in the statement that imports the CSV and assigns the values to the $Users variable. You would modify the filename each time the script is run, to process a different group of users.

Since the script makes a lot of changes that must be replicated, and users will have their passwords expired for a brief time, the script should be run during off hours, when users will not be logging on.

↑ Return to Top


Script to Export User sAMAccountNames into a Series of CSV Files

The following script can be used to export all of the user sAMAccountNames in your domain into a specified number of CSV files.

Before running the script, make sure the values of the variables $FileName and $NumFiles meet your needs. If $FileName is "Users" and $NumFiles is 5, the CSV files "Users1.csv", "Users2.csv", "Users3.csv", "Users4.csv", and "Users5.csv" will be created. Each of the files will have a header line defining the field "ID", which is used in the previous script. As written, the files will be created in the current directory, but you can include a different path.

# StaggerNTNames.ps1
# PowerShell script to export the sAMAccountNames of all enabled users
# in the domain with passwords that expire into a series of CSV files.

# Author: Richard L. Mueller # Version 1.0 - November 23, 2018
# Specify the base CSV file name. # Files will be created in the current directory with names like # <base file name>n.csv, where n is an integer incrementing from 1. $FileName = "Users"
# Specify the number of CSV files to be created. $NumFiles = 5
# Retrieve the sAMAccountName of all enabled users with passwords that expire. $Users = Get-ADUser -Filter {(Enabled -eq $True) -And (PasswordNotRequired -eq $False) -And (PasswordNeverExpires -eq $False)} | Select sAMAccountName # If your users do not yet have passwords that expire, use the following for all enabled users. # $Users = Get-ADUser -Filter {Enabled -eq $True} | Select sAMAccountName # And, if you want to include disabled users, use the following. # $Users = Get-ADUser -Filter * | Select sAMAccountName
# Determine the number of users retrieved. $NumUsers = $Users.Count "Total number of users: $NumUsers"
# Calculate the maximum number of users per CSV file. $UsersPerCSV = [Math]::Ceiling($NumUsers / $NumFiles)
# $i is the CSV file number, incrementing from 1. $i = 1
# $j is the number of users in the CSV file. $j = 0
# $m is the total number of users processed. $m = 0
ForEach ($User In $Users) {     $j = $j + 1     $m = $m + 1     # Add the header line, defining the field "ID".     If ($j -eq 1) {"ID" >> .\$FileName$i.csv}     "$User" >> .\$FileName$i.csv     If ($j -eq $UsersPerCSV)     {         "$FileName$i.csv has $j users"         $i = $i + 1         $j = 0     } }
# Process the last CSV file, if there are more users. If ($j -gt 0) {     "$FileName$i.csv has $j users" } "Total number of users in the all $NumFiles CSV files: $m"

If your domain spans several time zones, or if your organization is active 24 hours per day, it may be difficult to find a time when no one will be logging on. In this case, you can add a -SearchBase to the Get-ADUser statement if users in different time zones are in different OU's. The Get-ADUser statement in the previous script can be revised similar to below.

$Users = Get-ADUser -SearchBase "ou=East,dc=domain,dc=com" -Filter * | Select sAMAccountName

Or, you may be able to divide your users according to group membership. The Get-ADUser statement can be revised similar to below.

$Users = Get-ADUser -LDAPFilter {memberOf=cn=First Shift,ou=East,dc=domain,dc=com} | Select sAMAccountName

You must specify the full distinguished name of the group. The script will process all direct members of the group. If the numbers are not too large, you can have the script create one CSV file by assigning 1 to the variable $NumFiles in the script.

If you need to include all direct members of the group, plus all members of any nested groups, then the following will do the trick.

$Users = Get-ADUser -LDAPFilter {memberOf:1.2.840.113556.1.4.1941:=cn=First Shift,ou=East,dc=domain,dc=com} | Select sAMAccountName

↑ Return to Top


Number of CSV Files to Use

The number of CSV files you use depends on a number of factors.

  • The number of users to be processed. 
  • The number of users you want to have in each file.
  • The new maximum password age.
  • How often you want to process groups of users.
  • The time zones and shifts when your users will logon.
  • The number and location of support organizations (that can assist users).

For example, assume the new maximum password age policy is 42 days. If you process users in one CSV file per week, you probably want 6 CSV files at most.

But if this means that each CSV file has 1000 users, you may need to compromise. Or, you could reduce the maximum password age in stages. For example, you could first reduce the maximum password age to 90 days. Then use 12 CSV files over 12 weeks. Half as many users would be in each file, with passwords expiring at about the same time. This would give the users some experience changing passwords. Then you could reduce the maximum password age to 42 days, and use 6 CSV files over 6 weeks.

↑ Return to Top


See Also

↑ Return to Top


Other Resources

↑ Return to Top