> az@cloudshell:~$ az role assignment list --assignee $(az ad signed-in-user show --query id -o tsv) | jq '.[] | .roleDefinitionName'<span class="cursor-blink">_</span>_
There is a misconception — common among organisations migrating to the cloud — that security in Azure, AWS, or GCP is primarily about patching virtual machines, configuring network security groups, and encrypting storage. These are important. But they are not where the most consequential cloud compromises originate.
The most consequential cloud compromises originate in IAM — Identity and Access Management. They do not involve exploiting a vulnerability. They do not require malware. They do not trigger an EDR alert. They involve an attacker who has legitimate credentials taking a series of actions that are individually authorised and collectively devastating.
On this engagement, we escalated from a developer's account with read access to a single resource group to Global Administrator of the entire Azure tenant. Every action we took was permitted by the permissions assigned to the accounts we used. No security control flagged any step as malicious, because no step was — in isolation — malicious. The misconfiguration was not in what any single permission allowed. It was in how the permissions chained together.
The client was a mid-sized SaaS company — approximately three hundred employees — with a cloud-native architecture running entirely in Microsoft Azure. Their infrastructure comprised several hundred resources across twelve subscriptions, organised by environment (production, staging, development) and function (platform, data, applications). They used Entra ID (formerly Azure AD) for identity management, with a mix of cloud-only and hybrid-joined identities.
We had been engaged to conduct a cloud penetration test focused on Azure IAM and privilege escalation. The starting position was a compromised developer account — simulating a scenario where an attacker had obtained valid credentials through phishing, credential stuffing, or a compromised workstation. We were given a standard developer account with the permissions that a typical member of the platform engineering team would hold.
The client was confident in their Azure configuration. They had engaged a cloud consultancy to design their landing zone architecture. They used Azure Policy for guardrails. They had implemented Privileged Identity Management (PIM) for administrative roles. They believed their IAM model followed least privilege.
It did not.
Our starting account — e.ward@[client].com — was a member of the Platform Engineering Entra ID security group. We began by enumerating our permissions: what could this account see, and what could it do?
The account had Reader access to three subscriptions — the two development subscriptions and the staging platform subscription. It also had a custom role called 'Platform Engineer' scoped to a single resource group: platform-dev in the Dev-Platform subscription.
Reader access is benign in isolation — it permits viewing resources but not modifying them. The interesting question was: what did the custom 'Platform Engineer' role permit?
Azure IAM uses Role-Based Access Control (RBAC). Permissions are defined in role definitions — documents that specify which actions an identity can perform on which resource types. Azure provides dozens of built-in roles (Reader, Contributor, Owner, etc.), but organisations can also create custom roles tailored to their specific needs.
Custom roles are where misconfigurations concentrate. Built-in roles are well-documented, widely understood, and tested by Microsoft. Custom roles are created by the organisation's own engineers, often under time pressure, and rarely reviewed after creation.
The custom role definition contained several permissions that were individually reasonable for a platform engineer in a development environment — the ability to manage compute, networking, and storage resources. However, three permissions were critically overpermissive.
| Permission | What It Allows | Risk |
|---|---|---|
| Microsoft.Authorization/ roleAssignments/* |
Create, read, update, and delete role assignments within the scope | Critical — allows the user to grant themselves or any other identity any role, up to and including Owner, on any resource within the scope |
| Microsoft.ManagedIdentity/* | Create and manage user-assigned managed identities | High — managed identities can be assigned roles and used to authenticate to Azure services without passwords |
| Microsoft.KeyVault/ vaults/secrets/* |
Full control over Key Vault secrets including read, write, and delete | High — Key Vaults frequently contain service principal credentials, connection strings, and API keys |
The Microsoft.Authorization/roleAssignments/* permission was the critical finding. This single permission grants the ability to assign any Azure RBAC role — including Owner — to any identity within the scope. It is the most dangerous permission in Azure RBAC outside of the Owner role itself, because it allows the holder to escalate their own privileges by assigning themselves additional roles.
The custom 'Platform Engineer' role included Microsoft.Authorization/roleAssignments/* which allows the holder to create arbitrary role assignments within scope. This permits privilege escalation by self-assigning Owner or Contributor roles on the resource group, subscription, or any resource within scope.
The first escalation was immediate. We used the roleAssignments permission to assign ourselves the Owner role on the Dev-Platform subscription.
A single command. No approval workflow. No Privileged Identity Management activation. No multi-factor authentication challenge. No alert in the SIEM. We were now Owner of the Dev-Platform subscription — with full control over every resource within it, including the ability to create further role assignments, modify resources, and access all data.
The client had implemented PIM for built-in administrative roles at the Entra ID level — Global Administrator, User Administrator, and similar directory roles required PIM activation. But PIM was not configured for Azure RBAC roles at the subscription level. The custom role's ability to create role assignments bypassed PIM entirely because the escalation occurred within Azure RBAC, not within Entra ID.
With Owner access to the Dev-Platform subscription, we enumerated its Key Vaults. Azure Key Vault is the recommended service for storing secrets — connection strings, API keys, certificates, and service principal credentials. The platform-dev resource group contained a Key Vault named kv-platform-dev.
The Key Vault contained six secrets, including the client secrets for two service principals — automated identities used by the CI/CD pipeline and infrastructure-as-code tooling. It also contained a SQL administrator password, a production storage account key, an API gateway signing key, and a GitHub personal access token used by the CI/CD pipeline.
Two observations were immediately concerning. First, the Key Vault in the development subscription contained a secret named storage-account-key-prod — a production credential stored in a development environment. Second, the service principal secrets had been created in January 2023 — nearly two years ago — and had not been rotated.
We focused on the two service principals.
Service principals are the non-human identities in Azure — the accounts used by applications, CI/CD pipelines, and automation tools. They authenticate with client secrets or certificates rather than passwords and MFA. They are often granted broad permissions to perform their automated tasks. And they are frequently the most overprivileged identities in any Azure tenant.
We authenticated as the sp-platform-deployer service principal using the client secret retrieved from the Key Vault, and enumerated its role assignments.
The service principal had Contributor access to six subscriptions — both development, both staging, and both production subscriptions. This single service principal, whose secret was stored in a development Key Vault accessible to any platform engineer, had write access to the production environment.
The Contributor role cannot create role assignments (that requires Owner or User Access Administrator), so we could not directly self-escalate further via Azure RBAC from this position. But Contributor can modify resources — and modifying the right resource is just as powerful as having the right role.
Azure has a well-documented escalation path from a Contributor role on a subscription to Global Administrator of the Entra ID tenant. The path involves Azure Automation — a service that allows organisations to run PowerShell and Python scripts on a schedule or in response to events.
The Prod-Platform subscription contained an Automation Account named auto-infra-ops. This Automation Account had a system-assigned managed identity — an identity automatically created by Azure and attached to the resource. The managed identity had been granted roles to perform its automated tasks.
The Automation Account's managed identity was a Global Administrator. This is the highest-privilege role in Entra ID — equivalent to domain administrator in on-premises Active Directory. It grants full control over every identity, every application, every group, and every configuration in the tenant.
Why was an Automation Account a Global Administrator? Investigation revealed that the account ran scripts that managed Entra ID objects — creating service principals, managing group memberships, and configuring application registrations. Rather than granting the specific Graph API permissions required for these tasks, someone had assigned the Global Administrator role — the cloud equivalent of giving root access because it is easier than determining the minimum necessary permissions.
As a Contributor on the subscription, we had the ability to create and execute runbooks within the Automation Account. Runbooks execute in the context of the Automation Account's managed identity. A runbook executed by us would run as Global Administrator.
We created a PowerShell runbook that, when executed, used the managed identity's Global Administrator privileges to add our original user account to the Global Administrator role. The runbook executed. The role was assigned. e.ward — a developer with read access to three subscriptions and a custom role on a single resource group — was now Global Administrator of the entire Azure tenant.
| Step | Action | Weakness Exploited |
|---|---|---|
| 01 | Enumerated custom 'Platform Engineer' role definition | Custom role included roleAssignments/* — self-escalation capability |
| 02 | Self-assigned Owner on Dev-Platform subscription | No PIM for Azure RBAC roles; no approval workflow for role assignments |
| 03 | Retrieved service principal secret from Key Vault | Development Key Vault contained CI/CD service principal credentials |
| 04 | Authenticated as service principal — Contributor on 6 subscriptions | Single service principal with cross-environment access including production |
| 05 | Identified Automation Account with Global Admin managed identity | Managed identity granted Global Administrator — grossly overprivileged |
| 06 | Created and executed runbook as Global Admin; elevated own account | Contributor can create runbooks that execute as the managed identity |
This engagement involved no software vulnerabilities. No CVEs. No buffer overflows. No unpatched services. Every action we took was performed through the Azure CLI and REST API — using commands that are documented, supported, and intended. The issue was not that Azure allowed us to do something it should not have. The issue was that the organisation's IAM configuration permitted us to do things they did not intend.
This is the fundamental challenge of cloud IAM. In a traditional on-premises environment, the network provides a physical constraint — you must be on the right VLAN, in the right building, with the right cable plugged in. In the cloud, there are no physical constraints. Access is determined entirely by permissions — and permissions are determined by configuration. A single overpermissive role, a single misconfigured service principal, a single Global Administrator managed identity, and the entire tenant is compromised.
The immediate priority was removing the roleAssignments/* permission from the custom role. This single permission was the entry point to the entire escalation chain. If this permission had not been present, none of the subsequent steps would have been possible from the developer account. Custom roles should never include Microsoft.Authorization/* actions unless the role is specifically designed for IAM management — and even then, conditions should restrict which roles can be assigned.
The Automation Account's managed identity must be downgraded from Global Administrator to the minimum permissions required for its tasks. Azure provides granular Graph API permissions — Application.ReadWrite.All, Group.ReadWrite.All, and similar scoped permissions — that allow specific operations without granting full tenant administration. The principle of least privilege applies to non-human identities with the same force as it does to human users.
Environment isolation is essential. A service principal should never span development and production. Credentials for production systems should never be stored in development environments. Each environment should have its own service principals, its own Key Vaults, and its own role assignments — with no cross-environment trust. The cost of managing separate identities per environment is negligible compared to the cost of a compromised production environment.
Cloud Infrastructure Entitlement Management (CIEM) tooling provides continuous visibility into the effective permissions of every identity in the tenant — human and non-human. CIEM tools can identify overprivileged service principals, detect escalation paths, and alert on permission changes that introduce risk. In a cloud environment where IAM is the primary security boundary, CIEM is as essential as EDR is for endpoints.
On-premises security is built around network boundaries — firewalls, VLANs, segmentation. Cloud security is built around identity boundaries — roles, permissions, policies. The firewall rules that kept an attacker out of the server room have no equivalent in Azure. The only thing standing between a developer account and Global Administrator is the correctness of the IAM configuration.
This organisation had invested in Azure Policy, PIM, and a professionally designed landing zone. These investments were real and valuable. But a single custom role with one overpermissive action, a single service principal that spanned environments, and a single managed identity with unnecessary privileges created a chain that bypassed every control they had built.
No vulnerability was exploited. No patch was missing. No security tool failed. The configuration was the vulnerability. The permission was the exploit. The identity was the attack surface.
Until next time — stay sharp, stay curious, and read your custom role definitions. Every line of JSON is a security decision.
This article describes a cloud penetration test conducted under formal engagement with full written authorisation from the client. All identifying details have been altered or omitted to preserve client confidentiality. Privilege escalation was demonstrated and immediately reversed — all created role assignments and accounts were removed upon completion. Unauthorised access to computer systems is a criminal offence under the Computer Misuse Act 1990 and equivalent legislation worldwide. Do not attempt to replicate these techniques without proper authorisation.
Hedgehog Security conducts cloud penetration testing that focuses on the attack surface that matters most — identity and access management. We map permission chains, identify escalation paths, audit service principal privileges, and test whether your cloud IAM model enforces the least privilege you intended. The cloud has no perimeter. Permissions are all you have.