AWS Bedrock Model Invocation Impossible Travel


Description

Detects the same AWS principal invoking Amazon Bedrock models (InvokeModel, Converse, and their streaming variants) from geographically distant source IPs within a short window at an impossible travel speed. Stolen access keys and session credentials are frequently resold or reused for LLMjacking, and an inference call from a second continent minutes after a legitimate one is a strong indicator that the credential is being used by someone else.

Query · esql

from logs-aws.cloudtrail-*
| where
    data_stream.dataset == "aws.cloudtrail" and
    event.provider == "bedrock.amazonaws.com" and
    event.action in ("InvokeModel", "InvokeModelWithResponseStream", "Converse", "ConverseStream") and
    event.outcome == "success" and
    aws.cloudtrail.user_identity.arn is not null and
    source.ip is not null and
    source.geo.location is not null and
    source.geo.country_name is not null
| eval
    Esql.identity = coalesce(
        aws.cloudtrail.user_identity.session_context.session_issuer.arn,
        aws.cloudtrail.user_identity.arn
    ),
    Esql.source_geo_lat = st_y(source.geo.location),
    Esql.source_geo_lon = st_x(source.geo.location)
| where Esql.source_geo_lat is not null and Esql.source_geo_lon is not null
| stats
    Esql.first_lat = first(Esql.source_geo_lat, @timestamp),
    Esql.first_lon = first(Esql.source_geo_lon, @timestamp),
    Esql.last_lat = last(Esql.source_geo_lat, @timestamp),
    Esql.last_lon = last(Esql.source_geo_lon, @timestamp),
    Esql.event_count = count(*),
    Esql.country_count = count_distinct(source.geo.country_name),
    Esql.source_ip_values = values(source.ip),
    Esql.source_geo_country_name_values = values(source.geo.country_name),
    Esql.source_geo_city_name_values = values(source.geo.city_name),
    Esql.source_as_organization_name_values = values(source.as.organization.name),
    Esql.event_action_values = values(event.action),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.aws_cloudtrail_user_identity_arn_values = values(aws.cloudtrail.user_identity.arn),
    Esql.aws_cloudtrail_user_identity_type_values = values(aws.cloudtrail.user_identity.type),
    Esql.aws_cloudtrail_user_identity_access_key_id_values = values(aws.cloudtrail.user_identity.access_key_id),
    Esql.cloud_region_values = values(cloud.region),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by
    Esql.identity,
    cloud.account.id
| where Esql.event_count >= 2 and Esql.country_count >= 2
| eval
    Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.first_lon), " ", to_string(Esql.first_lat), ")")),
    Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.last_lon), " ", to_string(Esql.last_lat), ")"))
| eval
    Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
    Esql.window_seconds = date_diff("second", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
    Esql.travel_kmh = case(Esql.window_seconds > 0, round(Esql.distance_km * 3600.0 / Esql.window_seconds, 0), null),
    source.ip = mv_first(Esql.source_ip_values)
| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
| keep
    Esql.identity,
    cloud.account.id,
    source.ip,
    Esql.source_ip_values,
    Esql.source_geo_country_name_values,
    Esql.source_geo_city_name_values,
    Esql.source_as_organization_name_values,
    Esql.event_action_values,
    Esql.user_agent_original_values,
    Esql.aws_cloudtrail_user_identity_arn_values,
    Esql.aws_cloudtrail_user_identity_type_values,
    Esql.aws_cloudtrail_user_identity_access_key_id_values,
    Esql.cloud_region_values,
    Esql.distance_km,
    Esql.travel_kmh,
    Esql.window_seconds,
    Esql.country_count,
    Esql.event_count,
    Esql.timestamp_first_seen,
    Esql.timestamp_last_seen

Investigation fields

Pivot points the source recommends for triage.

  • Esql.identity
  • cloud.account.id
  • source.ip
  • Esql.distance_km
  • Esql.travel_kmh
  • Esql.window_seconds
  • Esql.country_count
  • Esql.event_count
  • Esql.source_ip_values
  • Esql.source_geo_country_name_values
  • Esql.source_geo_city_name_values
  • Esql.source_as_organization_name_values
  • Esql.user_agent_original_values
  • Esql.aws_cloudtrail_user_identity_arn_values
  • Esql.aws_cloudtrail_user_identity_access_key_id_values

Implementation guide

This rule requires AWS CloudTrail management events ingested via the AWS integration (aws.cloudtrail data stream) with source.geo.location and source.geo.country_name populated by the integration's GeoIP enrichment. Bedrock runtime operations are logged as management events by default; no Bedrock model invocation logging is required.

Known false positives

  • Multi-region automation or CI pipelines that share one IAM user or role session across distant egress IPs. Prefer per-region identities, or raise the distance and speed thresholds.
  • VPN, proxy, or cloud egress that geolocates incorrectly or jumps between distant points of presence. Validate the IP locations and ASNs before rotating credentials.

Analyst notes

Investigating AWS Bedrock Model Invocation Impossible Travel

The same AWS principal (role issuer ARN when present, otherwise aws.cloudtrail.user_identity.arn) invoked Amazon Bedrock models from source IPs whose geolocation implies travel faster than the rule thresholds (default at least 500 km and 800 km/h) inside the lookback window. Assumed-role sessions are grouped by the issuing role so re-assumes of the same role still correlate. Bedrock runtime calls are CloudTrail management events, so this works without model invocation logging.

Possible investigation steps

  • Review Esql.source_ip_values, Esql.source_geo_country_name_values, Esql.source_geo_city_name_values, Esql.source_as_organization_name_values, Esql.distance_km, Esql.travel_kmh, and Esql.window_seconds.
  • Identify the credential type from Esql.aws_cloudtrail_user_identity_type_values and Esql.aws_cloudtrail_user_identity_access_key_id_values. Long-term AKIA keys used from two continents are a stronger signal than a shared role session.
  • Compare Esql.user_agent_original_values across the two locations. A change from an SDK or console agent to a generic HTTP client or LLM proxy is common in LLMjacking.
  • Search CloudTrail for the same principal around the distant event for ListFoundationModels, GetFoundationModelAvailability, model access changes, or guardrail and logging changes.
  • If Bedrock model invocation logging is enabled, review prompts and token volume from the unfamiliar location.

False positive analysis

  • Shared credentials used by multi-region workloads or CI runners. Move these workloads to region-scoped roles.
  • VPN or proxy egress that geolocates inconsistently. Corroborate with ASN and user agent before acting.

Response and remediation

  • If the travel is not explained, deactivate the access key or revoke the role session, rotate credentials, and review IAM policies that grant bedrock:InvokeModel*.
  • Review Bedrock usage and billing for the period and enable service quotas or budgets to cap abuse.
Raw source AWS Bedrock Model Invocation Impossible Travel · Elastic TOML
Esc
Published by elastic/detection-rules ↗, licensed under Elastic License 2.0 ↗. Reproduced here unmodified.
[metadata]
creation_date = "2026/10/23"
integration = ["aws"]
maturity = "production"
min_stack_comments = "ES|QL FIRST/LAST aggregations and st_distance require 9.4.0+."
min_stack_version = "9.4.0"
updated_date = "2026/10/23"

[rule]
author = ["Elastic"]
description = """
Detects the same AWS principal invoking Amazon Bedrock models (InvokeModel, Converse, and their streaming variants) from
geographically distant source IPs within a short window at an impossible travel speed. Stolen access keys and session
credentials are frequently resold or reused for LLMjacking, and an inference call from a second continent minutes after
a legitimate one is a strong indicator that the credential is being used by someone else.
"""
false_positives = [
    """
    Multi-region automation or CI pipelines that share one IAM user or role session across distant egress IPs. Prefer
    per-region identities, or raise the distance and speed thresholds.
    """,
    """
    VPN, proxy, or cloud egress that geolocates incorrectly or jumps between distant points of presence. Validate the IP
    locations and ASNs before rotating credentials.
    """,
]
from = "now-60m"
interval = "10m"
language = "esql"
license = "Elastic License v2"
name = "AWS Bedrock Model Invocation Impossible Travel"
note = """## Triage and analysis

### Investigating AWS Bedrock Model Invocation Impossible Travel

The same AWS principal (role issuer ARN when present, otherwise `aws.cloudtrail.user_identity.arn`) invoked Amazon
Bedrock models from source IPs whose geolocation implies travel faster than the rule thresholds (default at least
500 km and 800 km/h) inside the lookback window. Assumed-role sessions are grouped by the issuing role so re-assumes
of the same role still correlate. Bedrock runtime calls are CloudTrail management events, so this works without model
invocation logging.

#### Possible investigation steps

- Review `Esql.source_ip_values`, `Esql.source_geo_country_name_values`, `Esql.source_geo_city_name_values`,
  `Esql.source_as_organization_name_values`, `Esql.distance_km`, `Esql.travel_kmh`, and `Esql.window_seconds`.
- Identify the credential type from `Esql.aws_cloudtrail_user_identity_type_values` and
  `Esql.aws_cloudtrail_user_identity_access_key_id_values`. Long-term `AKIA` keys used from two continents are a
  stronger signal than a shared role session.
- Compare `Esql.user_agent_original_values` across the two locations. A change from an SDK or console agent to a
  generic HTTP client or LLM proxy is common in LLMjacking.
- Search CloudTrail for the same principal around the distant event for `ListFoundationModels`,
  `GetFoundationModelAvailability`, model access changes, or guardrail and logging changes.
- If Bedrock model invocation logging is enabled, review prompts and token volume from the unfamiliar location.

### False positive analysis

- Shared credentials used by multi-region workloads or CI runners. Move these workloads to region-scoped roles.
- VPN or proxy egress that geolocates inconsistently. Corroborate with ASN and user agent before acting.

### Response and remediation

- If the travel is not explained, deactivate the access key or revoke the role session, rotate credentials, and
  review IAM policies that grant `bedrock:InvokeModel*`.
- Review Bedrock usage and billing for the period and enable service quotas or budgets to cap abuse.
"""
references = [
    "https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html",
    "https://www.elastic.co/docs/reference/integrations/aws/cloudtrail",
]
risk_score = 47
rule_id = "94025e72-d8bf-499d-967b-eb479d36afc8"
setup = """## Setup

This rule requires AWS CloudTrail management events ingested via the AWS integration (`aws.cloudtrail` data stream)
with `source.geo.location` and `source.geo.country_name` populated by the integration's GeoIP enrichment. Bedrock
runtime operations are logged as management events by default; no Bedrock model invocation logging is required.
"""
severity = "medium"
tags = [
    "Domain: Cloud",
    "Domain: GenAI",
    "Data Source: AWS",
    "Data Source: Amazon Web Services",
    "Data Source: AWS CloudTrail",
    "Platform: AWS",
    "Service: AWS Bedrock",
    "Use Case: Threat Detection",
    "Tactic: Credential Access",
    "Tactic: Initial Access",
    "Threat: Impossible Travel",
    "Threat: LLMjacking",
    "Threat: Unauthorized AI Usage",
    "Mitre Atlas: AML.T0012",
    "Mitre Atlas: AML.T0091",
    "Mitre Atlas: AML.T0091.000",
    "Resources: Investigation Guide",
    "Rule Type: ES|QL",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-aws.cloudtrail-*
| where
    data_stream.dataset == "aws.cloudtrail" and
    event.provider == "bedrock.amazonaws.com" and
    event.action in ("InvokeModel", "InvokeModelWithResponseStream", "Converse", "ConverseStream") and
    event.outcome == "success" and
    aws.cloudtrail.user_identity.arn is not null and
    source.ip is not null and
    source.geo.location is not null and
    source.geo.country_name is not null
| eval
    Esql.identity = coalesce(
        aws.cloudtrail.user_identity.session_context.session_issuer.arn,
        aws.cloudtrail.user_identity.arn
    ),
    Esql.source_geo_lat = st_y(source.geo.location),
    Esql.source_geo_lon = st_x(source.geo.location)
| where Esql.source_geo_lat is not null and Esql.source_geo_lon is not null
| stats
    Esql.first_lat = first(Esql.source_geo_lat, @timestamp),
    Esql.first_lon = first(Esql.source_geo_lon, @timestamp),
    Esql.last_lat = last(Esql.source_geo_lat, @timestamp),
    Esql.last_lon = last(Esql.source_geo_lon, @timestamp),
    Esql.event_count = count(*),
    Esql.country_count = count_distinct(source.geo.country_name),
    Esql.source_ip_values = values(source.ip),
    Esql.source_geo_country_name_values = values(source.geo.country_name),
    Esql.source_geo_city_name_values = values(source.geo.city_name),
    Esql.source_as_organization_name_values = values(source.as.organization.name),
    Esql.event_action_values = values(event.action),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.aws_cloudtrail_user_identity_arn_values = values(aws.cloudtrail.user_identity.arn),
    Esql.aws_cloudtrail_user_identity_type_values = values(aws.cloudtrail.user_identity.type),
    Esql.aws_cloudtrail_user_identity_access_key_id_values = values(aws.cloudtrail.user_identity.access_key_id),
    Esql.cloud_region_values = values(cloud.region),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by
    Esql.identity,
    cloud.account.id
| where Esql.event_count >= 2 and Esql.country_count >= 2
| eval
    Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.first_lon), " ", to_string(Esql.first_lat), ")")),
    Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.last_lon), " ", to_string(Esql.last_lat), ")"))
| eval
    Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
    Esql.window_seconds = date_diff("second", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
    Esql.travel_kmh = case(Esql.window_seconds > 0, round(Esql.distance_km * 3600.0 / Esql.window_seconds, 0), null),
    source.ip = mv_first(Esql.source_ip_values)
| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
| keep
    Esql.identity,
    cloud.account.id,
    source.ip,
    Esql.source_ip_values,
    Esql.source_geo_country_name_values,
    Esql.source_geo_city_name_values,
    Esql.source_as_organization_name_values,
    Esql.event_action_values,
    Esql.user_agent_original_values,
    Esql.aws_cloudtrail_user_identity_arn_values,
    Esql.aws_cloudtrail_user_identity_type_values,
    Esql.aws_cloudtrail_user_identity_access_key_id_values,
    Esql.cloud_region_values,
    Esql.distance_km,
    Esql.travel_kmh,
    Esql.window_seconds,
    Esql.country_count,
    Esql.event_count,
    Esql.timestamp_first_seen,
    Esql.timestamp_last_seen
'''


[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1528"
name = "Steal Application Access Token"
reference = "https://attack.mitre.org/techniques/T1528/"


[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1078"
name = "Valid Accounts"
reference = "https://attack.mitre.org/techniques/T1078/"
[[rule.threat.technique.subtechnique]]
id = "T1078.004"
name = "Cloud Accounts"
reference = "https://attack.mitre.org/techniques/T1078/004/"



[rule.threat.tactic]
id = "TA0001"
name = "Initial Access"
reference = "https://attack.mitre.org/tactics/TA0001/"
[[rule.threat_mappings]]
framework = "MITRE ATLAS"
version = "2026.09"
[[rule.threat_mappings.threat]]
framework = "MITRE ATLAS"
[[rule.threat_mappings.threat.technique]]
id = "AML.T0012"
name = "Valid Accounts"
reference = "https://atlas.mitre.org/techniques/AML.T0012/"


[rule.threat_mappings.threat.tactic]
id = "AML.TA0004"
name = "Initial Access"
reference = "https://atlas.mitre.org/tactics/AML.TA0004/"
[[rule.threat_mappings.threat]]
framework = "MITRE ATLAS"
[[rule.threat_mappings.threat.technique]]
id = "AML.T0091"
name = "Use Alternate Authentication Material"
reference = "https://atlas.mitre.org/techniques/AML.T0091/"
[[rule.threat_mappings.threat.technique.subtechnique]]
id = "AML.T0091.000"
name = "Application Access Token"
reference = "https://atlas.mitre.org/techniques/AML.T0091.000/"



[rule.threat_mappings.threat.tactic]
id = "AML.TA0015"
name = "Lateral Movement"
reference = "https://atlas.mitre.org/tactics/AML.TA0015/"

[rule.alert_suppression]
group_by = ["Esql.identity"]
missing_fields_strategy = "suppress"

[rule.investigation_fields]
field_names = [
    "Esql.identity",
    "cloud.account.id",
    "source.ip",
    "Esql.distance_km",
    "Esql.travel_kmh",
    "Esql.window_seconds",
    "Esql.country_count",
    "Esql.event_count",
    "Esql.source_ip_values",
    "Esql.source_geo_country_name_values",
    "Esql.source_geo_city_name_values",
    "Esql.source_as_organization_name_values",
    "Esql.user_agent_original_values",
    "Esql.aws_cloudtrail_user_identity_arn_values",
    "Esql.aws_cloudtrail_user_identity_access_key_id_values",
]

[rule.alert_suppression.duration]
unit = "h"
value = 4

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.