The “Password never expires” setting (the DONT_EXPIRE_PASSWORD userAccountControl flag)
is one of those legacy conveniences that quietly turns into a long-term security and compliance problem.
This article shows how to find these accounts, decide what “good” looks like per account type, and remove the setting
safely—without breaking services.
Why “Password Never Expires” Is a Problem
- Credential age becomes unbounded: if a password leaks (logs, scripts, ticketing notes, backups), it stays valid indefinitely.
- Compliance gaps: many standards expect periodic rotation or compensating controls for non-rotating credentials.
- Operational drift: exceptions accumulate over time (shared accounts, app accounts, vendor accounts) and nobody owns them.
- Attack path durability: attackers love stable credentials—especially for service and privileged accounts.
Know What You’re Actually Fixing
In Active Directory, “password never expires” is a bit flag inside userAccountControl.
Clearing the flag does not change the password by itself—it re-enables expiration rules based on your domain/Fine-Grained Password Policies (FGPP).
If the password is already very old, clearing the flag can cause the account to be treated as expired immediately (depending on policy and timing).
Step 1: Inventory Accounts with “Password Never Expires”
Start with a clean export. This gives you a working list for ownership, risk ranking, and remediation tracking.
Import-Module ActiveDirectory
Get-ADUser -LDAPFilter "(userAccountControl:1.2.840.113556.1.4.803:=65536)" `
-Properties DisplayName, Enabled, PasswordNeverExpires, PasswordLastSet, LastLogonDate, MemberOf, Description |
Select-Object SamAccountName, DisplayName, Enabled, PasswordNeverExpires, PasswordLastSet, LastLogonDate, Description |
Sort-Object Enabled -Descending, SamAccountName |
Export-Csv ".\PasswordNeverExpires_Users.csv" -NoTypeInformation
The LDAP matching rule 1.2.840.113556.1.4.803 checks bitwise flags.
The value 65536 corresponds to DONT_EXPIRE_PASSWORD.
Include Service Accounts Too (If They’re Users)
Many service accounts are still classic user objects. Export and tag them for special handling.
Get-ADUser -Filter { PasswordNeverExpires -eq $true } `
-Properties ServicePrincipalName, Description, PasswordLastSet, LastLogonDate |
Select-Object SamAccountName, Enabled, PasswordLastSet, LastLogonDate, ServicePrincipalName, Description |
Export-Csv ".\PasswordNeverExpires_WithSPN.csv" -NoTypeInformation
Step 2: Classify Accounts (This Decides the Fix)
Do not bulk-remove the flag until you sort accounts into a few practical categories:
1) Human user accounts
- Should almost never have “password never expires.”
- Move to standard policy (and ideally MFA + risk controls in hybrid environments).
2) Shared / generic accounts
- Prefer removing them entirely or replacing with named accounts + delegation.
- If they must exist temporarily, enforce rotation and strong monitoring, and create an end-date plan.
3) Application / service accounts (classic user objects)
- Best option: gMSA (Group Managed Service Account) where supported.
- Next best: managed rotation via PAM/ITDR/credential vaulting, plus tight scoping and monitoring.
- Avoid: hard-coded passwords in scripts/config files.
4) Privileged accounts
- Highest risk. Remove “never expires” with urgency.
- Also verify: separate admin accounts, tiering, PAWs, and privileged group hygiene.
Step 3: Decide Your Target Policy (Domain Policy vs FGPP)
Many orgs use a baseline domain policy for most users and Fine-Grained Password Policies (FGPP) for special populations (service accounts, privileged accounts, break-glass, etc.).
- Human users: standard expiry (or modern approach with strong MFA + risk-based access if your org allows it).
- Service accounts: prefer no human-managed password (gMSA), otherwise controlled rotation with compensating controls.
- Break-glass: strong password, very restricted use, monitored sign-ins, stored in a vault, tested regularly; policy decisions here should be explicit and documented.
Step 4: Remediate Safely (Without Outages)
Option A: Remove “Never Expires” and Force Change at Next Logon (Human Accounts)
This is the cleanest for user accounts. Clearing the flag puts the account back under policy, and setting
ChangePasswordAtLogon forces a controlled reset.
# Example: one user
Set-ADUser -Identity jdoe -PasswordNeverExpires $false -ChangePasswordAtLogon $true
Option B: Remove “Never Expires” But Do NOT Force Immediate Change (Service Accounts During Transition)
For service accounts, forcing a change can break applications. Use a transition plan: clear the flag only when you’ve confirmed the service can handle a rotation process (or you’ve migrated to gMSA).
# Example: remove the flag only (no forced change)
Set-ADUser -Identity svc_app1 -PasswordNeverExpires $false
Option C: Stage the Rollout (Recommended)
- Export and classify (owner, system, risk, last logon, SPNs).
- Fix “easy” users first (active human accounts).
- Handle service accounts next (migrate to gMSA or implement managed rotation).
- Close out shared accounts (eliminate or heavily control with an end date).
- Verify privileged accounts and remove exceptions aggressively.
Bulk Removal: A Practical, Safer Pattern
Bulk changes should be driven by a CSV that includes an explicit “Approved” column and an “ExceptionReason” column. This prevents accidental changes to accounts you haven’t validated.
1) Create a working CSV
# Build candidate list
Get-ADUser -Filter { PasswordNeverExpires -eq $true -and Enabled -eq $true } `
-Properties DisplayName, PasswordLastSet, LastLogonDate, Description, ServicePrincipalName |
Select-Object SamAccountName, DisplayName, PasswordLastSet, LastLogonDate, Description, ServicePrincipalName,
@{n="Approved";e={""}},
@{n="ForceChangeAtLogon";e={""}},
@{n="ExceptionReason";e={""}} |
Export-Csv ".\PNE_Worklist.csv" -NoTypeInformation
2) Apply changes only where approved
$rows = Import-Csv ".\PNE_Worklist.csv"
foreach ($r in $rows) {
if ($r.Approved -ne "YES") { continue }
$force = $false
if ($r.ForceChangeAtLogon -eq "YES") { $force = $true }
try {
if ($force) {
Set-ADUser -Identity $r.SamAccountName -PasswordNeverExpires $false -ChangePasswordAtLogon $true
} else {
Set-ADUser -Identity $r.SamAccountName -PasswordNeverExpires $false
}
Write-Host "Updated: $($r.SamAccountName)"
}
catch {
Write-Warning "Failed: $($r.SamAccountName) :: $($_.Exception.Message)"
}
}
Service Accounts: The Right End State
Move to gMSA where possible
- Windows services and many scheduled tasks can run under a gMSA.
- Passwords rotate automatically and are not known to humans.
- Strong reduction in operational risk and “password sprawl.”
If gMSA isn’t possible (legacy apps, appliances, non-Windows systems), aim for: credential vaulting + automated rotation, strict scoping (least privilege), and monitoring.
Common “gotchas” before rotating service accounts
- Hard-coded credentials in config files, connection strings, scripts, or scheduled tasks.
- Multiple dependencies: the same account may be used by several services across servers.
- SPNs/Kerberos: service accounts with SPNs may be tied to authentication flows; changes must be planned.
- Privilege creep: service accounts often have unnecessary group memberships.
Auditing and Detection: Prove It’s Fixed (and Stays Fixed)
Detect accounts reintroduced later
Run a scheduled report and alert when new “PasswordNeverExpires” accounts appear.
# Report any enabled accounts where PasswordNeverExpires is true
Get-ADUser -Filter { PasswordNeverExpires -eq $true -and Enabled -eq $true } `
-Properties WhenChanged, PasswordLastSet, LastLogonDate |
Select-Object SamAccountName, WhenChanged, PasswordLastSet, LastLogonDate |
Sort-Object WhenChanged -Descending
Track changes in the Security log
If you enable the right auditing, common Event IDs to watch include:
- 4738: A user account was changed (often includes UAC-related changes).
- 4723: An attempt was made to change an account’s password.
- 4724: An attempt was made to reset an account’s password.
Combine event monitoring with change control: any exception should have an owner, reason, compensating controls, and an expiration date.
Operational Playbook
- Export all “Password never expires” accounts.
- Tag ownership (who is responsible) and purpose (human/shared/service/privileged).
- Choose the target state:
- Humans: clear flag + force change at next logon (typical).
- Services: migrate to gMSA or implement managed rotation.
- Shared: eliminate or tightly control with end date.
- Stage changes in waves (pilot OU/group first).
- Monitor authentication failures and service health after each wave.
- Prevent regression with recurring detection and approval-based exceptions.
Common Pitfalls (and How to Avoid Them)
-
Breaking a service by forcing a password change
Fix: For service accounts, don’t force changes until you’ve confirmed credential update paths and dependencies. -
Clearing the flag and triggering immediate expiry unexpectedly
Fix: ReviewPasswordLastSetand your policy timelines; stage and communicate. -
Leaving “temporary exceptions” forever
Fix: Exceptions must have an owner, compensating controls, and a review/expiry date. -
Ignoring privileged accounts
Fix: Prioritize them; they’re the worst place to allow infinite password age.
Minimum Standard for Exceptions
If an account truly cannot rotate on a normal cadence, treat it as a controlled exception and require:
- Documented business reason and named owner
- Compensating controls (vaulting, monitoring, limited logon rights, least privilege, alerts)
- Review cadence (e.g., quarterly) and an end date or migration plan
Closing Thoughts
Removing “Password never expires” isn’t just a checkbox change—it’s a governance upgrade. Done correctly, you’ll reduce long-lived credential risk, simplify audits, and push service identity toward safer patterns like gMSA and managed rotation.


