THREAT-CloudAI-AzureOpenAIKeyHijacking Microsoft Sentinel · KQL

Detect Azure OpenAI API Key Theft and Reverse-Proxy Resale (LLMjacking) in Microsoft Sentinel

Adversaries who obtain a victim's Azure OpenAI resource API key — via a leaked key committed to a public repository, a phished Entra ID identity holding Cognitive Services Contributor/User, or a compromised CI/CD secret — can invoke the Chat Completions, Completions, and Embeddings endpoints directly using key-based authentication rather than an Entra ID bearer token. Because Azure OpenAI key auth is a static bearer credential with no MFA and no corresponding Entra ID sign-in event, the theft is invisible to identity-centric monitoring; the only telemetry is the Cognitive Services resource's own diagnostic logs and token-usage metrics. The dominant monetization pattern (documented by Sysdig, Permiso P0 Labs, and Lacework Labs as 'LLMjacking') is reselling proxied access to the stolen key on underground marketplaces or Discord/Telegram channels through an open-source reverse-proxy (e.g. oai-reverse-proxy) that fans a single stolen key out to many paying end users. This produces a distinctive fingerprint at the victim's Cognitive Services resource: a sharp, sustained increase in request volume and token consumption originating from a large and rapidly growing set of distinct caller IP addresses and user-agent strings, all authenticating with the same underlying key, well outside the resource's normal single-application usage baseline. Left undetected the victim absorbs the full compute cost of the resold access — frequently thousands of dollars per day for GPT-4-class deployments — and may exhaust provisioned-throughput quota needed for legitimate production traffic.

MITRE ATT&CK

Tactic
Impact

KQL Detection Query

Microsoft Sentinel (KQL)
kusto
// THREAT-CloudAI-AzureOpenAIKeyHijacking (T1496.004 Cloud Service Hijacking - LLMjacking via stolen Azure OpenAI key)
let Lookback = 24h;
let BucketWindow = 1h;
let RequestVolumeThreshold = 300;
let UniqueIPThreshold = 10;
// Branch 1: high request volume fanned out across an unusually large number of caller IPs on one Cognitive Services resource
let ProxyResaleFanout = AzureDiagnostics
| where TimeGenerated > ago(Lookback)
| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
| where Category == "RequestResponse"
| where OperationName has_any ("ChatCompletions_Create", "Completions_Create", "Embeddings_Create", "ChatCompletions_Create_V2")
| summarize RequestCount = count(), UniqueCallerIPs = dcount(CallerIpAddress), UniqueUserAgents = dcount(UserAgent), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by ResourceId, bin(TimeGenerated, BucketWindow)
| where RequestCount >= RequestVolumeThreshold and UniqueCallerIPs >= UniqueIPThreshold
| extend AttackPattern = "High-Volume Multi-Source Invocation — Likely Reverse-Proxy Key Resale", RiskLevel = "Critical";
// Branch 2: token-usage / cost metric spike on the same Cognitive Services resource (confirms financial impact, not just call volume)
let TokenUsageSpike = AzureMetrics
| where TimeGenerated > ago(Lookback)
| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
| where MetricName in ("ProcessedPromptTokens", "GeneratedTokens", "TokenTransaction", "AzureOpenAIRequests")
| summarize TotalUsage = sum(Total), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by ResourceId, MetricName, bin(TimeGenerated, BucketWindow)
| extend AttackPattern = strcat("Token/Request Metric Spike: ", MetricName), RiskLevel = "High";
// Branch 3: API key regenerated (containment/rotation event) followed by abuse resuming from a fresh set of IPs within hours — indicates the leaked key was already circulating and a second key was also compromised or reused
let KeyRegen = AzureActivity
| where TimeGenerated > ago(Lookback)
| where OperationNameValue =~ "MICROSOFT.COGNITIVESERVICES/ACCOUNTS/REGENERATEKEY/ACTION"
| where ActivityStatusValue == "Succeeded"
| project RegenTime = TimeGenerated, ResourceId, Caller;
let PostRegenAbuse = KeyRegen
| join kind=inner (ProxyResaleFanout) on ResourceId
| where TimeGenerated > RegenTime and TimeGenerated < RegenTime + 6h
| extend AttackPattern = "Abuse Pattern Resumed Within 6h of Key Regeneration — Possible Second Compromised Credential", RiskLevel = "Critical"
| project TimeGenerated, ResourceId, AttackPattern, RiskLevel, RequestCount, UniqueCallerIPs, UniqueUserAgents, FirstSeen, LastSeen;
ProxyResaleFanout
| project TimeGenerated, ResourceId, AttackPattern, RiskLevel, RequestCount, UniqueCallerIPs, UniqueUserAgents, FirstSeen, LastSeen
| union (TokenUsageSpike | project TimeGenerated, ResourceId, AttackPattern, RiskLevel, RequestCount = toint(TotalUsage), UniqueCallerIPs = 0, UniqueUserAgents = 0, FirstSeen, LastSeen)
| union PostRegenAbuse
| sort by RiskLevel desc, RequestCount desc
high severity medium confidence

Detects LLMjacking against Azure OpenAI resources using Cognitive Services diagnostic logs (AzureDiagnostics, Category=RequestResponse) and usage metrics (AzureMetrics). The primary branch flags a single Cognitive Services resource receiving a high volume of Chat Completions/Completions/Embeddings calls from an unusually large and rapidly growing number of distinct caller IPs and user agents within a 1-hour window — the signature of a reverse-proxy fanning one stolen key out to many paying resale customers, rather than a single legitimate application. A secondary branch confirms financial impact via token-usage/cost metric spikes, and a tertiary branch flags abuse resuming within 6 hours of an API key regeneration, indicating a second compromised credential or incomplete containment.

Data Sources

Azure Cognitive Services diagnostic settings (Log Analytics workspace)AzureDiagnostics table (ResourceProvider=MICROSOFT.COGNITIVESERVICES, Category=RequestResponse)AzureMetrics table (Azure OpenAI token/request metrics)AzureActivity table (key regeneration management-plane events)

Required Tables

AzureDiagnosticsAzureMetricsAzureActivity

False Positives & Tuning

  • A legitimate multi-tenant SaaS application that itself proxies many end-user requests through a single shared Azure OpenAI deployment — baseline the expected caller IP range (typically the application's own egress IPs/CDN, not thousands of residential IPs) before tuning the unique-IP threshold
  • Load testing or capacity/burn-in testing of a new Azure OpenAI deployment performed by the platform team ahead of a product launch
  • A misconfigured client-side application that fans requests out through multiple regional gateways or a CDN, inflating unique caller IP counts without any credential compromise
  • Batch data-processing jobs (e.g. bulk embeddings generation for a RAG pipeline) that intentionally parallelize across many worker nodes/containers, each with a different egress IP
  • Migration of a workload to a serverless/autoscaling compute platform (e.g. Azure Functions, AKS with cluster autoscaler) that legitimately increases the diversity of caller IPs for the same application

Other platforms for THREAT-CloudAI-AzureOpenAIKeyHijacking


Testing Methodology

Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.

  1. Test 1Simulate High-Volume Multi-Source Azure OpenAI Invocation

    Expected signal: AzureDiagnostics RequestResponse entries for the test resource: OperationName=ChatCompletions_Create, 20 events within a short window, distinct UserAgent values per request (atomic-test-client-1 through 20).

  2. Test 2Simulate Token-Usage Cost Spike Metric

    Expected signal: AzureMetrics entries for the test resource: MetricName=ProcessedPromptTokens and GeneratedTokens showing an aggregate increase over the 1-hour bucket well above the resource's idle baseline.

  3. Test 3Simulate Abuse Recurrence After Key Regeneration

    Expected signal: AzureActivity entry: OperationNameValue=MICROSOFT.COGNITIVESERVICES/ACCOUNTS/REGENERATEKEY/ACTION, ActivityStatusValue=Succeeded, followed within minutes by AzureDiagnostics RequestResponse entries for the same ResourceId showing the second request burst.


Response Playbook

Triage

  1. Identify the affected Cognitive Services resource(s) and confirm whether the invocations authenticated via a static API key (api-key header) or an Entra ID bearer token — key-based auth with no matching AADSignInLogs/SigninLogs entry is the hallmark of a stolen-key resale scenario rather than a compromised interactive identity
  2. Enumerate the distinct caller IP addresses and user agents hitting the resource in the alerting window: a large, geographically dispersed set of IPs with generic/scripted user agents (e.g. python-requests, curl, or a known reverse-proxy client string) strongly indicates a multi-tenant reseller proxy rather than one legitimate application
  3. Check whether request volume and distinct-caller-IP count have been climbing steadily over the alerting window (consistent with a proxy operator advertising and onboarding new paying customers) versus a single instantaneous spike (more consistent with a scripted bulk-abuse or load-test false positive)
  4. Correlate token-usage/cost metrics (ProcessedPromptTokens, GeneratedTokens, AzureOpenAIRequests) against the resource's normal 30-day baseline to quantify the financial exposure and confirm this is not routine growth
  5. Determine how the key was likely obtained: search recent Git commits/CI logs for the key value, review Entra ID sign-in and Azure Activity Log for the identity that last read/regenerated the key (ListKeys/RegenerateKey calls), and check for any recently granted Cognitive Services Contributor/User role assignments that would explain unauthorized key access
  6. If a reverse-proxy fingerprint is confirmed, consider whether the proxy's specific request pattern (e.g. a distinctive System prompt injection, model-selection header, or the oai-reverse-proxy project's known request shape) is present in the RequestResponse log body to corroborate the resale hypothesis

Containment

  1. Regenerate (rotate) the exposed API key immediately via the Cognitive Services resource's Keys blade or az cognitiveservices account keys regenerate — this is the single action that stops the resale traffic, since the reverse proxy has no way to obtain the new key
  2. Where feasible, migrate the resource's authentication model from key-based auth to Entra ID (Azure AD) token-based auth with a managed identity, which eliminates the static bearer-credential attack surface entirely going forward
  3. Set or tighten a spending/quota cap (Azure Cost Management budget alert, or a Provisioned Throughput Unit ceiling) on the affected Cognitive Services resource so a future key leak cannot generate unbounded cost before detection
  4. Restrict the resource's network access (Private Endpoint / Azure OpenAI network ACLs / firewall allowed IP ranges) to only the known application's egress IPs, which independently blocks proxy traffic from arbitrary internet source IPs regardless of key validity
  5. Search all source-control repositories, CI/CD pipeline logs, notebooks, and internal wikis the key may have been committed or pasted into, and purge/rotate every location found — a key regenerated once but still present in a stale repo will be re-exfiltrated
  6. Notify the application owner(s) of the resource to re-deploy with the new key/managed identity and confirm legitimate traffic resumes normally post-rotation

Evidence Collection

  1. Full AzureDiagnostics RequestResponse log entries for the affected resource across the entire abuse window: CallerIpAddress, UserAgent, OperationName, DurationMs, ResultSignature for every request
  2. AzureMetrics time series for ProcessedPromptTokens, GeneratedTokens, TokenTransaction, and AzureOpenAIRequests across the abuse window and the preceding 30-day baseline period
  3. AzureActivity entries for any ListKeys, RegenerateKey, or role-assignment changes on the Cognitive Services resource in the 30 days prior to the abuse window
  4. Entra ID sign-in logs (SigninLogs/AADSignInLogs) for any identity holding Cognitive Services Contributor/User on the resource, to determine whether an interactive account compromise (versus a leaked static key) is the root cause
  5. A geolocation/IP-reputation enrichment of the distinct caller IP set observed during the abuse window, to support the reseller-proxy hypothesis and potentially identify the marketplace/channel the access was advertised on
  6. Azure Cost Management billing detail for the Cognitive Services resource covering the full abuse window, to quantify financial impact for the incident report

Escalation Criteria

  • !Confirmed key-based (non-Entra-ID) authentication carrying the bulk of abusive traffic, with no corresponding legitimate application account for the observed request pattern
  • !Distinct caller IP count in the hundreds or continuing to grow hour over hour, indicating an actively operating and scaling resale proxy rather than a one-off scripted abuse burst
  • !Abuse traffic resumes from a new set of IPs within hours of a key regeneration, indicating a second leaked credential, a compromised managed identity, or that the regeneration did not fully invalidate cached client-side keys
  • !Token-usage cost projection for the abuse window exceeds a material threshold (e.g. resource's normal monthly spend) within a single day
  • !The same identity/subscription shows this pattern across multiple Cognitive Services resources, suggesting a broader compromise of the Azure subscription or a shared secrets-management failure rather than a single leaked key

Investigation Guide

Related Techniques

Forensic Artifacts

  • >AzureDiagnostics RequestResponse entries for the Cognitive Services resource (CallerIpAddress, UserAgent, OperationName, ResultSignature, DurationMs, full request/response correlation ID)
  • >AzureMetrics token/request usage time series for the resource, both during the incident and for the preceding 30-day baseline
  • >AzureActivity management-plane events (ListKeys, RegenerateKey, RoleAssignment writes) for the resource and its resource group
  • >Entra ID sign-in logs for any identity with Cognitive Services Contributor/User role scoped to the resource
  • >Source-control / CI pipeline audit logs (e.g. GitHub secret-scanning alerts, Azure DevOps pipeline logs) that may show where and when the key was exposed
  • >Azure Cost Management / consumption export data for the resource, to build the financial-impact timeline

Tuning Guidance

The load-bearing control is distinguishing a legitimate multi-tenant application (which fans out across many caller IPs by design, e.g. a mobile app's user base or a CDN edge network) from a resale proxy. Build and maintain a per-resource allowlist of expected caller IP ranges/CIDRs (the application's known egress ranges, CDN edge ranges, or corporate NAT gateways) and only alert on caller IPs falling outside that allowlist, rather than alerting on raw unique-IP count alone — this single change removes the majority of false positives from legitimate consumer-facing or autoscaled applications. Track each resource's own 30-day baseline for request volume and caller diversity (see the hunting query) and alert on relative deviation from that baseline in addition to the fixed absolute thresholds, since a high-traffic production deployment's normal volume may already exceed the generic 300-requests/10-IPs threshold. Treat the RegenerateKey correlation branch as a standing containment-verification control — every key rotation event should automatically re-arm this detection for the following 24 hours regardless of prior alert status, to catch incomplete remediation.


Hunting Queries

30-day daily baseline of request volume and distinct caller-IP count per Cognitive Services resource, used to spot resources whose caller-IP diversity or volume has grown far beyond its own historical norm — the earliest, quietest signal of a key beginning to circulate on a resale channel, well before it crosses the fixed absolute thresholds used by the primary detection.

Hunting — KQL
kql
// Hunt: 30-day baseline of distinct caller IPs and request volume per Cognitive Services resource, to identify resources whose caller diversity has grown abnormally vs. their historical norm
AzureDiagnostics
| where TimeGenerated > ago(30d)
| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
| where Category == "RequestResponse"
| where OperationName has_any ("ChatCompletions_Create", "Completions_Create", "Embeddings_Create")
| summarize RequestCount = count(), UniqueCallerIPs = dcount(CallerIpAddress) by ResourceId, bin(TimeGenerated, 1d)
| sort by ResourceId, TimeGenerated asc
Hunting — SPL
spl
index=azure sourcetype="azure:diagnostics" resourceProvider="MICROSOFT.COGNITIVESERVICES" category="RequestResponse" operationName IN ("ChatCompletions_Create","Completions_Create","Embeddings_Create")
| bin _time span=1d
| stats count as RequestCount, dc(callerIpAddress) as UniqueCallerIPs by resourceId, _time
| sort resourceId, _time

Atomic Red Team Tests

Test 1 Simulate High-Volume Multi-Source Azure OpenAI Invocation
linux

Issues a burst of Chat Completions requests against a disposable test Azure OpenAI deployment from multiple simulated source IPs/user agents (via a local proxy rotating outbound identifiers or multiple cloud shell sessions) to reproduce the request-volume-plus-caller-diversity fan-out signature of a resold key. Use a throwaway test resource and deployment only — never a production Azure OpenAI resource.

Command

bash
for i in $(seq 1 20); do curl -s -X POST "https://df00tech-atomic-test.openai.azure.com/openai/deployments/test-gpt/chat/completions?api-version=2024-02-01" -H "api-key: $TEST_AOAI_KEY" -H "Content-Type: application/json" -H "User-Agent: atomic-test-client-$i" -d '{"messages":[{"role":"user","content":"atomic test ping"}],"max_tokens":5}' > /dev/null; done

Cleanup

bash
Regenerate the test resource's API key to invalidate the value used in this test: az cognitiveservices account keys regenerate --name df00tech-atomic-test --resource-group df00tech-atomic-test-rg --key-name key1

Expected Telemetry

AzureDiagnostics RequestResponse entries for the test resource: OperationName=ChatCompletions_Create, 20 events within a short window, distinct UserAgent values per request (atomic-test-client-1 through 20).

Expected Detection

Alert fires once RequestCount and UniqueCallerIPs/UserAgents both clear threshold within the 1-hour BucketWindow for the test ResourceId — AttackPattern='High-Volume Multi-Source Invocation'. In a lab run, lower RequestVolumeThreshold/UniqueIPThreshold to match the smaller atomic-test volume.

Test 2 Simulate Token-Usage Cost Spike Metric
linux

Generates a burst of longer-completion requests against the disposable test deployment to drive a measurable spike in ProcessedPromptTokens/GeneratedTokens metrics, simulating the cost-confirmation branch of the detection.

Command

bash
for i in $(seq 1 10); do curl -s -X POST "https://df00tech-atomic-test.openai.azure.com/openai/deployments/test-gpt/chat/completions?api-version=2024-02-01" -H "api-key: $TEST_AOAI_KEY" -H "Content-Type: application/json" -d '{"messages":[{"role":"user","content":"Write a 300 word essay about clouds."}],"max_tokens":400}' > /dev/null; done

Cleanup

bash
No resource state to clean up beyond the token usage already billed for the test call volume; confirm the test deployment's quota was not exhausted.

Expected Telemetry

AzureMetrics entries for the test resource: MetricName=ProcessedPromptTokens and GeneratedTokens showing an aggregate increase over the 1-hour bucket well above the resource's idle baseline.

Expected Detection

Alert fires on the TokenUsageSpike branch — AttackPattern='Token/Request Metric Spike: GeneratedTokens' (or ProcessedPromptTokens) — for the test ResourceId.

Test 3 Simulate Abuse Recurrence After Key Regeneration
linux

Regenerates the test resource's API key (simulating incident containment), then immediately re-issues a burst of requests using a second pre-provisioned test key to simulate a scenario where a second leaked credential lets abuse continue, reproducing the PostRegenAbuse detection branch.

Command

bash
az cognitiveservices account keys regenerate --name df00tech-atomic-test --resource-group df00tech-atomic-test-rg --key-name key1 && for i in $(seq 1 20); do curl -s -X POST "https://df00tech-atomic-test.openai.azure.com/openai/deployments/test-gpt/chat/completions?api-version=2024-02-01" -H "api-key: $TEST_AOAI_KEY2" -H "Content-Type: application/json" -H "User-Agent: atomic-test-client-post-regen-$i" -d '{"messages":[{"role":"user","content":"atomic test ping 2"}],"max_tokens":5}' > /dev/null; done

Cleanup

bash
Regenerate both test keys (key1 and key2) a final time to fully invalidate all credentials used during the test: az cognitiveservices account keys regenerate --name df00tech-atomic-test --resource-group df00tech-atomic-test-rg --key-name key1 && az cognitiveservices account keys regenerate --name df00tech-atomic-test --resource-group df00tech-atomic-test-rg --key-name key2

Expected Telemetry

AzureActivity entry: OperationNameValue=MICROSOFT.COGNITIVESERVICES/ACCOUNTS/REGENERATEKEY/ACTION, ActivityStatusValue=Succeeded, followed within minutes by AzureDiagnostics RequestResponse entries for the same ResourceId showing the second request burst.

Expected Detection

Alert fires on the PostRegenAbuse branch — AttackPattern='Abuse Pattern Resumed Within 6h of Key Regeneration — Possible Second Compromised Credential' — for the test ResourceId.

Related Detections

Tactic Hub