
Falco Feeds extends the power of Falco by giving open source-focused companies access to expert-written rules that are continuously updated as new threats are discovered.

The Sysdig Threat Research Team (TRT) recently watched as an attacker started with a single leaked service-principal credential and, by the next morning, owned the tenant. This attacker took complete, dual-plane control of the tenant with Global Administrator (GA) ownership of the directory and root-level access over every resource. They planted persistence across dozens of identities and took the keys to the telemetry pipeline that was supposed to be watching them.
Stopping this attack was easy. However, analyzing the attack was actually quite complex, and it came down to one question with many answers:
After this attack, who and what can actually touch this tenant?
In Azure, that question does not have a single answer, nor is there a single place to find one. Permission lives in at least five disjointed systems that Azure doesn’t unify. These permission systems don't share identifiers either; they have different logs and data models. An attacker can hop between them without getting caught precisely because defenders rarely look at more than one at a time.
So, let’s explore how one credential walked through all five permission systems, and why a "single pane of glass" for cloud identity is still mostly aspirational.
How it started: One credential, many hands
The entry point was unglamorous and extremely common: a leaked service-principal client secret. Service principals are Azure's non-human identities (NHIs) – the accounts that apps, automation, and CI/CD pipelines use to authenticate. The Sysdig 2026 Cloud-Native Security and Usage Report states that just over 38% of Azure service principals are considered risky. This means they may carry a long-lived secret or certificate or be over-permissioned, or that no one is getting a push notification when one of these identities logs in from the other side of the world.
This particular secret was exposed for about two weeks before the attacker found it. During that window, it was replayed from a rotating set of anonymization infrastructure: commercial VPN ranges, Tor exit nodes, and small hosting providers across the US, Germany, Sweden, Hong Kong, and the Netherlands. The diversity of origins is itself a finding. This wasn't one tracked human, but a credential sitting in some shared or automated pool, getting picked up and replayed by multiple independent, opportunistic consumers. Most of them did nothing but reconnaissance: enumerate ARM, poke at Graph, list storage and Key Vault.
Then, one session stopped doing reconnaissance and started escalating.
What the Sysdig TRT observed
All times are in UTC. Every operation below returned success.
Phase 1: Directory escalation (03:27–03:50)
- 03:27 — First valid sign-in as the service principal from a Netherlands hosting ASN.
- 03:28 — Add member to role (self).
- 03:29:57 — Add member to role outside of PIM (permanent) → Global Administrator, at the directory root. Approximately two and a half minutes from first login to owning the entire Entra directory. (Note: How could a service principal grant itself GA? It already held a Graph API permission that allows exactly that. See "The fifth surface" below.)
- 03:29–03:50 — Attacker-controlled client secrets added to 26 application registrations. (More on why this number matters below.)
Phase 2: Resource-plane escalation + key harvest (04:32–04:37)
- 04:32:37 —
Microsoft.Authorization/elevateAccess/action→ User Access Administrator at root over Azure resources. - 04:32:44 —
roleAssignments/writeat subscription scope (an Owner-class role, self-assigned). - 04:33 —
storageAccounts/listKeyson four storage accounts. - 04:33:37–38 —
listKeysonrootmanagesharedaccesskeyfor both Event Hub namespaces: The ones carrying the tenant's own audit telemetry. - 04:34 —
Microsoft.KeyVault/vaults/accessPolicies/write(self-add) on three Key Vaults, followed byroleAssignments/writeon the same vaults. Data-plane secret reads followed.
Phase 3: Persistence and return (through ~09:57)
- Continued access throughout the morning. One of the planted secrets was later re-used from a different egress to confirm the backdoor worked. Then, the attacker went quiet.
From first login to owning the directory, every subscription resource, the storage keys, Event Hub keys, and Key Vault access took the attacker about an hour of active effort, spread across one morning. There was no malware, no exploits, and no zero-days, just permissions – granted, escalated, and harvested – through entirely legitimate Azure APIs.
Post-incident, to identify everything the attacker controls by 09:57, you’ll need to refer to at least five different places to get the full answer, and none of them will tell you they’re missing pieces the others hold.
The real problem: Five permission planes that never meet
Plane 1: Entra directory roles
The Global Administrator (GA) grant lives in Entra ID (formerly Azure AD), in directory role management. This is the plane most people think of as "Azure permissions": Global Admin, Privileged Role Administrator, and the directory roles you assign to people and applications.
If you check only this plane after an incident, you'll see the GA grant and conclude: “Remove that assignment and we're clean.” You would be deeply wrong, however, because the attacker has already left this plane and moved on to the next.
Plane 2: Azure Role-Based Access Control (RBAC)
Azure RBAC is the resource control plane. This includes the Owner / Contributor / User Access Administrator roles you assign on subscriptions, resource groups, and resources. It is a completely separate system from Entra directory roles, with different role definitions, a different assignment store, and a different scope model. A GA is not, by default, an Owner of your subscriptions.
The bridge between Plane 1 and Plane 2 is (in our opinion) one of the strangest and most under-watched moves in all of Azure: elevateAccess.
A GA can call Microsoft.Authorization/elevateAccess/action and instantly grant themselves User Access Administrator at the root scope (/) over every subscription in the tenant, present and future. It's a single API call. It exists for legitimate "I locked myself out of my own subscriptions" recovery. And it is the cleanest possible way to cross from "I own the directory" to "I own every resource."
Here's the visibility kicker: elevateAccess does not show up where you'd expect. It is not a normal role assignment, so it doesn't appear in the subscription's RBAC blade as an Owner you can spot and remove. And it doesn't land in the exported AzureActivity (Azure resource) log where the rest of the resource-plane operations live. elevateAccess shows up in the directory Activity logs in the portal, but these logs are not exported via the Azure Activity log API, they're recorded in the Entra audit log, under a role-management category.
That is the fragmentation in miniature: A move that bridges two permission systems is visible in neither's "normal" view.
Plane 3: Key Vault
Users may assume that once they’ve audited Entra roles and Azure RBAC, they’ve covered "permissions." They haven't, because some resources carry their own, third permission model that lives on the resource itself.
Key Vault is the canonical offender. A vault can be in one of two access modes: RBAC (permissions via Azure RBAC, Plane 2) or access policies (a permissions list stored on the vault, granting principals get/list/set on secrets, keys, and certs). These are parallel, independent authorization models for the same resource.
In the instance the Sysdig TRT observed, the attacker used vaults/accessPolicies/write to add themselves to the access policy of three vaults and also did roleAssignments/write on the same vaults, covering both models at once. So even within a single resource type, "who can read these secrets?" is a union of two different permission stores that you have to check separately. Miss the access-policy entries, and your "cleanup" leaves a door open that the RBAC blade will never show you.
Plane 4: Bearer keys and SAS
This is the one that breaks "single pane of glass" entirely, because it isn't an identity system at all.
Storage accounts, Event Hubs, Service Bus, Cosmos DB and others support shared keys and Shared Access Signatures (SAS) – long, high-privilege secrets that grant data-plane access to whoever holds them. There is no principal, role assignment, or entry in any IAM view. If you have the key, you are authorized, full stop.
The attacker called listKeys on four storage accounts and on both Event Hub namespaces, walking away with bearer credentials to the data and to the telemetry transport. Now, if a user asks their "who has access to this storage account?" tooling who can read the data, it will confidently show them the RBAC role assignments and say the account is locked down. All the whole, a copy of the account key sits in the attacker's pocket, granting full read/write, and appearing in no access review, no RBAC report, and no identity graph.
Shared keys are permission laundering: they convert an identity-scoped grant (listKeys, which does require a permission and does log) into an identity-less bearer secret that subsequently shows up nowhere. The act of minting the key is visible for one moment in the activity log. Every use of it thereafter is anonymous. If you weren't watching the listKeys call at the instant it happened, the resulting access is invisible forever.
And it's worse than identity-less. It's also, by default, unlogged. Azure records the control-plane grant (the listKeys call lands in the activity log), but the data-plane use of the resulting key – reading the blobs, draining the Event Hub – is logged only if you've explicitly turned on per-resource data-plane diagnostics, which are off by default for storage, Event Hubs, and most resource types. In this incident, the Event Hub namespaces had no diagnostic settings at all, so use of the stolen root keys would have left no trace whatsoever; the storage accounts had no data-plane logging either. So the listKeys call isn't just your best chance to see a bearer key. Out of the box, it's your only chance. Miss that one control-plane event and the access it created is both identity-less and invisible, permanently: a double blind spot.
Plane 5: Graph API application permissions
The fifth permission system is the one you're least likely to think of as "permissions" at all, and it's the one that made Phase 1 possible in the first place.
The leaked service principal carried a Microsoft Graph application permission: RoleManagement.ReadWrite.Directory, blandly described in the portal as "Read and write all directory RBAC settings." That single application permission is a tenant-takeover-equivalent. It lets the application assign any directory role to anyone, including granting itself GA. The 03:29 GA self-grant back in Plane 1 wasn't a privilege the attacker stumbled into; it was the direct, intended capability of a Graph permission an administrator had consented to long ago. Graph API permissions are their own surface – separate scope model, separate consent system, and separate place in the portal – and they appear in none of the other four planes above.
The cruelty is that you can't see this from the place you'd naturally look. Walk it the way a defender would, drilling into this one app:

The application's Roles and administrators pane looks like the place to answer "what can this app do?" but it is not a reliable source of truth. It lists only the app-management roles assignable here (Cloud Application Administrator), and a footnote explains that directory-level roles "can only be assigned at the directory level." Read that carefully: If this app holds GA or User Access Administrator, this pane will not show it. The most powerful roles are simply invisible from the per-app view. You'd have to already know to check directory-role membership from the other direction.
And the capability that actually enabled the takeover isn't a role at all. It's one blade over:

This is the same app's API permissions. There it is: RoleManagement.ReadWrite.Directory, admin-consented, in a different blade with a different model and a different mental category. A defender auditing this app's roles sees nothing alarming. The takeover key was one tab over the entire time.
So even when you narrow all the way down to a single identity and ask the simplest possible question – "what can this one app do?" – the answer is still smeared across panes that don't reference each other, and the most dangerous capability lives in the one users are often least likely to open. The fragmentation isn't just tenant-wide across the five planes; it's reproduced inside the detail page of a single application.
And then there's the NHI sprawl
Let’s step back to that throwaway line from Phase 1: The attacker planted secrets on 26 application registrations.
This is persistence, and it's persistence designed around exactly the fragmentation we've been describing. Each application registration is an independent non-human identity with its own credential list and its own (often forgotten) permission footprint. Removing the GA grant does nothing to them, and neither does rotating the original leaked secret. Each is a separate, valid way back in.
And there is no single Azure view that lists "every credential on every app registration in my tenant." To find all 26, you have to enumerate app registrations and inspect their passwordCredentials one by one – or you reconstruct it from the audit log if you happened to be capturing it. The blast radius of the persistence is itself only knowable by brute-force enumeration across a plane (app/SP credentials) that most identity dashboards treat as an afterthought.
According to the Sysdig 2026 Cloud-Native Security and Usage Report, NHIs account for approximately 97% of managed identities in cloud environments, they hold the longest-lived secrets, and they are the worst-instrumented corner of the whole permission estate. This incident started with one leaked NHI and ended with 26 backdoored ones, and the tooling to see them clearly mostly doesn't exist out of the box.
This is a visibility problem, not just an attacker problem
The attacker didn't do anything clever to hide. Every action was a logged, legitimate API call. The reason the full picture was hard to assemble is structural. To reconstruct "who and what now has access," we had to query five different surfaces that don't talk to each other:
- Entra directory roles → Entra audit log (
AuditLogs) - Azure RBAC + key harvest → Azure activity log (
AzureActivity) - Key Vault data-plane access → resource diagnostic logs (
AzureDiagnostics) - NHI credential persistence → enumerate app registrations (or reconstruct from audit)
- Graph API application permissions → each app's API-permissions config (or the
Add app role assignment/ consent events back inAuditLogs)
…and elevateAccess, the single most consequential event, sits in the Entra table despite being a resource-plane grant, just to keep you honest.
There is no native Azure screen that answers either of the two questions defenders often have:
- Show me everything this principal can reach across directory roles, RBAC at every scope, resource-local policies, and any bearer keys it has minted.
- Show me everything that can reach this resource, including the access-policy entries, the RBAC assignments, and the identity-less keys floating around for it.
These questions span all five planes, and the planes don't share a join key. A service principal's object ID, an RBAC assignment's scope path, a Key Vault access-policy entry, and a storage account key have almost nothing in common to correlate on. The fragmentation isn't just in Azure's UI. It propagates into how most of us monitor Azure, because we build detections shaped like the planes we're given.
The one thing that saved the investigation was unglamorous: We had captured the control-plane logs to immutable, off-tenant storage the attacker couldn't reach. They held the Event Hub root keys for the telemetry pipeline and could have blinded or polluted the live workspace, but the durable copy was outside their blast radius, so the full chain was still fully reconstructable. Evidence durability has to be independent of the plane the attacker controls. That's lesson zero, and it's the only reason there's a blog post to write.
What to do about it
You can't fix Azure's permission model, but you can stop being surprised by it. Concretely:
1. Inventory across all five planes, as one estate. Any access review that covers only Entra roles and Azure RBAC is missing resource-local policies (Key Vault and others), bearer keys, and Graph API application permissions entirely. Treat all five as parts of one permission graph, because to an attacker they are.
2. Treat NHIs as first-class identities, including their Graph permissions. Service principals and their credentials need the same scrutiny as user accounts: Inventory every app registration's secrets/certs, alert on credential additions, and age-out long-lived secrets. And audit the Graph API application permissions they hold; an admin-consented RoleManagement.ReadWrite.Directory (or AppRoleAssignment.ReadWrite.All, Application.ReadWrite.All, and friends) is tenant-takeover-equivalent and belongs on a short, deliberately-maintained allow-list. The admin-consent grant of one of those is itself a high-signal event to alert on. A leaked NHI holding exactly that permission was the entire front door here.
3. Watch the cross-plane bridges, because that's where attackers cross and visibility is thinnest. The high-signal, low-noise events to alert on:
Microsoft.Authorization/elevateAccess/action– near-zero legitimate use; it is the Entra→resource bridge. Alert on every occurrence.- Privileged directory-role grants (Global Administrator, Privileged Role Administrator, User Access Administrator), especially when the grantee is a service principal.
vaults/accessPolicies/writeand Key VaultroleAssignments/write, the resource-local self-grant.listKeys/ SAS issuance on storage, Event Hub, etc. the moment an identity grant becomes an identity-less key. This is your only chance to see that key; after this, it's invisible.
4. Starve the identity-less plane. Where you can, disable shared-key access on storage accounts and prefer Entra-based (RBAC) data-plane auth, so access stays attached to an identity you can actually review and revoke. Every shared key you eliminate is a row that comes back into your single pane of glass.
5. Turn the logging on before exposure. It isn't on by default. Azure's defaults will quietly leave you blind exactly where this attacker operated. The control plane is recorded but not necessarily routed anywhere you can query or alert on; the data plane and the Graph plane are mostly off. Treat the following as go-live prerequisites, not nice-to-haves:
- Route the Activity Log and Entra audit + sign-in logs to a workspace. They're retained in-portal by default but aren't queryable/alertable until you create the diagnostic settings (sign-in logs require Entra P1). This is where
elevateAccess, the privileged role grants, and thelistKeyscalls live. - Enable
MicrosoftGraphActivityLogs(tenant diagnostic setting, P1). It is off by default and is the only place you'll see Graph-plane recon and app-registration tampering. In this incident, it started flowing mid-attack and missed the escalation entirely. - Enable data-plane diagnostics on the resources that issue bearer keys: Storage, Event Hubs, Key Vault. These, too, are all off by default. Without them, every use of a stolen key is invisible (see Plane 4). The only data-plane signal we did have here – Key Vault secret reads – existed solely because that logging had been explicitly mandated via policy; out of the box, even that would have been dark.
6. Unify the telemetry, then correlate by identity, not by plane. AuditLogs, AzureActivity, resource diagnostic logs, and Microsoft Graph activity logs each hold one slice. The value isn't in any one of them – it's in stitching them into a single identity-centric timeline so that "SP grants itself GA → elevates to root UAA → assigns Owner → harvests keys" reads as one escalation story instead of five unrelated log lines in five tables. This is exactly the gap that cloud detection and response (CDR) and cloud infrastructure entitlement management (CIEM) tooling exists to close: a permission graph and an event stream that span the planes Azure keeps apart.
Sysdig detections that trigger across the planes
The fragmentation that makes this hard to investigate is the same thing that makes it hard to detect. Unless the detection layer reads all the planes as one stream. Sysdig's agentless cloud detection ingests both the Entra audit log and the Azure Activity log and evaluates them with Falco rules against a single, identity-keyed timeline. That property is the whole game: the elevateAccess event (which lives in the Entra plane) and the listKeys harvest (which lives in the Activity plane) land in the same timeline for the same principal, so the escalation reads as one story instead of two unrelated log lines in two systems.
Walking through the exact kill chain from this incident, here are the Sysdig detection rules that trigger for each attacker move:
The highest-value entry is the elevateAccess rule (†). That move was the hinge of the whole takeover – the single call that crossed from "owns the directory" to "owns every resource" – and it's exactly the one that hides in the other plane's logs (Entra audit, not Activity). A detection that watches the Entra plane for it while also watching the Activity plane for the key harvest collapses a two-plane attack back into one alertable chain. That's the cross-plane correlation the native consoles can't give you.
The four detections marked with † are ones we contributed to Sysdig's open source Falco rules in direct response to the gaps this incident exposed: the cross-plane bridge (elevateAccess), the resource-local self-grant (Key Vault access policy), and the two bearer-key mints that had no dedicated rule (Event Hub root keys and Function App host keys). They close the blind spots that map precisely to Planes 2, 3, and 4 above, the harvest of the telemetry pipeline's own keys (Event Hub) being the one most worth alerting on loudly, since it's how an attacker goes after your visibility next.
Takeaways
- "Who has access?" is not a one-query question in Azure. Permission lives in at least five disjointed systems – Entra directory roles, Azure RBAC, resource-local access policies, identity-less bearer keys, and Graph API application permissions – that don't share identifiers and log to (or surface in) different places. The Graph permission is the one that enabled this whole takeover, and it's invisible from the app's own roles pane.
- The most dangerous moves are the cross-plane ones.
elevateAccessbridges directory and resource control with a single call and is logged in the opposite plane from the one it affects. Hunt for the bridges. - Bearer keys are a permanent blind spot. A stolen storage or Event Hub key grants access that appears in no IAM view ever again. The
listKeyscall is the only moment you can see it. Prefer identity-based data-plane auth and disable shared keys. - NHIs are the soft underbelly. One leaked service principal opened the door. 26 backdoored ones held it open, and you can only find them by enumerating credentials plane by plane.
- Build visibility that crosses the seams. If your monitoring is shaped like Azure's permission planes, an attacker who crosses between them crosses out of your view. Correlate by identity, across all five planes, or you don't have a single pane of glass; you have five panes and a lot of hope.
- Detect at the bridges. Each pivotal move is high-signal and low-noise on its own:
elevateAccess, privileged role grants, Key Vault access-policy self-grants,listKeys. The leverage comes from evaluating the Entra and Activity planes as one identity timeline, so a cross-plane chain alerts as a chain. Sysdig's secure cloud rules cover every step of the chain above.
