Remediation Guides

How to Fix AD Accounts Whose Passwords Never Expire

26 September 2026 8 min read

AD Starter Scan – Non-Expiring Account Password (Nessus plugin 150483) means enabled Active Directory accounts carry the “Password never expires” flag, so they ignore your maximum password age. To fix it, list them with PowerShell, clear the flag and rotate those passwords, move service accounts to gMSAs, and put the remaining ones under a fine-grained password policy.

The catch is timing: clearing the flag on an account with a years-old password expires it immediately, and on a service account that means an outage.

What the scanner is actually detecting

Scanner Finding title ID Scope notes
Nessus AD Starter Scan – Non-Expiring Account Password Plugin 150483 Medium, Windows family. Skips disabled accounts unless thorough tests are enabled
PingCastle Check that there is no account with never-expiring passwords Rule S-PwdNeverExpires Stale Objects category. Enabled accounts only, and accounts whose password was set in the last 30 days are excluded
Tenable Identity Exposure Accounts With Never Expiring Passwords Indicator C-PASSWORD-DONT-EXPIRE Medium. Active user and computer accounts only

All three read the userAccountControl attribute and report accounts with the DONT_EXPIRE_PASSWORD bit set (hex 0x10000, decimal 65536). In Active Directory Users and Computers it is the Password never expires check box on the Account tab, and in PowerShell it is the PasswordNeverExpires property. Flags add up, so a normal enabled user (512) with the flag shows as 66048.

Why it matters: a domain controller calculates password expiry as pwdLastSet plus the effective maximum password age from the domain policy or a fine-grained policy. When this bit is set, the computed expiry is “never”, whatever the policy says.

Nessus runs the check from the Active Directory Starter Scan template over LDAPS with domain credentials. Tenable says results are accurate for up to 5,000 users, groups or machines, so in a larger domain treat the PowerShell inventory below as authoritative.

Real-world risk

This is not a remote exploit. Tenable rates the plugin Medium (CVSS v3 4.5) and maps the matching indicator to MITRE ATT&CK T1078, Valid Accounts. The flag says nothing about password strength. It removes the control that eventually invalidates a password that leaked unnoticed; PingCastle’s rationale is that an attacker who compromises such an account keeps long-term access.

The accounts that matter are service and administrator accounts, whose passwords are often set once, shared and stored in scripts. For a service account with an SPN, any domain user can request a Kerberos service ticket and try to crack the password offline, and a password that never changes gives that attempt unlimited time. Passwords harvested from memory stay useful for the same reason, which is why LSASS protection belongs in the same conversation.

For human users it is more nuanced. Microsoft’s security baseline dropped the password expiration policy as less effective than modern mitigations, but Microsoft says organizations without MFA, Entra Password Protection or similar controls should keep it. NIST SP 800-63B-4 says verifiers shall not require periodic password changes, but shall force a change on evidence of compromise. Whatever you decide for users, the answer for service accounts is automatic rotation, not an exemption.

How to confirm it

From a machine with the ActiveDirectory PowerShell module (RSAT), list enabled users with the flag and save a before snapshot:

Import-Module ActiveDirectory
Get-ADUser -Filter 'PasswordNeverExpires -eq $true -and Enabled -eq $true' -Properties PasswordLastSet, LastLogonDate, ServicePrincipalName, adminCount |
    Select-Object SamAccountName, PasswordLastSet, LastLogonDate, adminCount,
        @{n='SPNs';e={$_.ServicePrincipalName -join ';'}}, DistinguishedName |
    Sort-Object PasswordLastSet |
    Export-Csv .pwd_never_expires_before.csv -NoTypeInformation

Search-ADAccount -PasswordNeverExpires -UsersOnly returns the same set, including disabled accounts. To see every object type carrying the bit, use the LDAP bitwise filter:

Get-ADObject -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=65536)' |
    Group-Object ObjectClass | Select-Object Name, Count

Before changing anything, check the policy these accounts will fall under and preview when each password would expire:

Get-ADDefaultDomainPasswordPolicy | Select-Object MaxPasswordAge, MinPasswordLength
Get-ADFineGrainedPasswordPolicy -Filter * | Select-Object Name, Precedence, MaxPasswordAge, AppliesTo

$maxAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
Get-ADUser -Filter 'PasswordNeverExpires -eq $true -and Enabled -eq $true' -Properties PasswordLastSet |
    Select-Object SamAccountName, PasswordLastSet,
        @{n='WouldExpireOn';e={ if ($_.PasswordLastSet) { $_.PasswordLastSet + $maxAge } }} |
    Sort-Object WouldExpireOn

A date in the past means the password expires the moment you clear the flag. For accounts under a fine-grained policy, use Get-ADUserResultantPasswordPolicy -Identity <user> instead of the domain value. If the domain’s Maximum password age (Computer Configuration > Windows Settings > Security Settings > Account Policies > Password Policy) is 0, passwords never expire domain-wide and clearing flags changes nothing until a policy with an expiry applies.

How to fix it

1. Sort the list

  • Human and admin accounts: clear the flag and rotate the password.
  • Service accounts for Windows services, IIS application pools or scheduled tasks: move them to a group Managed Service Account.
  • Service accounts that cannot use a gMSA (third-party apps, LDAP bind accounts, appliances): long password, fine-grained policy, named owner and a rotation procedure.
  • Product accounts: Microsoft Entra Connect express setup creates an MSOL_ account with a long, complex password that does not expire. Rotate accounts like this through the product’s own procedure, never with a blind flag change.
  • Accounts nobody owns: disable them.

2. Clear the flag on user and admin accounts

Set-ADUser -Identity jdoe -PasswordNeverExpires $false
Set-ADUser -Identity jdoe -ChangePasswordAtLogon $true

Clear the flag first, as its own command: Microsoft documents that PasswordNeverExpires cannot be true while ChangePasswordAtLogon is true. For a batch, scope it to one OU and dry-run it first:

Get-ADUser -Filter 'PasswordNeverExpires -eq $true -and Enabled -eq $true' -SearchBase 'OU=Staff,DC=example,DC=com' |
    Set-ADUser -PasswordNeverExpires $false -WhatIf

Reset privileged account passwords now rather than waiting for them to expire, because they may predate your current length rules.

3. Move service accounts to gMSAs

A gMSA’s password is managed by the domain controllers, retrieved by the hosts you allow, and rotated automatically (every 30 days unless you set -ManagedPasswordIntervalInDays, which is fixed at creation). gMSAs require Windows Server 2012 or later, and failover clusters do not support them, although services running on a cluster can use them. If Get-KdsRootKey returns nothing, create the forest’s KDS root key first; domain controllers wait up to 10 hours before gMSAs can be created with a new key.

Add-KdsRootKey -EffectiveImmediately

New-ADServiceAccount -Name gmsa-app01 -DNSHostName gmsa-app01.example.com -PrincipalsAllowedToRetrieveManagedPassword 'SG-App01-Hosts'

# on each host that runs the service
Install-ADServiceAccount -Identity gmsa-app01
Test-ADServiceAccount -Identity gmsa-app01

Then set the service’s Log On account to EXAMPLEgmsa-app01$ with the password fields left blank, move any SPNs from the old account to the gMSA, and disable the old account once the service has run cleanly for a while.

4. Cover the rest with a fine-grained password policy

New-ADFineGrainedPasswordPolicy -Name 'SvcAccPSO' -Precedence 100 -MinPasswordLength 20 -MaxPasswordAge '365.00:00:00' -ComplexityEnabled $true -PasswordHistoryCount 24
Add-ADFineGrainedPasswordPolicySubject -Identity 'SvcAccPSO' -Subjects 'SG-ServiceAccounts'

Set-ADAccountPassword -Identity svc-backup -Reset -NewPassword (Read-Host -AsSecureString 'New password')
Set-ADUser -Identity svc-backup -PasswordNeverExpires $false

Set -MaxPasswordAge to an interval your rotation process can actually meet; the value above is only an example. The minimum length applies only when a password is set, hence the reset before clearing the flag. Update every system that uses the account in the same change window.

PingCastle also notes domain-joined Linux servers whose machine password never changes. On SSSD hosts, make sure ad_maximum_machine_account_password_age is not 0 (the default is 30 days).

How to verify the fix and rescan

Re-run the inventory against the domain controller your scanner targets (add -Server dc01.example.com) so replication lag does not mislead you. For individual accounts, read the computed expiry:

Get-ADUser svc-backup -Properties 'msDS-UserPasswordExpiryTimeComputed' |
    Select-Object SamAccountName, @{n='Expires';e={
        $v = $_.'msDS-UserPasswordExpiryTimeComputed'
        if ($v -eq 9223372036854775807) { 'Never' } elseif ($v -eq 0) { 'Must change now' } else { [datetime]::FromFileTime($v) } }}

Then rerun the Active Directory Starter Scan against the same domain controller with the same credentials and confirm plugin 150483 no longer lists the fixed accounts. A scan that could not bind over LDAPS reports nothing either, so check that it authenticated. For PingCastle, confirm S-PwdNeverExpires is gone or its count dropped.

What can break and how to roll back

  • Old passwords expire instantly. Users get a change prompt at their next interactive logon, but services, scheduled tasks, application pools and LDAP binds using that account fail to authenticate. Reset first, or use the WouldExpireOn preview.
  • gMSA hosts outside the allowed group fail Test-ADServiceAccount. Restart a host after adding it to the group so its Kerberos ticket includes the new membership.
  • Encryption types: Microsoft recommends configuring AES for managed service accounts; hosts that do not support the required types cannot use them.

To roll back one account, run Set-ADUser -Identity <account> -PasswordNeverExpires $true. The computed expiry returns to “never”, so an account that expired only because of age can log on again (if you also set ChangePasswordAtLogon, set it back to $false first). For a gMSA migration, point the service back to the old account, which is why you disable it before deleting it. Record any restored flag as a time-limited exception through your risk acceptance process.

Common false positive reasons

  • Disabled accounts appear in Nessus only when thorough tests are enabled.
  • Exchange HealthMailbox accounts carry the flag, but Exchange changes their passwords itself. PingCastle skips them when the password is recent; other tools may not.
  • Different tool rules: PingCastle ignores passwords set in the last 30 days, so its count can differ from Nessus without either being wrong.
  • Documented exceptions such as the MSOL_ account are real findings, not false positives. Tenable Identity Exposure lets you exclude them through its allowed users and organizational unit options.
  • Stale results: the report predates the change, or a different domain controller answered before replication.

FAQ

Does clearing “Password never expires” reset the password?

No. It only changes the flag. Expiry is then calculated from the existing pwdLastSet, so an old password can expire immediately.

Is there a GPO that removes the flag?

No. The flag lives on each account’s userAccountControl attribute. Group Policy only sets the maximum password age that the flag overrides.

Does NIST guidance make this finding irrelevant?

Not for service and admin accounts. NIST’s position covers periodic changes to user passwords and still requires a change after compromise, which a never-rotated service password rarely gets.

Should any account keep the flag?

Only a documented exception with an owner and a manual rotation schedule, when neither a gMSA nor a fine-grained policy works.

Tracking this finding across scans

Non-expiring passwords tend to come back whenever someone creates a service account in a hurry. SITEY, a self-hosted vulnerability management platform, imports Nessus results when you upload a .nessus export (it does not pull them automatically), so plugin 150483 can be tracked alongside your other findings. Per-finding retest works for Nessus findings, so it can confirm closure once the accounts are fixed.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing