Microsoft Foundry Subscription Key Impossible Travel

Last updated a day ago on 2026-10-05
Created 5 days ago on 2026-10-01

About

Detects the same API Management subscription key calling Microsoft Foundry from source IPs in two or more countries, far enough apart and fast enough that the hop is not physical travel. Calls are grouped by apim_subscription_id. Requests with no subscription key are ignored, because those are unauthenticated calls and share no key.
Tags
Data Source: Microsoft FoundryUse Case: Threat DetectionMitre Atlas: AML.T0091Mitre Atlas: AML.T0012Tactic: Credential AccessTactic: Initial AccessRule Type: ES|QLPlatform: AzureDomain: CloudDomain: GenAIService: Azure API ManagementThreat: Impossible TravelLanguage: esql
Severity
medium
Risk Score
47
MITRE ATT&CK™

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

Initial Access (TA0001)(external, opens in a new tab or window)

False Positive Examples
A subscription key used from a VPN, a cloud VM, or a corporate proxy whose egress geo is far from the developer's network. A home ISP and a cloud build agent on the same key look like impossible travel. One shared subscription key in front of users in different countries. Split that key per application, or raise the distance and speed thresholds in the query.
License
Elastic License v2(external, opens in a new tab or window)

Definition

Integration Pack
Prebuilt Security Detection Rules
Related Integrations

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

Query
text code block:
from logs-azure_ai_foundry.logs-* | where data_stream.dataset == "azure_ai_foundry.logs" and azure.ai_foundry.category == "GatewayLogs" and azure.ai_foundry.properties.apim_subscription_id 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.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_region_name_values = values(source.geo.region_name), Esql.source_geo_city_name_values = values(source.geo.city_name), Esql.source_as_organization_name_values = values(source.as.organization.name), Esql.azure_ai_foundry_properties_user_agent_values = values(azure.ai_foundry.properties.user_agent), Esql.azure_ai_foundry_properties_backend_response_code_values = values(azure.ai_foundry.properties.backend_response_code), Esql.azure_ai_foundry_properties_apim_subscription_id = min(azure.ai_foundry.properties.apim_subscription_id), Esql.timestamp_first_seen = min(@timestamp), Esql.timestamp_last_seen = max(@timestamp) by azure.ai_foundry.properties.apim_subscription_id, azure.ai_foundry.properties.api_id, azure.ai_foundry.properties.operation_id, azure.resource.name, azure.resource.group, url.domain, url.path | 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_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen), Esql.travel_kmh = case(Esql.window_minutes > 0, round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null), source.ip = MV_FIRST(Esql.source_ip_values) | where Esql.distance_km >= 500 and Esql.travel_kmh >= 800 | keep azure.ai_foundry.properties.apim_subscription_id, azure.ai_foundry.properties.api_id, azure.ai_foundry.properties.operation_id, azure.resource.name, azure.resource.group, url.domain, url.path, source.ip, Esql.*

Install detection rules in Elastic Security

Detect Microsoft Foundry Subscription Key Impossible Travel 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).