Detect Azure OpenAI API Key Theft and Reverse-Proxy Resale (LLMjacking) in Splunk
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
SPL Detection Query
index=azure sourcetype="azure:diagnostics" resourceProvider="MICROSOFT.COGNITIVESERVICES" category="RequestResponse"
| search operationName IN ("ChatCompletions_Create","Completions_Create","Embeddings_Create","ChatCompletions_Create_V2")
| bin _time span=1h
| stats count as RequestCount, dc(callerIpAddress) as UniqueCallerIPs, dc(userAgent) as UniqueUserAgents, min(_time) as FirstSeen, max(_time) as LastSeen by resourceId, _time
| where RequestCount >= 300 AND UniqueCallerIPs >= 10
| eval AttackPattern="High-Volume Multi-Source Invocation - Likely Reverse-Proxy Key Resale", RiskLevel="Critical"
| append
[ search index=azure sourcetype="azure:metrics" resourceProvider="MICROSOFT.COGNITIVESERVICES" metricName IN ("ProcessedPromptTokens","GeneratedTokens","TokenTransaction","AzureOpenAIRequests")
| bin _time span=1h
| stats sum(total) as RequestCount, min(_time) as FirstSeen, max(_time) as LastSeen by resourceId, metricName, _time
| eval AttackPattern="Token/Request Metric Spike: ".metricName, RiskLevel="High" ]
| append
[ search index=azure sourcetype="azure:activity" operationNameValue="MICROSOFT.COGNITIVESERVICES/ACCOUNTS/REGENERATEKEY/ACTION" activityStatusValue="Succeeded"
| rename _time as RegenTime
| join resourceId
[ search index=azure sourcetype="azure:diagnostics" resourceProvider="MICROSOFT.COGNITIVESERVICES" category="RequestResponse"
| bin _time span=1h
| stats count as RequestCount, dc(callerIpAddress) as UniqueCallerIPs by resourceId, _time ]
| where _time > RegenTime AND _time < RegenTime + 21600
| eval AttackPattern="Abuse Pattern Resumed Within 6h of Key Regeneration - Possible Second Compromised Credential", RiskLevel="Critical" ]
| sort - RiskLevel, - RequestCount SPL equivalent spanning Azure Cognitive Services diagnostic logs and metrics ingested via the Splunk Add-on for Microsoft Cloud Services. Flags a resource receiving a high volume of Azure OpenAI inference calls from an unusually large set of distinct caller IPs/user agents in a 1-hour bucket (reverse-proxy resale signature), correlates with token/request metric spikes for cost confirmation, and flags abuse recurrence within 6 hours of a key regeneration event.
Data Sources
Required Sourcetypes
False Positives & Tuning
- Legitimate multi-tenant SaaS application proxying many end-user requests through one shared deployment
- Load/capacity testing of a new Azure OpenAI deployment ahead of launch
- Client-side application fanning requests across multiple regional gateways or a CDN
- Batch/RAG embeddings pipelines parallelized across many autoscaled workers
- Migration to serverless/autoscaling compute that legitimately increases caller IP diversity
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.
- 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).
- 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.
- 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.
References (8)
- https://attack.mitre.org/techniques/T1496/004/
- https://attack.mitre.org/tactics/TA0040/
- https://sysdig.com/blog/llmjacking-stolen-cloud-credentials-used-in-new-ai-attack/
- https://permiso.io/blog/exploring-llmjacking-attacks
- https://www.lacework.com/blog/llmjacking-stolen-cloud-credentials-used-in-new-ai-attack
- https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/monitor-openai
- https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/switching-endpoints
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1496.004/T1496.004.md
Response Playbook
Triage
- 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
- 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
- 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)
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- Full AzureDiagnostics RequestResponse log entries for the affected resource across the entire abuse window: CallerIpAddress, UserAgent, OperationName, DurationMs, ResultSignature for every request
- AzureMetrics time series for ProcessedPromptTokens, GeneratedTokens, TokenTransaction, and AzureOpenAIRequests across the abuse window and the preceding 30-day baseline period
- AzureActivity entries for any ListKeys, RegenerateKey, or role-assignment changes on the Cognitive Services resource in the 30 days prior to the abuse window
- 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
- 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
- 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.
// 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 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
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
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
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.
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
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
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.
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
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
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.