← Back to Explore
elasticmediumTTP
Potential Okta Credential Stuffing (Single Source)
Detects potential credential stuffing attacks where a single source IP attempts authentication against many Okta user accounts with minimal attempts per user, indicating the use of breached credential lists.
Detection Query
FROM logs-okta.system-* METADATA _id, _version, _index
| WHERE
data_stream.dataset == "okta.system"
AND (event.action LIKE "user.authentication.*" OR event.action == "user.session.start")
AND okta.outcome.reason IN ("INVALID_CREDENTIALS", "LOCKED_OUT")
AND okta.actor.alternate_id IS NOT NULL
// Build user-source context as JSON for enrichment
| EVAL Esql.user_source_info = CONCAT(
"{\"user\":\"", okta.actor.alternate_id,
"\",\"ip\":\"", COALESCE(okta.client.ip::STRING, "unknown"),
"\",\"user_agent\":\"", COALESCE(okta.client.user_agent.raw_user_agent, "unknown"), "\"}"
)
// FIRST STATS: Aggregate by (IP, user) to get per-user attempt counts
// This prevents skew from outlier users with many attempts
| STATS
Esql.user_attempts = COUNT(*),
Esql.user_dt_hashes = COUNT_DISTINCT(okta.debug_context.debug_data.dt_hash),
Esql.user_source_info = VALUES(Esql.user_source_info),
Esql.user_agents_per_user = VALUES(okta.client.user_agent.raw_user_agent),
Esql.devices_per_user = VALUES(okta.client.device),
Esql.is_proxy = VALUES(okta.security_context.is_proxy),
Esql.geo_country = VALUES(client.geo.country_name),
Esql.geo_city = VALUES(client.geo.city_name),
Esql.asn_number = VALUES(source.as.number),
Esql.asn_org = VALUES(source.as.organization.name),
Esql.threat_suspected = VALUES(okta.debug_context.debug_data.threat_suspected),
Esql.risk_level = VALUES(okta.debug_context.debug_data.risk_level),
Esql.risk_reasons = VALUES(okta.debug_context.debug_data.risk_reasons),
Esql.event_actions = VALUES(event.action),
Esql.first_seen_user = MIN(@timestamp),
Esql.last_seen_user = MAX(@timestamp)
BY okta.client.ip, okta.actor.alternate_id
// SECOND STATS: Aggregate by IP to detect credential stuffing pattern
// Now we can accurately measure the distribution of attempts across users
| STATS
Esql.unique_users = COUNT(*),
Esql.total_attempts = SUM(Esql.user_attempts),
Esql.max_attempts_per_user = MAX(Esql.user_attempts),
Esql.min_attempts_per_user = MIN(Esql.user_attempts),
Esql.avg_attempts_per_user = AVG(Esql.user_attempts),
Esql.users_with_single_attempt = SUM(CASE(Esql.user_attempts == 1, 1, 0)),
Esql.users_with_few_attempts = SUM(CASE(Esql.user_attempts <= 2, 1, 0)),
Esql.first_seen = MIN(Esql.first_seen_user),
Esql.last_seen = MAX(Esql.last_seen_user),
Esql.target_users = VALUES(okta.actor.alternate_id),
Esql.user_source_mapping = VALUES(Esql.user_source_info),
Esql.event_action_values = VALUES(Esql.event_actions),
Esql.user_agent_values = VALUES(Esql.user_agents_per_user),
Esql.device_values = VALUES(Esql.devices_per_user),
Esql.is_proxy_values = VALUES(Esql.is_proxy),
Esql.geo_country_values = VALUES(Esql.geo_country),
Esql.geo_city_values = VALUES(Esql.geo_city),
Esql.source_asn_values = VALUES(Esql.asn_number),
Esql.source_asn_org_values = VALUES(Esql.asn_org),
Esql.threat_suspected_values = VALUES(Esql.threat_suspected),
Esql.risk_level_values = VALUES(Esql.risk_level),
Esql.risk_reasons_values = VALUES(Esql.risk_reasons)
BY okta.client.ip
// Calculate stuffing signature: most users should have very few attempts
| EVAL Esql.pct_users_few_attempts = Esql.users_with_few_attempts * 100.0 / Esql.unique_users
// Credential stuffing: many users, most with 1-2 attempts each, low max per user
// Stacked stats gives us accurate per-user distribution instead of skewed averages
| WHERE
Esql.total_attempts >= 25
AND Esql.unique_users >= 15
AND Esql.max_attempts_per_user <= 2
AND Esql.pct_users_few_attempts >= 80.0
| SORT Esql.unique_users DESC
| KEEP Esql.*, okta.client.ip
Author
Elastic
Created
2024/06/17
Data Sources
OktaOkta System Logs
References
- https://support.okta.com/help/s/article/Troubleshooting-Distributed-Brute-Force-andor-Password-Spray-attacks-in-Okta
- https://www.okta.com/identity-101/brute-force/
- https://support.okta.com/help/s/article/How-does-the-Device-Token-work?language=en_US
- https://developer.okta.com/docs/reference/api/event-types/
- https://www.elastic.co/security-labs/testing-okta-visibility-and-detection-dorothy
- https://sec.okta.com/articles/2023/08/cross-tenant-impersonation-prevention-and-detection
- https://www.okta.com/resources/whitepaper-how-adaptive-mfa-can-help-in-mitigating-brute-force-attacks/
- https://www.elastic.co/security-labs/monitoring-okta-threats-with-elastic-security
- https://www.elastic.co/security-labs/starter-guide-to-understanding-okta
Tags
Domain: IdentityUse Case: Identity and Access AuditData Source: OktaData Source: Okta System LogsTactic: Credential AccessResources: Investigation Guide
Raw Content
[metadata]
creation_date = "2024/06/17"
integration = ["okta"]
maturity = "production"
updated_date = "2026/04/10"
[rule]
author = ["Elastic"]
description = """
Detects potential credential stuffing attacks where a single source IP attempts authentication against many Okta
user accounts with minimal attempts per user, indicating the use of breached credential lists.
"""
false_positives = [
"Corporate proxy or VPN exit nodes may aggregate traffic from multiple legitimate users.",
"Shared systems such as kiosks or conference room computers may have multiple users authenticating.",
]
from = "now-15m"
language = "esql"
license = "Elastic License v2"
name = "Potential Okta Credential Stuffing (Single Source)"
note = """## Triage and analysis
### Investigating Potential Okta Credential Stuffing (Single Source)
This rule identifies a single source IP attempting authentication against many user accounts with minimal attempts per user. This pattern indicates credential stuffing where attackers rapidly test breached username and password pairs.
#### Possible investigation steps
- Identify the source IP and determine if it belongs to known proxy, VPN, or cloud infrastructure.
- Review the list of targeted user accounts and check if any authentications succeeded.
- Examine the user agent strings for signs of automation or scripting tools.
- Check if Okta flagged the source as a known threat or proxy.
- Determine if any targeted accounts have elevated privileges or access to sensitive systems.
- Review the geographic location and ASN of the source IP for anomalies.
### False positive analysis
- Corporate proxies or VPN exit nodes may aggregate traffic from multiple legitimate users.
- Shared systems such as kiosks or conference room computers may have multiple users authenticating.
- Legitimate SSO integrations may generate multiple authentication attempts from a single source.
### Response and remediation
- If attack is confirmed, block the source IP at the network perimeter.
- Reset passwords for any accounts that may have been compromised.
- Enable or strengthen MFA for targeted accounts.
- Review Okta sign-on policies to add additional friction for suspicious authentication patterns.
- If this is a known legitimate source, consider adding an exception for the IP or ASN.
"""
references = [
"https://support.okta.com/help/s/article/Troubleshooting-Distributed-Brute-Force-andor-Password-Spray-attacks-in-Okta",
"https://www.okta.com/identity-101/brute-force/",
"https://support.okta.com/help/s/article/How-does-the-Device-Token-work?language=en_US",
"https://developer.okta.com/docs/reference/api/event-types/",
"https://www.elastic.co/security-labs/testing-okta-visibility-and-detection-dorothy",
"https://sec.okta.com/articles/2023/08/cross-tenant-impersonation-prevention-and-detection",
"https://www.okta.com/resources/whitepaper-how-adaptive-mfa-can-help-in-mitigating-brute-force-attacks/",
"https://www.elastic.co/security-labs/monitoring-okta-threats-with-elastic-security",
"https://www.elastic.co/security-labs/starter-guide-to-understanding-okta",
]
risk_score = 47
rule_id = "94e734c0-2cda-11ef-84e1-f661ea17fbce"
setup = "The Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule."
severity = "medium"
tags = [
"Domain: Identity",
"Use Case: Identity and Access Audit",
"Data Source: Okta",
"Data Source: Okta System Logs",
"Tactic: Credential Access",
"Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"
query = '''
FROM logs-okta.system-* METADATA _id, _version, _index
| WHERE
data_stream.dataset == "okta.system"
AND (event.action LIKE "user.authentication.*" OR event.action == "user.session.start")
AND okta.outcome.reason IN ("INVALID_CREDENTIALS", "LOCKED_OUT")
AND okta.actor.alternate_id IS NOT NULL
// Build user-source context as JSON for enrichment
| EVAL Esql.user_source_info = CONCAT(
"{\"user\":\"", okta.actor.alternate_id,
"\",\"ip\":\"", COALESCE(okta.client.ip::STRING, "unknown"),
"\",\"user_agent\":\"", COALESCE(okta.client.user_agent.raw_user_agent, "unknown"), "\"}"
)
// FIRST STATS: Aggregate by (IP, user) to get per-user attempt counts
// This prevents skew from outlier users with many attempts
| STATS
Esql.user_attempts = COUNT(*),
Esql.user_dt_hashes = COUNT_DISTINCT(okta.debug_context.debug_data.dt_hash),
Esql.user_source_info = VALUES(Esql.user_source_info),
Esql.user_agents_per_user = VALUES(okta.client.user_agent.raw_user_agent),
Esql.devices_per_user = VALUES(okta.client.device),
Esql.is_proxy = VALUES(okta.security_context.is_proxy),
Esql.geo_country = VALUES(client.geo.country_name),
Esql.geo_city = VALUES(client.geo.city_name),
Esql.asn_number = VALUES(source.as.number),
Esql.asn_org = VALUES(source.as.organization.name),
Esql.threat_suspected = VALUES(okta.debug_context.debug_data.threat_suspected),
Esql.risk_level = VALUES(okta.debug_context.debug_data.risk_level),
Esql.risk_reasons = VALUES(okta.debug_context.debug_data.risk_reasons),
Esql.event_actions = VALUES(event.action),
Esql.first_seen_user = MIN(@timestamp),
Esql.last_seen_user = MAX(@timestamp)
BY okta.client.ip, okta.actor.alternate_id
// SECOND STATS: Aggregate by IP to detect credential stuffing pattern
// Now we can accurately measure the distribution of attempts across users
| STATS
Esql.unique_users = COUNT(*),
Esql.total_attempts = SUM(Esql.user_attempts),
Esql.max_attempts_per_user = MAX(Esql.user_attempts),
Esql.min_attempts_per_user = MIN(Esql.user_attempts),
Esql.avg_attempts_per_user = AVG(Esql.user_attempts),
Esql.users_with_single_attempt = SUM(CASE(Esql.user_attempts == 1, 1, 0)),
Esql.users_with_few_attempts = SUM(CASE(Esql.user_attempts <= 2, 1, 0)),
Esql.first_seen = MIN(Esql.first_seen_user),
Esql.last_seen = MAX(Esql.last_seen_user),
Esql.target_users = VALUES(okta.actor.alternate_id),
Esql.user_source_mapping = VALUES(Esql.user_source_info),
Esql.event_action_values = VALUES(Esql.event_actions),
Esql.user_agent_values = VALUES(Esql.user_agents_per_user),
Esql.device_values = VALUES(Esql.devices_per_user),
Esql.is_proxy_values = VALUES(Esql.is_proxy),
Esql.geo_country_values = VALUES(Esql.geo_country),
Esql.geo_city_values = VALUES(Esql.geo_city),
Esql.source_asn_values = VALUES(Esql.asn_number),
Esql.source_asn_org_values = VALUES(Esql.asn_org),
Esql.threat_suspected_values = VALUES(Esql.threat_suspected),
Esql.risk_level_values = VALUES(Esql.risk_level),
Esql.risk_reasons_values = VALUES(Esql.risk_reasons)
BY okta.client.ip
// Calculate stuffing signature: most users should have very few attempts
| EVAL Esql.pct_users_few_attempts = Esql.users_with_few_attempts * 100.0 / Esql.unique_users
// Credential stuffing: many users, most with 1-2 attempts each, low max per user
// Stacked stats gives us accurate per-user distribution instead of skewed averages
| WHERE
Esql.total_attempts >= 25
AND Esql.unique_users >= 15
AND Esql.max_attempts_per_user <= 2
AND Esql.pct_users_few_attempts >= 80.0
| SORT Esql.unique_users DESC
| KEEP Esql.*, okta.client.ip
'''
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1110"
name = "Brute Force"
reference = "https://attack.mitre.org/techniques/T1110/"
[[rule.threat.technique.subtechnique]]
id = "T1110.004"
name = "Credential Stuffing"
reference = "https://attack.mitre.org/techniques/T1110/004/"
[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"