Most Azure breaches I've reviewed didn't start with a zero-day. They started with a service principal that held Owner on a subscription, and a developer who didn't know it was there.
The four paths
- Service principal with Owner / Contributor at subscription scope.
- Managed identity attached to a VM that hosts a CI/CD agent.
- Custom role with Microsoft.Authorization/roleAssignments/write.
- Eligible PIM role with no MFA-on-activation requirement.
az role assignment list \
--all \
--query "[?roleDefinitionName=='Owner' && principalType=='ServicePrincipal'].{principal:principalName, scope:scope}" \
-o tableRisk
A subscription-scoped Owner SP whose secret lives in a public-repo .env is the single most common path I see. Always crosscheck the SP names against your secret-scanner findings.
What 'good' looks like
- No service principal holds Owner above resource-group scope.
- All Microsoft.Authorization/* permissions live in PIM-eligible roles only.
- PIM activation requires MFA + justification + max 8h.
- All role changes are alerted via Defender for Cloud and reviewed weekly.
Best practice
Run this audit quarterly with the same KQL queries each time. Trend the count of subscription-Owner SPs over time โ if the line isn't trending down, your IAM hygiene isn't working.
Audit deliverables
Produce three artifacts: a CSV of every privileged identity with last-used timestamp, a reduced list of identities that have not been used in 90 days, and a sign-off matrix where each business owner confirms the identities they still need.
Rudy Prasetiya
IT GRC, cybersecurity & audit practitioner. Writes about controls that actually hold.
