Fix it now
18487 means the password of the account has expired. 18488 means it must be changed. Neither is corruption, a network fault or a permission problem, and neither is fixed by recreating the login. The question they raise is whether age-based expiry belongs on this login at all.
SELECT name, is_expiration_checked, is_policy_checked,
LOGINPROPERTY(name,'IsMustChange') AS must_change,
LOGINPROPERTY(name,'IsExpired') AS expired,
LOGINPROPERTY(name,'DaysUntilExpiration') AS days_left
FROM sys.sql_logins
ORDER BY days_left;
- For a person, connect once with SSMS using the old password and let it prompt for a replacement, or change it in T-SQL:
ALTER LOGIN [jsmith] WITH PASSWORD = 'new' OLD_PASSWORD = 'old'; - For a service account, set a new password:
ALTER LOGIN [svc_app] WITH PASSWORD = '<long random password>'; - Then decide whether expiry belongs on it:
ALTER LOGIN [svc_app] WITH CHECK_EXPIRATION = OFF; - For 18488, clear the flag by setting the password again without MUST_CHANGE, then confirm
LOGINPROPERTY('svc_app','IsMustChange')returns 0. - Restart the application or service so it stops presenting the old password, then confirm it connects.
Stop the application before you reset the password. A service reconnecting with the old one will keep failing, and on a policy that locks accounts it will lock the login again within seconds of each reset.
If the login connects without a change prompt, you are done. If the reset is itself being rejected, the next section covers the policy rules that produce that.
Why it happens
SQL Server does not maintain a password policy of its own. When a SQL login has CHECK_POLICY on, the instance asks Windows to validate the password against the policy in force on the machine hosting SQL Server: length, complexity, history, lockout and maximum age. On a domain member that is the domain policy; on a standalone server it is the local policy. That is the whole mechanism, and it explains most of the surprises.
CHECK_EXPIRATION is the separate switch that applies the maximum-age part, and it depends on CHECK_POLICY. Microsoft documents the T-SQL defaults as CHECK_POLICY on and CHECK_EXPIRATION off, and documents that CHECK_EXPIRATION cannot be turned on while CHECK_POLICY is off. So do not assume from a default what a given login has; read is_expiration_checked and is_policy_checked and find out.
MUST_CHANGE is the third option and it produces 18488. It marks the login so the password an administrator set must be replaced by the user before the login can do anything. Microsoft documents that MUST_CHANGE requires both CHECK_EXPIRATION and CHECK_POLICY to be on, otherwise the statement fails. It is a sensible setting for a person and a poor one for a service, because a service has no way to answer a prompt.
15128 is the engine enforcing exactly that relationship: CHECK_POLICY and CHECK_EXPIRATION cannot be turned off while MUST_CHANGE is on. It is not a fault, it is the order of operations. Clear MUST_CHANGE by setting the password, and only then change the policy switches.
A person’s password has expired
You have this one if 18487 for a named individual who connects with SSMS or another interactive tool.
- Connect with SSMS using the old password; it prompts for a replacement and sets it for you.
- Or change it in T-SQL by supplying the old one:
ALTER LOGIN [jsmith] WITH PASSWORD = 'new' OLD_PASSWORD = 'old'; - Confirm afterwards with
SELECT LOGINPROPERTY('jsmith','DaysUntilExpiration');
Supplying OLD_PASSWORD is the route that does not need ALTER ANY LOGIN. Microsoft documents that a principal can change the password for its own login, which is how a user resets themselves without an administrator.
A service account’s password has expired
You have this one if 18487 for an account used by an application, and the failure arrives for every connection at once rather than gradually.
- Stop the application or service so it stops presenting the old password.
- Set a new password:
ALTER LOGIN [svc_app] WITH PASSWORD = '<long random password>'; - Decide whether age-based expiry belongs here. If not:
ALTER LOGIN [svc_app] WITH CHECK_EXPIRATION = OFF; - Start the service with the new password and confirm it connects.
Turning CHECK_EXPIRATION off is a deliberate trade, not a shortcut. Do it with a long random password, minimum permissions and a note of why, or you have simply moved the problem.
MUST_CHANGE is set on an account that cannot answer a prompt
You have this one if 18488 on a login used by an application, immediately after somebody reset it with the change-at-next-login option ticked.
- Set the password again without the flag:
ALTER LOGIN [svc_app] WITH PASSWORD = '<new password>'; - Confirm it cleared:
SELECT LOGINPROPERTY('svc_app','IsMustChange');should return 0. - Then apply the policy settings you actually want, in that order.
Try to switch the policy options off while MUST_CHANGE is still set and you get 15128, which is the engine telling you to clear the flag first.
The new password is rejected by policy
You have this one if You are trying to fix the expiry and get a validation failure instead: 15116 for a password that is too short, 15118 for one that is not complex enough.
- Read the effective policy: run
secpol.mscon the SQL Server host and look at Account Policies, Password Policy, orgpresult /ron a domain member. - Meet the length and complexity rules that policy sets. Password history is the one people forget: a recently used password is refused.
- If the account genuinely should not be under policy, set
CHECK_POLICY = OFF, accepting that CHECK_EXPIRATION goes off with it.
The design is wrong rather than the password
You have this one if You are resetting the same service account every ninety days and it fails in production first, every time.
- Move the application to Windows authentication, so the account is managed once in Active Directory rather than twice.
- Where the application supports it, use a group managed service account so Windows rotates the password and nothing has to store it.
- If SQL authentication is unavoidable, keep expiry off on that login and rotate it on a schedule you control, in a window you choose.
Full reference
Telling the codes apart
| Code | What Microsoft publishes | Who it usually hits |
|---|---|---|
| 18487 | The password of the account has expired | Service accounts created years ago under a policy nobody re-read |
| 18488 | The password of the account must be changed | A login just created or reset by an administrator |
| 15116 | Password validation failed: the password does not meet the operating system policy requirements because it is too short | Anyone choosing a short password |
| 15118 | Password validation failed: the password does not meet the operating system policy requirements because it is not complex enough | Anyone choosing a simple password |
| 15128 | The CHECK_POLICY and CHECK_EXPIRATION options cannot be turned off when MUST_CHANGE is on | Anyone trying to disable policy before clearing the flag |
15116 and 15118 are frequently treated as the same message and they are not. One is about length and the other about complexity, and knowing which you have saves a round of guessing at a password the policy will accept.
The login options and how they constrain each other
| Option | Documented behaviour |
|---|---|
CHECK_POLICY |
Whether the Windows password policy of the host is enforced. Default ON in T-SQL |
CHECK_EXPIRATION |
Whether the maximum-age part is enforced. Default OFF. Cannot be ON while CHECK_POLICY is OFF |
MUST_CHANGE |
Prompts for a new password the first time the login is used. Requires both CHECK_EXPIRATION and CHECK_POLICY to be ON, or the statement fails |
UNLOCK |
Unlocks a locked-out SQL login. A password option on ALTER LOGIN |
OLD_PASSWORD |
Required when a principal changes its own password without ALTER ANY LOGIN |
Turning CHECK_POLICY off has two documented side effects people do not expect: the password history is cleared, and the lockout time is reset. Turning it back on initialises the history with the current password hash. If you are toggling it to unlock an account, that is fine; if you are toggling it in a hardened environment, know what you are resetting.
Reading LOGINPROPERTY without guessing
| Property | What it returns |
|---|---|
DaysUntilExpiration |
Days remaining. 0 if expired or expiring today, -1 if the Windows policy never expires passwords, NULL if CHECK_POLICY or CHECK_EXPIRATION is off |
IsExpired |
1 if the password has expired |
IsMustChange |
1 if the login must change its password on next connection |
IsLocked |
1 if the login is locked |
LockoutTime |
When the login was locked after too many failed attempts |
PasswordLastSetTime |
When the current password was set, which is what a maximum age is measured from |
BadPasswordCount |
Consecutive attempts with an incorrect password, which identifies the service still retrying |
A NULL from DaysUntilExpiration is the answer people misread most often. It does not mean the login is fine; it means expiry is not being checked on it at all, which may or may not be what you intended. -1 is different again, and means the policy in force never expires passwords.
Unlocking without changing the password
Microsoft documents toggling CHECK_POLICY off and back on as a way to unlock a login without setting a new password. It works because turning the policy off resets the lockout time. Use it when the password is known to be correct and an application simply hammered it until the account locked, and pair it with fixing whatever was hammering it, because otherwise the account locks again on the next retry.
Seeing the next wave before it lands
A maximum password age is measured from the day the password was set, so logins created together expire together, and nobody records the date at creation time. The query at the top of this article, ordered by days remaining, tells you which logins expire next. Run it on a schedule and this error stops being a surprise, which is a better outcome than getting quicker at fixing it.
Every code this article covers
| Code | What it points at | Source |
|---|---|---|
18487 |
Login failed because the password of the account has expired. Severity 14 | Microsoft Learn |
18488 |
Login failed because the password of the account must be changed. Severity 14 | Microsoft Learn |
15118 |
Password validation failed: the password does not meet the operating system policy requirements because it is not complex enough. Severity 16 | Microsoft Learn |
15128 |
The CHECK_POLICY and CHECK_EXPIRATION options cannot be turned off when MUST_CHANGE is on. Severity 16 | Microsoft Learn |
15116 |
Password validation failed: the password does not meet the operating system policy requirements because it is too short. Severity 16 | Microsoft Learn |
Confirm the fix worked
- Connect with the affected login and confirm it succeeds without a change prompt.
SELECT LOGINPROPERTY('<login>','IsMustChange');returns 0.SELECT LOGINPROPERTY('<login>','DaysUntilExpiration');returns a value you understand: a number you are comfortable with, -1 for a policy that never expires, or NULL because expiry is deliberately off.SELECT LOGINPROPERTY('<login>','BadPasswordCount');is not climbing, which proves nothing is still retrying with the old password.- The application reconnects and runs its real workload, not just a test from SSMS.
Questions people ask about this
Is turning CHECK_EXPIRATION off a security failure?
Not on its own. It is a considered decision for an account that cannot answer a password prompt. The failure would be turning it off and leaving a short or shared password in place. Pair it with a long random secret, minimum permissions and a rotation schedule you actually run.
Why can I not turn the policy off on this login?
Because MUST_CHANGE is still set, and Microsoft documents that CHECK_POLICY and CHECK_EXPIRATION cannot be turned off while it is on. That is error 15128. Set the password again without the flag first, confirm IsMustChange returns 0, then change the policy switches.
Does this cost anything to fix?
No. Password policy behaviour is the same in every edition, including the free ones. This is a configuration change using T-SQL and Windows policy tools you already have.
Why did every application fail on the same morning?
Because a maximum password age is measured from the day the password was set. Logins created together expire together, and nobody writes the date down. Query DaysUntilExpiration across the instance now and you will see the next wave coming.
The reset itself is rejected. What is wrong?
Read which code you got. 15116 is a password that is too short and 15118 is one that is not complex enough, both judged against the Windows policy on the host. Password history also applies, so a recently used password is refused even when it meets the other rules.
