- Published on
How to Find Users with "Password Never Expires" in Active Directory (PowerShell Script)
PasswordNeverExpires is one of the first things a security audit checks, and it's almost never empty. Every account with this flag set is permanently exempt from your domain's password expiration policy — sometimes for a legitimate reason (a service account that can't handle a forced rotation), sometimes because someone checked the box once and forgot about it.
The quick answer
Get-ADUser -Filter {PasswordNeverExpires -eq $true -and Enabled -eq $true} -Properties PasswordNeverExpires
A more useful reporting script
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Lists Active Directory user accounts with "Password never expires" set.
.DESCRIPTION
Queries Active Directory for enabled user accounts where the
PasswordNeverExpires flag is set, optionally scoped to an OU, and
optionally exporting the results to CSV. This flag is a common finding in
security audits since it exempts an account from your domain's password
expiration policy indefinitely. Uses the current logged-on user's
credentials — run it from a domain-joined machine with RSAT installed.
.PARAMETER SearchBase
Distinguished name of the OU to search (e.g. "OU=Sales,DC=contoso,DC=com").
If omitted, searches the entire domain.
.PARAMETER IncludeDisabled
Also include disabled accounts in the results. By default only enabled
accounts are shown, since disabled accounts with this flag are lower risk.
.PARAMETER OutputCsv
Path to a CSV file to export the results to. If omitted, results are only
written to the console.
.EXAMPLE
.\Find-PasswordNeverExpiresADUsers.ps1
Lists all enabled users with password-never-expires set.
.EXAMPLE
.\Find-PasswordNeverExpiresADUsers.ps1 -IncludeDisabled -OutputCsv .\never-expire.csv
Lists all users (enabled and disabled) with password-never-expires set
and exports the results to never-expire.csv.
#>
[CmdletBinding()]
param(
[string]$SearchBase,
[switch]$IncludeDisabled,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$filter = if ($IncludeDisabled) {
{ PasswordNeverExpires -eq $true }
} else {
{ PasswordNeverExpires -eq $true -and Enabled -eq $true }
}
$params = @{
Filter = $filter
Properties = @('PasswordNeverExpires', 'Enabled', 'PasswordLastSet', 'DistinguishedName')
}
if ($SearchBase) {
$params['SearchBase'] = $SearchBase
}
$users = Get-ADUser @params |
Select-Object Name, SamAccountName, Enabled, PasswordLastSet, DistinguishedName |
Sort-Object Name
if (-not $users) {
Write-Host "No accounts found with password-never-expires set." -ForegroundColor Yellow
return
}
Write-Host "Found $(@($users).Count) account(s) with password-never-expires set." -ForegroundColor Cyan
$users | Format-Table Name, SamAccountName, Enabled, PasswordLastSet -AutoSize
if ($OutputCsv) {
$users | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
What to actually do with the list
Not every hit is a finding — some are legitimate service accounts that can't survive a forced password rotation. What matters is whether each one is documented as an intentional exception, with a reason and an owner, rather than just sitting there because nobody looked. PasswordLastSet in the output tells you how stale the password itself is, independent of the never-expires flag — a service account with never-expires set and a 4-year-old password is a bigger finding than one rotated last month.
Where the manual approach runs out of road
A one-off script is fine for a single check. It starts to hurt once you actually need to run this regularly:
- No scheduling. Cron/Task Scheduler can run the script, but now you own the scheduling, the credentials it runs as, and what happens when it silently fails.
- No history. A CSV export is a snapshot. Was this the same five accounts as last month, or a growing list? A single export can't tell you.
- No distribution. Getting the report to the right people (security, IT ops, compliance) on a schedule means building that plumbing yourself.
- Multi-domain/multi-forest pain. Run it once per domain, reconcile the results yourself, and hope naming/OU conventions are consistent across all of them.
- No alerting. If something changes unexpectedly between runs — an account re-enabled, a privileged group gaining a member — nothing tells you until you happen to run the script again.
None of that is a PowerShell problem. It's what turns a script into a product.
What SysFlint AD does instead
Our SysFlint AD runs the same kind of discovery shown above automatically, on a schedule, entirely inside your own network. No agents on domain controllers, no data leaving your environment. It's free, forever.
- Password and expiry reporting — Accounts with "password never expires", passwords older than your policy, and accounts that never set one at all.
- Scheduled reports by email — Daily, weekly, or monthly runs delivered to the right inbox automatically. No Task Scheduler job for you to babysit.
If you're currently doing this with a script on a scheduled task, here's the honest comparison between the two, or go straight to the download page.
Get this script, and the rest of them
The script above is part of awesome-it-scripts, SysFlint's free, open-source library of PowerShell scripts for Active Directory and other IT-admin tasks — no signup, no catch, MIT licensed. Grab this one directly from active-directory/Find-PasswordNeverExpiresADUsers.ps1, or browse the whole thing.
Find every disabled/locked-out/stale/privileged-account script we've published (plus the FAQ that goes with each one) in the repo's active-directory folder. Star it if it's useful — new scripts land there regularly.