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.identitycloud.account.idsource.ipEsql.distance_kmEsql.travel_kmhEsql.window_secondsEsql.country_countEsql.event_countEsql.source_ip_valuesEsql.source_geo_country_name_valuesEsql.source_geo_city_name_valuesEsql.source_as_organization_name_valuesEsql.user_agent_original_valuesEsql.aws_cloudtrail_user_identity_arn_valuesEsql.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, andEsql.window_seconds. - Identify the credential type from
Esql.aws_cloudtrail_user_identity_type_valuesandEsql.aws_cloudtrail_user_identity_access_key_id_values. Long-termAKIAkeys used from two continents are a stronger signal than a shared role session. - Compare
Esql.user_agent_original_valuesacross 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.