Detection rules › Elastic

Potential Okta Brute Force (Multi-Source)

Status
production
Severity
medium
Time window
30m
Group by
okta.actor.alternate_id
Author
Elastic
Source
github.com/elastic/detection-rules

Detects potential brute force attacks against a single Okta user account from multiple source IPs, indicating attackers rotating through proxy infrastructure to evade IP-based detection.

Known false positives

  • Users with legitimate multi-location access (mobile + home + office) experiencing concurrent login issues.
  • Shared service accounts accessed from multiple legitimate infrastructure IPs.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

PlatformRecord / event type
OktaSystem Log event type user.authentication.auth: Authenticate user.
OktaSystem Log event type user.authentication.auth_identifier_not_in_policy: Authenticate a user with an identifier that's not in the User Profile Policy.
OktaSystem Log event type user.authentication.auth_unconfigured_identifier: Fired after a user authenticates via a directory instance that is not the highest priority profile source for the user.
OktaSystem Log event type user.authentication.auth_via_AD_agent: Authenticate user with AD agent.
OktaSystem Log event type user.authentication.auth_via_IDP: Authenticate user via IDP.
OktaSystem Log event type user.authentication.auth_via_LDAP_agent: Authenticate user via LDAP agent.
OktaSystem Log event type user.authentication.auth_via_inbound_SAML: Authenticate user via inbound SAML.
OktaSystem Log event type user.authentication.auth_via_inbound_delauth: Authenticate user via inbound delauth.
OktaSystem Log event type user.authentication.auth_via_iwa: Authenticate user via IWA.
OktaSystem Log event type user.authentication.auth_via_mfa: User authenticated via MFA
OktaSystem Log event type user.authentication.auth_via_radius: Authentication of user via Radius.
OktaSystem Log event type user.authentication.auth_via_richclient: Authentication of a user via Rich Client.
OktaSystem Log event type user.authentication.auth_via_social: User authenticated via social login
OktaSystem Log event type user.authentication.authenticate: Authentication via device trust certificate.
OktaSystem Log event type user.authentication.dsso_via_non_priority_source: Desktop Single Sign On (DSSO) authentication has been attempted using a profile source that is not the highest priority profile source for the given Okta user.
OktaSystem Log event type user.authentication.slo: User single logout out (SLO) from app.
OktaSystem Log event type user.authentication.sso: User SSO to application
OktaSystem Log event type user.authentication.universal_logout: This event is fired when an admin or system account triggers Universal Logout against an app instance.
OktaSystem Log event type user.authentication.universal_logout.scheduled: This event is fired when an admin manually triggers Universal Logout for a user.
OktaSystem Log event type user.authentication.verify: User identity verified
OktaSystem Log event type user.session.start: User logged in to Okta

Rules detecting the same action

These rules filter on the same operation.

Rule body

[metadata]
creation_date = "2026/02/19"
integration = ["okta"]
maturity = "production"
updated_date = "2026/04/10"

[rule]
author = ["Elastic"]
description = """
Detects potential brute force attacks against a single Okta user account from multiple source IPs, indicating
attackers rotating through proxy infrastructure to evade IP-based detection.
"""
false_positives = [
    "Users with legitimate multi-location access (mobile + home + office) experiencing concurrent login issues.",
    "Shared service accounts accessed from multiple legitimate infrastructure IPs.",
]
from = "now-30m"
language = "esql"
license = "Elastic License v2"
name = "Potential Okta Brute Force (Multi-Source)"
note = """## Triage and analysis

### Investigating Potential Okta Brute Force (Multi-Source)

This rule identifies a single user account receiving failed authentication attempts from multiple unique source IPs. This pattern indicates attackers rotating through proxy infrastructure to evade IP-based detection while targeting a specific account.

#### Possible investigation steps
- Identify the targeted user account and determine if it has elevated privileges or sensitive access.
- Review the geographic distribution of source IPs for anomalies such as multiple countries or unusual locations.
- Examine the ASN ownership of source IPs for signs of proxy, VPN, or cloud infrastructure.
- Check if Okta flagged any of the sources as known threats or proxies.
- Determine if any authentication attempts succeeded following the failed attempts.
- Review the user's recent activity for signs of account compromise.

### False positive analysis
- Users traveling internationally with mobile devices may generate failed attempts from multiple locations.
- Service accounts accessed from distributed legitimate infrastructure may trigger this rule.
- Corporate VPN exit nodes spread across regions could appear as multiple IPs for a single user.

### Response and remediation
- If attack is confirmed, reset the user's password immediately.
- Review and potentially reset MFA for the targeted account.
- Block attacking IP addresses at the network perimeter.
- Consider implementing geo-restrictions for the targeted account if dispersed access is not expected.
- Monitor for any successful authentication that may indicate compromise.
"""
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://developer.okta.com/docs/reference/api/event-types/",
    "https://www.elastic.co/security-labs/testing-okta-visibility-and-detection-dorothy",
    "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 = "5889760c-9858-4b4b-879c-e299df493295"
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",
    "Use Case: Threat Detection",
    "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

// Create source mapping for analyst context
| EVAL Esql.source_info = CONCAT(
    "{\"ip\":\"", COALESCE(okta.client.ip::STRING, "unknown"),
    "\",\"country\":\"", COALESCE(client.geo.country_name, "unknown"),
    "\",\"asn\":\"", COALESCE(source.as.organization.name, "unknown"),
    "\",\"user_agent\":\"", COALESCE(okta.client.user_agent.raw_user_agent, "unknown"), "\"}"
  )

| STATS
    Esql.unique_source_ips = COUNT_DISTINCT(okta.client.ip),
    Esql.total_attempts = COUNT(*),
    Esql.unique_user_agents = COUNT_DISTINCT(okta.client.user_agent.raw_user_agent),
    Esql.unique_dt_hashes = COUNT_DISTINCT(okta.debug_context.debug_data.dt_hash),
    Esql.unique_asns = COUNT_DISTINCT(source.as.number),
    Esql.unique_countries = COUNT_DISTINCT(client.geo.country_name),
    Esql.first_seen = MIN(@timestamp),
    Esql.last_seen = MAX(@timestamp),
    Esql.source_ip_values = VALUES(okta.client.ip),
    Esql.source_mapping = VALUES(Esql.source_info),
    Esql.event_action_values = VALUES(event.action),
    Esql.user_agent_values = VALUES(okta.client.user_agent.raw_user_agent),
    Esql.device_values = VALUES(okta.client.device),
    Esql.is_proxy_values = VALUES(okta.security_context.is_proxy),
    Esql.geo_country_values = VALUES(client.geo.country_name),
    Esql.geo_city_values = VALUES(client.geo.city_name),
    Esql.source_asn_values = VALUES(source.as.number),
    Esql.source_asn_org_values = VALUES(source.as.organization.name),
    Esql.threat_suspected_values = VALUES(okta.debug_context.debug_data.threat_suspected),
    Esql.risk_level_values = VALUES(okta.debug_context.debug_data.risk_level),
    Esql.risk_reasons_values = VALUES(okta.debug_context.debug_data.risk_reasons)
  BY okta.actor.alternate_id

| EVAL
    Esql.attempts_per_ip = Esql.total_attempts * 1.0 / Esql.unique_source_ips,
    Esql.duration_seconds = DATE_DIFF("seconds", Esql.first_seen, Esql.last_seen)

| WHERE
    Esql.unique_source_ips >= 5
    AND Esql.total_attempts >= 10
    AND (
        Esql.unique_countries >= 2 OR
        Esql.unique_asns >= 3 OR
        Esql.unique_source_ips >= 8 OR
        Esql.unique_user_agents >= 3
    )

| SORT Esql.unique_source_ips DESC
| KEEP Esql.*, okta.actor.alternate_id
'''


[[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.001"
name = "Password Guessing"
reference = "https://attack.mitre.org/techniques/T1110/001/"


[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"

Stages and Predicates

Stage 1: from

FROM logs-okta.system-* METADATA _id, _version, _index

Stage 2: where

| 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

Stage 3: eval

| EVAL Esql.source_info = CONCAT(
    "{\"ip\":\"", COALESCE(okta.client.ip::STRING, "unknown"),
    "\",\"country\":\"", COALESCE(client.geo.country_name, "unknown"),
    "\",\"asn\":\"", COALESCE(source.as.organization.name, "unknown"),
    "\",\"user_agent\":\"", COALESCE(okta.client.user_agent.raw_user_agent, "unknown"), "\"}"
  )

Stage 4: stats

| STATS
    Esql.unique_source_ips = COUNT_DISTINCT(okta.client.ip),
    Esql.total_attempts = COUNT(*),
    Esql.unique_user_agents = COUNT_DISTINCT(okta.client.user_agent.raw_user_agent),
    Esql.unique_dt_hashes = COUNT_DISTINCT(okta.debug_context.debug_data.dt_hash),
    Esql.unique_asns = COUNT_DISTINCT(source.as.number),
    Esql.unique_countries = COUNT_DISTINCT(client.geo.country_name),
    Esql.first_seen = MIN(@timestamp),
    Esql.last_seen = MAX(@timestamp),
    Esql.source_ip_values = VALUES(okta.client.ip),
    Esql.source_mapping = VALUES(Esql.source_info),
    Esql.event_action_values = VALUES(event.action),
    Esql.user_agent_values = VALUES(okta.client.user_agent.raw_user_agent),
    Esql.device_values = VALUES(okta.client.device),
    Esql.is_proxy_values = VALUES(okta.security_context.is_proxy),
    Esql.geo_country_values = VALUES(client.geo.country_name),
    Esql.geo_city_values = VALUES(client.geo.city_name),
    Esql.source_asn_values = VALUES(source.as.number),
    Esql.source_asn_org_values = VALUES(source.as.organization.name),
    Esql.threat_suspected_values = VALUES(okta.debug_context.debug_data.threat_suspected),
    Esql.risk_level_values = VALUES(okta.debug_context.debug_data.risk_level),
    Esql.risk_reasons_values = VALUES(okta.debug_context.debug_data.risk_reasons)
  BY okta.actor.alternate_id

Stage 5: eval

| EVAL
    Esql.attempts_per_ip = Esql.total_attempts * 1.0 / Esql.unique_source_ips,
    Esql.duration_seconds = DATE_DIFF("seconds", Esql.first_seen, Esql.last_seen)

Stage 6: where

| WHERE
    Esql.unique_source_ips >= 5
    AND Esql.total_attempts >= 10
    AND (
        Esql.unique_countries >= 2 OR
        Esql.unique_asns >= 3 OR
        Esql.unique_source_ips >= 8 OR
        Esql.unique_user_agents >= 3
    )

Stage 7: sort

| SORT Esql.unique_source_ips DESC

Stage 8: keep

| KEEP Esql.*, okta.actor.alternate_id

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Esql.*KEEP Esql.*
okta.actor.alternate_idKEEP okta.actor.alternate_id