Active Directory: Considerations When Implementing a New Password Expiration Policy
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.
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.
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.
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.
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.
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
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.
See Also
- Active Directory: Fun with Maximum Password Age
- Active Directory: Bad Passwords and Account Lockout
- Active Directory: Large Integer Attributes
- Active Directory: Glossary
- Wiki: Active Directory Domain Services (AD DS) Portal
- Wiki: Portal of TechNet Wiki Portals
Other Resources
- Maximum password age --impact (forum post)
- Account Lockout and Password Concepts
- Bad-Pwd-Count attribute
- Details of Account Lockout Settings and Processes
- Troubleshooting Account Lockout
- Bad-Password-Time attribute
- Attribute badPasswordTime
- Account Lockout Policy in Active Directory (blog post)
- Configuring Account Lockout (blog post)
- AD DS: Fine-Grained Password Policies
- Password Policy
- Maximum password age