Potential Okta Credential Stuffing (Single Source)
editPotential Okta Credential Stuffing (Single Source)
editDetects 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.
Rule type: esql
Rule indices: None
Severity: medium
Risk score: 47
Runs every: 5m
Searches indices from: now-15m (Date Math format, see also Additional look-back time)
Maximum alerts per execution: 100
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: Identity
- Use Case: Identity and Access Audit
- Data Source: Okta
- Data Source: Okta System Logs
- Tactic: Credential Access
- Resources: Investigation Guide
Version: 210
Rule authors:
- Elastic
Rule license: Elastic License v2
Investigation guide
editTriage 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.
Setup
editThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.
Rule query
editFROM logs-okta.system-* METADATA _id, _version, _index
| WHERE
event.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
Framework: MITRE ATT&CKTM
-
Tactic:
- Name: Credential Access
- ID: TA0006
- Reference URL: https://attack.mitre.org/tactics/TA0006/
-
Technique:
- Name: Brute Force
- ID: T1110
- Reference URL: https://attack.mitre.org/techniques/T1110/
-
Sub-technique:
- Name: Credential Stuffing
- ID: T1110.004
- Reference URL: https://attack.mitre.org/techniques/T1110/004/