Detecting Risky Password Reset Activity in Microsoft Sentinel
Correlating Cloud and On-Premises Signals to Catch Multi-Reset Attempts
Password resets are a normal part of day-to-day life in any environment: users forget credentials, policies enforce rotation, and self-service flows help reduce helpdesk load. But when an account starts generating a burst of reset attempts in a short window, the story often changes from “user convenience” to “identity under active attack”.
In this post I’ll walk through a Microsoft Sentinel analytic rule that uses KQL to detect suspicious patterns of multiple password reset attempts per user, correlating signals from:
- Azure AD / Entra ID Audit Logs (including self-service password reset activity)
- On-premises Windows Security logs (event IDs
4723and4724) - Unix / Linux
authandauthprivsyslog events
Why multiple password reset attempts are risky
A single password change in isolation is rarely interesting. A cluster of changes or resets for the same user within seconds usually is. Some common scenarios:
- Online password guessing – an attacker scripts repeated password change attempts hoping to hit the right current value.
- Post-compromise cleanup – an attacker with valid credentials tries to gain or maintain control by cycling passwords quickly.
- Helpdesk or self-service abuse – social engineering or abused flows cause repeated reset operations.
- Hybrid identity blind spots – resets happening on-prem while cloud activity looks benign, or vice versa.
Looking at any one log source in isolation makes it easy to miss these patterns. The query below unifies multiple sources and focuses on bursts of activity per user, not just individual events.
Detection overview
The analytic rule is built around a few design principles:
- Exclude known noise such as AD sync / service accounts via a watchlist.
- Separate “expected” self-service flows that are usually non-malicious (policy-driven rotations).
- Normalize password reset events from cloud, Windows, and Unix into a single schema.
- Sessionize attempts using a sliding window to identify clusters of resets per account.
- Apply a user-level threshold (default: more than 5 attempts in a short time frame).
The core pieces:
The KQL analytic rule
The following query is intended to run as an analytic rule in Microsoft Sentinel. It correlates password reset activity across your hybrid identity stack and highlights accounts with multiple reset attempts above a defined threshold.
let knownServiceAccount = (_GetWatchlist('AD-Sync-Accounts'))
| project SearchKey;
let selfServicePasswordReset = dynamic([
"Self-service password reset flow activity progress",
"Change password (self-service)",
"Reset password (self-service)"
]);
// Self-service password reset flow activity progress is typically caused by a password policy which requires users to rotate passwords.
// This operation already implies the user has signed in successfully and therefore the password reset is non-malicious.
let PerUserThreshold = 5;
let action = dynamic(["change", "changed", "reset"]);
let pWord = dynamic(["password", "credentials"]);
let SSPR = AuditLogs
| where OperationName has_any (pWord)
and OperationName has_any (action)
and Result =~ "success"
| where OperationName !in (selfServicePasswordReset)
| mv-apply TargetResource = TargetResources on (
where TargetResource.type =~ "User"
| extend AccountType = tostring(TargetResource.type),
Account = tostring(InitiatedBy.user.userPrincipalName),
TargetUserName = tolower(tostring(TargetResource.userPrincipalName))
)
| where not(Account has_any (knownServiceAccount))
| project TimeGenerated, AccountType, Account, TargetUserName, Computer = "", Type, OperationName;
let OnPremisesReset =
union isfuzzy=true
(//Password reset events
//4723: An attempt was made to change an account's password
//4724: An attempt was made to reset an accounts password
SecurityEvent
| where EventID in ("4723","4724")
| where not(Account has_any (knownServiceAccount))
| extend Account = tostring(split(Account, "\\")[-1])
| project TimeGenerated, Computer, AccountType, Account, Type, TargetUserName),
(// Unix syslog password reset events
Syslog
| where Facility in ("auth","authpriv")
| where SyslogMessage has_any (pWord) and SyslogMessage has_any (action)
| extend AccountType = iif(SyslogMessage contains "root", "Root", "Non-Root")
| where SyslogMessage matches regex ".*password changed for.*"
| parse SyslogMessage with * "password changed for" Account
| extend TargetAccount = Account
| project TimeGenerated, AccountType, Account, Computer = HostName, Type)
| where Account has TargetUserName and isnotempty(TargetUserName)
;
union isfuzzy=true SSPR, OnPremisesReset
| where Account =~ TargetUserName
| sort by TimeGenerated asc, Account asc
| extend AttemptStart = row_window_session(TimeGenerated, 25s, 25s, Account != prev(Account))
| summarize
StartTimeUtc = min(TimeGenerated),
EndTimeUtc = max(TimeGenerated),
Computerlist = make_set(Computer, 25),
Computer = arg_max(Computer , TimeGenerated),
AccountType = make_set(AccountType, 25),
TargetUserName = arg_max(TargetUserName, TimeGenerated),
AttemptCount = dcount(AttemptStart) by Account
| order by TimeGenerated desc
| where AttemptCount > PerUserThreshold
| extend timestamp = StartTimeUtc,
HostName = tostring(split(Computer, '.', 0)[0]),
DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')),
Name = tostring(split(Account, '@', 0)[0]),
UPNSuffix = tostring(split(Account, '@', 1)[0]),
TargetName = tostring(split(TargetUserName,'@',0)[0]),
TargetUPNSuffix = tostring(split(TargetUserName,'@',1)[0])
How the detection works
1. Ignore noisy service accounts
The query starts by pulling a watchlist named AD-Sync-Accounts and projecting only the
SearchKey field:
- These accounts often generate background changes during sync operations.
- They are excluded wherever the
Accountfield is checked.
Keeping them out reduces false positives and keeps the output focused on user-driven activity.
2. Separate “expected” self-service flows
Not all password changes are suspicious. The selfServicePasswordReset array captures
specific operation names that typically indicate user-initiated, policy-driven flows such as:
- Self-service password reset flow activity progress
- Change password (self-service)
- Reset password (self-service)
These are explicitly removed from the AuditLogs portion to avoid alerting on
routine, successful self-service activity.
3. Collect cloud password reset events (SSPR)
The SSPR let block:
- Filters
AuditLogswhereOperationNamecontains password-related keywords and actions. - Requires
Result == "success"to focus on completed operations. - Expands
TargetResourcesto extract user-level context (initiator and target UPN). - Excludes known service accounts from the initiator side.
At this stage, we have a clean, cloud-side view of successful password operations initiated by real users.
4. Collect on-premises and Unix resets
The OnPremisesReset block uses a fuzzy union to combine two worlds:
- Windows SecurityEvent with event IDs
4723and4724(change/reset password). - Syslog (facilities
authandauthpriv) with messages indicating password changes.
A few notable steps:
- Windows account names are normalized from
DOMAIN\userto just the user part. - Unix messages are parsed using a regex and
parseto extract the account name. - An
AccountTypeis derived for Unix events (e.g., root vs non-root).
After both branches are normalized, the query keeps only events where the Account
field matches a valid TargetUserName, ensuring we’re looking at meaningful identity-level activity.
5. Correlate across sources and align on the user
The query then:
- Unions
SSPRandOnPremisesResetinto one dataset. - Ensures the initiator and target align via
where Account =~ TargetUserName. - Sorts chronologically by
TimeGeneratedandAccount.
At this point, you have a time-ordered stream of password-related activity per account across cloud and on-prem sources.
6. Detect “bursts” of attempts with row_window_session
This is where the multi-attempt detection happens:
| extend AttemptStart = row_window_session(
TimeGenerated,
25s,
25s,
Account != prev(Account)
)
row_window_session groups events into logical sessions based on:
- A 25-second window.
- A break in the stream when the
Accountchanges.
Multiple reset events that fall within this short window for the same account are treated as a
single “attempt session”. Later, the query counts distinct sessions per account via
dcount(AttemptStart) to derive AttemptCount.
7. Summarize per user and apply a threshold
The summarize step aggregates everything per Account:
- StartTimeUtc / EndTimeUtc – range of observed resets.
- Computerlist – set of involved hosts (up to 25).
- AccountType – set of associated account types (e.g., User, Root).
- TargetUserName – final target username seen.
- AttemptCount – number of distinct reset “sessions”.
Only accounts with AttemptCount > PerUserThreshold are kept. With a threshold of 5,
you’re focusing on users that had more than five distinct reset clusters in the time window of your rule.
Finally, the query enriches identities by splitting:
HostNameandDnsDomainfromComputer.NameandUPNSuffixfromAccount.TargetNameandTargetUPNSuffixfromTargetUserName.
Tuning and operational guidance
Thresholds and time windows
PerUserThreshold– set this based on environment size and normal behaviour. For highly sensitive accounts, you may want to alert on any burst above 2–3 sessions.- Session window – the 25-second value is a good starting point for automated attacks. If you see slower attack patterns, consider extending this window.
- Rule schedule – in Sentinel, running this as a near-real-time analytic rule (e.g., every 5–15 minutes) helps catch attacks while they’re in progress.
Expanding the watchlist
The AD-Sync-Accounts watchlist is a natural place to maintain “known noisy” identities:
- Directory sync and automation accounts.
- Break-glass or operational accounts that follow strict runbooks.
- Any account you’ve already covered with separate detections and controls.
Keep the list small and intentional; over-excluding will hide genuine risk.
Incident triage tips
When this rule fires, a few quick checks help prioritize:
- Is the account highly privileged (admin, service owner, break-glass)?
- Does the activity originate from unusual hosts or geolocations?
- Do you see parallel sign-in failures, conditional access blocks, or MFA prompts?
- Is there matching evidence in helpdesk tickets or user reports?
Combining this detection with sign-in risk signals and device health can quickly distinguish “user fat-fingering their password” from “active account takeover”.
Closing thoughts
Password resets are more than just a user convenience feature—they’re a powerful signal for identity-centric detection when you look at them in context. By correlating reset attempts across Azure AD, on-prem Windows logs, and Unix syslog, and by focusing on per-user clusters of activity instead of single events, this KQL analytic rule helps surface accounts that are very likely under attack.
If you run Microsoft Sentinel in a hybrid environment, consider deploying and tuning this detection as part of your broader identity threat hunting and incident response playbook.
Shoutout to Guus Verbeek for pointing out a minor issue in the splitting of the Account for On-Premises password changes. This issue is fixed in the KQL.