Potential Okta Password Spray (Single Source)

Last updated 13 days ago on 2026-02-19
Created 6 years ago on 2020-07-16

About

Detects potential password spray attacks where a single source IP attempts authentication against multiple Okta user accounts with repeated attempts per user, indicating common password guessing paced to avoid lockouts.
Tags
Domain: IdentityUse Case: Identity and Access AuditTactic: Credential AccessData Source: OktaData Source: Okta System LogsLanguage: esql
Severity
medium
Risk Score
47
MITRE ATT&CK™

Credential Access (TA0006)(external, opens in a new tab or window)

False Positive Examples
Corporate proxy or VPN exit nodes may aggregate traffic from multiple legitimate users with login issues.Automated processes or misconfigured applications retrying authentication may trigger this rule.
License
Elastic License v2(external, opens in a new tab or window)

Definition

Integration Pack
Prebuilt Security Detection Rules
Related Integrations

okta(external, opens in a new tab or window)

Query
text code block:
FROM 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_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.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 password spray 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), // Spray band: 2-6 attempts per user (deliberate slow spray below lockout) Esql.users_in_spray_band = SUM(CASE(Esql.user_attempts >= 2 AND Esql.user_attempts <= 6, 1, 0)), // Also track users with only 1 attempt (stuffing-like) for differentiation Esql.users_with_single_attempt = SUM(CASE(Esql.user_attempts == 1, 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) BY okta.client.ip // Calculate spray signature metrics | EVAL // Percentage of users in the spray band (2-6 attempts) Esql.pct_users_in_spray_band = Esql.users_in_spray_band * 100.0 / Esql.unique_users, // Attack duration in minutes (spray is paced, not bursty) Esql.attack_duration_minutes = DATE_DIFF("minute", Esql.first_seen, Esql.last_seen) // Password spraying detection logic: // - Many users targeted (>= 5) // - Hard cap below Okta lockout threshold (max <= 8 attempts per user) // - Majority of users in spray band (2-6 attempts) (at least 60%) // - Attack is paced over time (>= 5 minutes) (not a 10-second burst like stuffing) // - Minimum total attempts to reduce noise // Note: For IP rotation attacks, see "Distributed Password Spray Attack in Okta" rule | WHERE Esql.unique_users >= 5 AND Esql.total_attempts >= 15 AND Esql.max_attempts_per_user <= 8 AND Esql.max_attempts_per_user >= 2 AND Esql.pct_users_in_spray_band >= 60.0 AND Esql.attack_duration_minutes >= 5 | SORT Esql.total_attempts DESC | KEEP Esql.*, okta.client.ip

Install detection rules in Elastic Security

Detect Potential Okta Password Spray (Single Source) in the Elastic Security detection engine by installing this rule into your Elastic Stack.

To setup this rule, check out the installation guide for Prebuilt Security Detection Rules(external, opens in a new tab or window).