Picture of Ryan Hausknecht

Ryan Hausknecht

Principal Lead Researcher Microsoft Azure, TrustOnCloud

(An Attempt at) Detecting Managed Identity Abuse

Whilst researching our ThreatModel for Azure Managed Identity, we discovered some challenges in detecting Managed Identity (MI) abuse that are worth discussing in more detail.

Managed Identities (MI) within Azure can be, and have been, abused in several ways. For instance, creating a system-assigned MI can serve as an effective persistence tactic because the MI has rights over the resource on which it was created. MIs can also be significantly exploited for privilege escalation, particularly in the case of user-assigned Managed Identities.

The aim of this article is to provide a few examples of how Managed Identities can be misused and how to detect this abuse by utilizing the logging features within Azure & Entra. Additionally, this article will highlight the deficiencies in logging across Azure & Entra, and suggest potential improvements.

Detection Philosophy

The first question is, what makes detecting MI abuse more difficult than other attack techniques within Azure? Detecting MI abuse requires logging from both the resource and Entra ID. The default logs on the resource are typically enough, but for Entra ID the logs need to be forwarded to a Log Analytics workspace (or whatever platform you’re using). 

Figure 1: Entra diagnostic log settings

Figure 1: Entra diagnostic log settings

The other massive challenge with detecting MI (or any identity) abuse is determining what is malicious behavior vs. standard behavior. There’s several key data points within the logs from the resource and from Entra ID that can be correlated in order to determine when something is off. The detection theory is then to establish a baseline of what is normal activity for the Managed Identity, then create a query that will return anything abnormal. Each resource will have different logs and properties/data points to look at, so several examples will be shown.

| where OperationName == 'Add service principal' and Resource == 'Microsoft.aadiam'

Finally, detection engineering can vary in difficulty depending on the size of the tenant and/or subscriptions. Tenants that have 100k+ users will have a lot of activity, so there’s some detections that may not be as accurate in comparison to a smaller tenant.

Detecting Managed Identity Persistence

First and foremost, detecting MI abuse for persistence is actually very straightforward. By looking at the Entra ID log provider, AuditLogs, a search for newly created system-assigned MIs with a filter on the resource will return any newly created managed identity.

This will result in a log which you can then determine the resource the MI was created on. 

Figure 2: AuditLog event from a new MI being created

Figure 2: AuditLog event from a new MI being created

System-assigned MIs are created very differently than user-assigned. A user-assigned MI first must be created (as it is its own resource), then it can be assigned to a resource. Since system-assigned MIs are resource specific, you do not need to do this. Since there’s the additional step of creating a user-assigned MI, detecting it can be performed by querying the resource-specific ActivityLog. 


Unfortunately, the AuditLogs in Entra for creating a user-assigned vs. system-assigned MI do not differ a whole lot. There’s no property for what type of MI it is, however by looking at the ‘ManagedIdentityResourceId’, the type can be determined.

| mv-expand TargetResources
| extend Properties = TargetResources.modifiedProperties
| mv-expand Properties
| where Properties.displayName == "ManagedIdentityResourceId"
| where tostring(Properties.newValue) contains "Microsoft.ManagedIdentity"

Figure 3: AuditLog event showing the details around a user-assigned MI.

For a user-assigned MI, it will always contain the ‘Microsoft.ManagedIdentity’ namespace, whereas for a system-assigned MI, it will always be the resource for which it was created.

Screenshot 2024 01 30 124919

Figure 4: AuditLog event showing the details around a system-assigned MI.

Finally, the detection for the assignment of the user-assigned MI to a resource can be generalized so it can be resource agnostic by searching the responseBody property of all logs and searching for the ‘Microsoft.ManagedIdentity’ namespace.

| extend ParsedProperties = parse_json(Properties)
| extend ParsedResponseBody = tostring(ParsedProperties.responseBody)
| extend ParsedIdentity = parse_json(ParsedResponseBody).identity
| where ParsedIdentity contains 'Microsoft.ManagedIdentity'

Screenshot 2024 01 30 125145

Figure 5: Above query results showing the assignment of a user-assigned MI.

This query avoids having to create several detections for each resource and instead will return anytime a user-assigned MI is assigned.

Detecting Managed Identity Token Abuse

Unfortunately there’s no silver bullet query that will detect when a MI token has been stolen or even generated. In a perfect world, anytime a MI token is generated a log will be created for it and the details surrounding the creation will be logged. However, this is not the case and instead there are no logs whenever a token is created, so we must rely on authentication logs instead and compare it to a baseline of what ‘normal’ activity looks like for a MI. 

Consider the following scenario:

An Azure Function App uses a user-assigned MI to fetch the contents of a Storage Account Container. The MI also has Contributor privileges over the entire subscription. An attacker compromises an account which has write privileges over the Function App and edits it so it instead generates a token for the MI, which they use to then login to the subscription.

From a detection point of view, there’s several things we can detect here, but for the point of this article we’re going to focus specifically on where the token is used to login to the tenant. To make matters even more difficult, the sign in logs for MIs are quite poor. They lack UserAgent context and the location property is always empty. This really only leaves one property to look at, ResourceDisplayName. 

First, we must establish what is considered “normal”. In this scenario, normal is when the MI interacts with the Storage Account directly. In ‘AADManagedIdentitySignInLogs’, we can see that the ResourceDisplayName property is ‘Azure Storage’. 

Screenshot 2024 01 30 125333

Figure 6: Sample ‘AADManagedIdentitySignInLogs’ log showing the audience (ResourceDisplayName).

Before a MI requests a token, the audience must first be established. In this example, the audience for the token is ‘Azure Storage’. Below is a small list of audiences and their respective resources:

Azure Resource Managerhttps://management.azure.com/
Azure Key Vaulthttps://vault.azure.net/
Azure SQL Databasehttps://database.windows.net/
Azure Event Hubshttps://eventhubs.azure.com/
Azure Service Bushttps://servicebus.azure.com/
Azure Storagehttps://storage.azure.com/
Azure Active Directory Graphhttps://graph.windows.net/
Microsoft Graphhttps://graph.microsoft.com/
Azure Logic Appshttps://logic.azure.com/

There’s many more, but these are some common ones. Considering our scenario, the attacker then generates an Azure JWT via Function App, but this time for the ‘management.azure.com’ audience. 

image 1

Figure 7: Crafting a MI JWT in a FunctionApp.

This gives the attacker much more access over the subscription now and has control over several resources within the subscription. The only difference in the logs is the ResourceDisplayName field. 

image 7

Figure 8: New audience for the token

A detection could then be created which will look back X number of days (I chose 30) to establish a baseline of what a normal ResourceDisplayName is for that MI, then return any that are abnormal (I chose a threshold of 5%).

image 2

Figure 9: Result of a query that returns anomalous audiences

There’s two variables that need to be established:

  1. How far back to look in data to establish a baseline
  2. The threshold in percentage for what would be considered anomalous
let AnomalyPercentageThreshold = 0.05; // 5%
let TotalOccurrences = AADManagedIdentitySignInLogs
| where TimeGenerated > ago(30d)
| summarize TotalCount = count() by ServicePrincipalId, ServicePrincipalName;
| where TimeGenerated > ago(30d)
| summarize Count = count() by ServicePrincipalId, ServicePrincipalName, ResourceDisplayName
| join kind=inner (TotalOccurrences) on ServicePrincipalId, ServicePrincipalName
| extend PercentageDiff = round(100.0 * Count / TotalCount, 2) // Calculate the percentage and round to two decimal places
| where PercentageDiff <= AnomalyPercentageThreshold * 100 and TotalCount > 1 // Compare percentage and ensure more than one total occurrence
| project ServicePrincipalId, ServicePrincipalName, ResourceDisplayName, Count, TotalCount, AnomalyPercentageThreshold, PercentageDiff
| order by ServicePrincipalId, ServicePrincipalName, ResourceDisplayName

These variables are dependent on the environment and your sample count. For example, if over 30 days there’s only five times a MI has legitimately been used, then using a 5% threshold will be much too low. If the MI abuse is within the resource it normally is used, then looking at resource-specific detections can help.

Resource-Specific Detections

In this same scenario, a query that returns whenever a FunctionApp is changed would detect whenever someone changes a FunctionApp. 

| where OperationNameValue == 'MICROSOFT.WEB/SITES/WRITE'

The problem with this is that operation could be very common in some organizations. Log quality is also highly dependent on the resource itself. For FunctionApps for example, there’s no logs that will disclose what was written/changed on the application. However, in comparison, the virtual machine resource can utilize the logging on the OS itself to craft a detection that will alert whenever a JWT is generated. 

Closing Thoughts

The biggest gap in detection for MI abuse remains detecting the forging system-assigned MI tokens and abusing that access, as well as user-assigned MI tokens when accessing the same resources as the MI legitimately does. Unfortunately, the logs are indistinguishable from when a system-assigned token is used legitimately vs. an adversary forging one and logging in under the MI.

If Microsoft were to also include location data in the properties (it’s an existing property already, just blank) and if they included UserAgent headers, then detecting MI abuse would be significantly more accurate. In addition, time-based anomaly detections were researched as well. An example of this would be anytime a FunctionApp is triggered, there should be a log for the MI signing in at the same exact time. If the time is different, then the token was used outside of the FunctionApp and therefore suspicious. However, the MI sign in logs are very inconsistent when viewing them through Log Analytics. 

Figure 10: Portal view of MI sign in logs shows an accurate login time that coincides with the FunctionApp execution.

Figure 10: Portal view of MI sign in logs shows an accurate login time that coincides with the FunctionApp execution

Figure 11: Log Analytics view of the same event. There’s a massive ~3 minute time difference.

Figure 11: Log Analytics view of the same event. There’s a massive ~3 minute time difference.

When logging is this inconsistent, it questions the integrity of several detections and makes it extremely difficult to craft time-based detections. Hopefully Microsoft recognizes not just the fact that logs ingested into Log Analytics are incorrect, but that MI logs in general are very poor and could use more context details.

Subscribe for Email Updates
Subscribe to TrustOnCloud's blog and newsletter for the latest insights on cloud security and industry best practices.
“Using TrustOnCloud has enabled us (as a very large Global organization) to move into our lowest level environment within about two weeks, and I know some small companies that can’t even move that fast.” 

— Director Cloud Security, Global Top 10 bank

Ready to see for yourself?