0x17: Kerberos Failure KDC_ERR_KEY_EXPIRED — Expired Password
Published
The domain controller refused the ticket request because the account's password has expired and has to be changed before Kerberos will issue a ticket — routine for people, but a problem worth chasing when it hits service accounts or comes from an address you do not recognise.
Did the attacker have the password? Cannot tell from this code alone — correlate with the events around it.
Microsoft's definition
KDC_ERR_KEY_EXPIRED
Password has expired—change password to reset
Source: Microsoft event 4771 Kerberos error codes · checked against the published table on every build · look up another code
Where it appears
- Event 4771 (Kerberos pre-authentication failed) on domain controllers: the Failure Code field
- Event 4768 (A Kerberos authentication ticket (TGT) was requested) on domain controllers: the Result Code field
- Microsoft Sentinel / Log Analytics: the Status column of the SecurityEvent table (EventID 4771 or 4768)
What it means
Failure code 0x17, KDC_ERR_KEY_EXPIRED, appears in Microsoft’s table for event 4771 as “Password has expired—change password to reset”, with the possible cause given as “The user’s password has expired.” The KDC located the principal and refused to issue a ticket because the long-term key derived from that account’s password is past its expiry date. The account is not deleted, not disabled and not locked — it simply cannot obtain tickets until the password is changed.
Microsoft’s documentation for both 4768 and 4771 states that “This event generates only on domain controllers”, so collect these events from every DC, not from member servers. The fields that drive triage are Account Name (TargetUserName), Client Address (IpAddress) — which Microsoft describes as the “IP address of the computer from which the TGT request was received” and which arrives as an IPv6 address, as ::ffff:IPv4_address, or as ::1 for localhost — plus ServiceName (normally krbtgt/DOMAIN), IpPort, and the DC name in Computer. Strip the ::ffff: prefix before matching the address against your IPv4 inventory.
One caution: 0x17 describes the state of the account’s key, not the outcome of checking what the client sent. Do not report it as confirmation that the requester knew the current password. If you need that judgement, look at what else the same account and client address produced — a run of 0x18 means someone was guessing.
Is it an attack?
| Pattern | Reading |
|---|---|
| A handful of 0x17 for one user, then a 4768 with Result Code 0x0 from the same workstation | Normal expiry; the user changed the password and carried on |
| 0x17 for one service or task account at a fixed interval from one or two internal hosts | A password policy expired a service credential; the service is broken, not attacked |
| 0x17 across many accounts at the same time of day | Scheduled expiry hitting a batch of users with the same password age |
| 0x17 for a leaver, dormant or retired service account, from an unfamiliar or external address | Someone still holds that credential — treat as an intrusion lead |
| 0x17 mixed with bursts of 0x18 or 0x6 from one source | List-driven guessing that happened to touch an expired account |
| 0x17 followed by 0x12 for the same account | The account also became disabled, locked or expired — check the AD object |
Because the traffic goes to the KDC, it may never produce a 4625 on a member server. Pair the DC view with endpoint detections such as the Windows 4625 password-spray rule. On the NTLM and interactive side, the equivalent condition surfaces as Sub Status 0xC0000071 (STATUS_PASSWORD_EXPIRED) in event 4625.
How to investigate
Start with which accounts and which sources are producing the code:
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID in (4768, 4771) and Status =~ "0x17"
| extend ClientIp = replace_string(IpAddress, "::ffff:", "")
| summarize Events = count(), Sources = dcount(ClientIp),
SourceList = make_set(ClientIp, 20),
FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated)
by TargetUserName, TargetDomainName, Computer
| order by Events desc
A single account with many events from one address over days is a stored credential; many accounts with one or two events each is routine expiry.
Next, separate expiry from guessing by checking whether the same sources also failed pre-authentication or asked for names that do not exist:
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID in (4768, 4771) and Status in~ ("0x17", "0x18", "0x6")
| extend ClientIp = replace_string(IpAddress, "::ffff:", "")
| summarize KeyExpired = countif(Status =~ "0x17"),
PreAuthFailed = countif(Status =~ "0x18"),
NoSuchUser = countif(Status =~ "0x6"),
Accounts = dcount(TargetUserName)
by ClientIp, Computer
| where KeyExpired > 0
| order by Accounts desc, PreAuthFailed desc
Finally, confirm whether the account recovered — a TGT success from the same address after the last 0x17 means the password was changed and the credential works again:
let expired = SecurityEvent
| where TimeGenerated > ago(7d) and EventID in (4768, 4771) and Status =~ "0x17"
| extend ClientIp = replace_string(IpAddress, "::ffff:", "")
| summarize ExpiredCount = count(), LastExpired = max(TimeGenerated) by TargetUserName, ClientIp;
SecurityEvent
| where TimeGenerated > ago(7d) and EventID == 4768 and Status =~ "0x0"
| extend ClientIp = replace_string(IpAddress, "::ffff:", "")
| join kind=inner expired on TargetUserName, ClientIp
| where TimeGenerated > LastExpired
| project TimeGenerated, Computer, TargetUserName, ClientIp, IpPort, ServiceName, ExpiredCount
If the source is a known internal host and the cadence is regular, find the service, task or saved credential on that machine and rotate it properly. If the account is one that should no longer authenticate, or the address is unfamiliar, keep it in the incident: identify the requesting process and review any service tickets (event 4769) from the same address. Exported Security logs can be grouped by account and client address in the free browser log analyzer.
Related codes
- 0x6 KDC_ERR_C_PRINCIPAL_UNKNOWN The domain controller could not find the requested account in its database — the name does not exist, and a run of these from one source is account enumeration.
- 0x12 KDC_ERR_CLIENT_REVOKED The domain controller refused the ticket request because the account itself is barred from logging on — Microsoft's examples are an account that is disabled, expired or locked out — rather than because of anything about the password.
- 0x18 KDC_ERR_PREAUTH_FAILED The domain controller rejected the pre-authentication data sent with a ticket request — Microsoft's listed cause is that the wrong password was provided; repeated 0x18 events for one account are a brute-force indicator.
- 0x20 KRB_AP_ERR_TKT_EXPIRED
Detect it
MITRE ATT&CK: T1078.002
Frequently Asked Questions
- Does 0x17 prove the client sent the correct password?
- Not on the evidence in the event. The code reports the state of the account's key — expired — and Microsoft's documentation does not state whether the KDC validates the supplied pre-authentication data before reporting the expiry. Treat 0x17 as a statement about the account, and look at the surrounding 0x18 and 0x0 events from the same client address to judge whether the requester actually knew a working password.
- Which event carries 0x17 — 4768 or 4771?
- Both can, so hunt across the pair. Microsoft's 4768 documentation lists 0x17 among that event's result codes and the 4771 table includes it too; only 0x10 and 0x18 are excluded from 4768, which Microsoft says "doesn't generate for Result Codes: 0x10 and 0x18". Both events are recorded only on domain controllers.
- Why does one service account produce 0x17 every few minutes?
- Because a domain password policy expired the account's password and the service, scheduled task or application pool holding the credential keeps retrying on its normal schedule. The giveaway is an evenly spaced stream of 0x17 from one or two known internal hosts for a single account, starting at the moment the maximum password age was reached.
- When is 0x17 actually a security lead rather than a help-desk ticket?
- When the account should not be authenticating at all — a leaver, a dormant account or a decommissioned service identity — or when the client address is external or belongs to a host that account has never used. In those cases something is still presenting that credential, and you should identify the source process before writing the events off as expiry noise.
Find 0x17 in your own logs
Load a domain controller Security event export into the free browser analyzer to group Kerberos failures by account, source address and failure code (nothing is uploaded), or run it through LogTriage for threat-intel enrichment and an AI-written incident report.