What Makes an Entra Service Principal Admin Role Over-Privileged
An Entra service principal admin role over-privileged for what its automation actually does is one of the least reviewed identities in Microsoft Entra ID — and one of the most dangerous, because unlike a human admin it never has to pass MFA, rarely has a lifecycle owner, and often outlives the project that created it. A service principal is the tenant-local identity of an application or script: the thing Terraform, a CI/CD pipeline, or a custom automation authenticates as when it calls Microsoft Graph. Give one of them a directory admin role such as Global Administrator, Privileged Role Administrator, or Application Administrator, and it can do everything a human admin in that role can do — create users, reset credentials, consent to new permissions, modify Conditional Access.
It's worth separating this from a related but different exposure. A directory role (what this article covers) grants tenant-wide administrative capability, assigned through roleManagement/directory/roleAssignments. An app role assignment — the delegated or application Graph API permissions like Directory.ReadWrite.All granted via servicePrincipals/{id}/appRoleAssignedTo (Grant an appRoleAssignment for a service principal) — is a separate mechanism scoped to one resource API, and is the subject covered in over-privileged Graph API permissions on app registrations. A service principal can hold neither, either, or both; this article is about the directory-role side of that equation.
The problem with the directory-role side is exposure, not the role itself. An over-privileged admin-role service principal sits there 24/7, authenticates non-interactively with a client secret or certificate, and — per Microsoft's own guidance on workload identity risk — workload identities "can't perform multifactor authentication," "often have no formal lifecycle process," and "need to store their credentials or secrets somewhere" (Securing workload identities with Microsoft Entra ID Protection). Mandatory Microsoft Entra MFA enforcement explicitly does not reach these identities either: Microsoft confirms workload identities "aren't impacted by either phase" of the mandatory MFA rollout (Plan for mandatory Microsoft Entra multifactor authentication). A leaked secret for an admin-role service principal is a leaked, MFA-proof admin session.
This gap is rarely a single misconfiguration. It's usually a cluster of related blind spots that compound: the admin role is broader than the automation needs, nobody owns the service principal, disabling it doesn't actually strip the permission, and — increasingly — the identity holding the role isn't even native to your tenant.
How Over-Privileged Service Principals Happen
Standing, Not Eligible
Privileged Identity Management (PIM) lets a human admin's role assignment be eligible — inactive until they activate it, time-bound, and logged. Service principals can't use that model: Microsoft Entra roles, Azure roles, and PIM for Groups can only grant a service principal an active assignment, because eligible assignments require an activation step (approval, an MFA challenge) that a non-interactive identity cannot perform (Eligible and time-bound role assignments in Azure RBAC). In practice, every admin-role service principal in your tenant is a standing assignment by design — there's no PIM safety net making it time-limited unless someone separately set an assignment expiration.
No Owner
servicePrincipal.owners is meant to answer "who is accountable for this." Microsoft Graph exposes it as a collection you populate with a POST /servicePrincipals/{id}/owners $ref call (servicePrincipal: Add owner) — but nothing forces it to be populated at creation. A service principal that turns up in a role-assignment sweep with an empty owners collection has an admin role that nobody gets notified about, nobody reviews at offboarding, and nobody can explain on an incident call.
Disabled but Still Permissioned
accountEnabled on a service principal is a boolean that blocks sign-in when set to false — "no users are able to sign in to this app, even if they're assigned to it" (servicePrincipal resource type). It does not touch role or app role assignments — removing those is a separate operation with its own cmdlet, Remove-MgServicePrincipalAppRoleAssignment (Microsoft Learn). A team that "decommissions" an integration by flipping accountEnabled to false and stopping there leaves the admin role assignment fully intact — re-enabling the account, or an attacker able to flip that same flag back, restores admin access instantly.
From an External Organization
Not every service principal in your tenant was created by you. Multi-tenant app registrations create a service principal locally while the underlying application object stays in the vendor's or partner's home tenant — recorded as appOwnerOrganizationId, which Microsoft Entra also writes into the audit event's additional details when the service principal is provisioned (Understand why a service principal was created in your tenant). When appOwnerOrganizationId doesn't match your own tenant ID and that service principal also holds an admin role, you've delegated standing admin access to an identity a third party controls — the application equivalent of the cross-tenant guest exposure most teams already track for user accounts, but almost never for applications.
Detection
Pull every directory role assignment, then cross-reference which principals are service principals versus users or groups — Get-MgRoleManagementDirectoryRoleAssignment doesn't filter by principal type on its own, so resolve that with a lookup against Get-MgServicePrincipal (List Microsoft Entra role assignments):
Connect-MgGraph -Scopes "RoleManagement.Read.Directory","Application.Read.All"
$roles = Get-MgRoleManagementDirectoryRoleDefinition
$spAssignments = foreach ($role in $roles) {
Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'" |
ForEach-Object {
$sp = Get-MgServicePrincipal -ServicePrincipalId $_.PrincipalId -ErrorAction SilentlyContinue
if ($sp) {
[pscustomobject]@{
ServicePrincipal = $sp.DisplayName
RoleAssigned = $role.DisplayName
AccountEnabled = $sp.AccountEnabled
OwnerCount = (Get-MgServicePrincipalOwner -ServicePrincipalId $sp.Id).Count
AppOwnerOrgId = $sp.AppOwnerOrganizationId
}
}
}
}
$spAssignments | Format-Table -AutoSize
The same query works directly against Microsoft Graph without the PowerShell module — useful for a script running outside a workstation with the Graph SDK installed:
GET https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?$filter=roleDefinitionId eq '{role-template-id}'
Each result returns a principalId; resolve it against GET /servicePrincipals/{id} to confirm the principal type and pull accountEnabled, appOwnerOrganizationId, and — via GET /servicePrincipals/{id}/owners — the owners collection in the same call (List Microsoft Entra role assignments).
| Detector | What to check | Why it matters |
|---|---|---|
PA_SERVICE_PRINCIPAL_ADMIN / SP_HIGH_PRIVILEGE | Any service principal above with a Global Administrator, Privileged Role Administrator, or other high-tier role | Standing, non-MFA-capable admin access |
SP_NO_OWNER | OwnerCount -eq 0 on a role-holding service principal | No accountable human to review or revoke it |
SP_DISABLED_WITH_PERMISSIONS | AccountEnabled -eq $false but the role assignment still resolves | Permission survived what looked like decommissioning |
SP_EXTERNAL_ORGANIZATION | AppOwnerOrganizationId present and not equal to your own tenant ID | Admin role held by an identity a different organization controls |
For the assignment event itself, filter Microsoft Entra audit logs to the Core Directory service and the Role Management category, then look for "Add member to role" activity entries, scoping TargetResources to type ServicePrincipal to isolate non-human grants from user ones (Easily Manage Privileged Role Assignments in Microsoft Entra ID Using Audit Logs). Export these logs before they age out — Entra audit log retention is short by default and easy to lose without diagnostic settings configured.
⚠️ Warning: AccountEnabled -eq $false on a service principal is not remediation. It stops sign-in, not the role assignment — treat a disabled admin-role service principal as still-privileged until you confirm the assignment itself is removed.
Remediation
💡 Quick Win: Run the detection script above once and triage every SP_NO_OWNER and SP_EXTERNAL_ORGANIZATION result first — those are the two categories nobody else in the tenant is likely already watching.
- Right-size the role. Replace broad admin roles (Global Administrator, Privileged Role Administrator) with the narrowest built-in or custom role that covers the automation's actual Graph calls — most integrations need Application Administrator or a resource-scoped role, not Global Administrator. A service principal that only provisions users doesn't need the same role as one that manages Conditional Access.
- Assign an owner.
POST /servicePrincipals/{id}/ownerswith a$refto an accountable engineer so the assignment shows up in access reviews instead of surfacing only during an incident. - Remove permissions before disabling. Run
Remove-MgServicePrincipalAppRoleAssignment(or the equivalent directory-role unassignment) as part of the decommission runbook — don't rely onaccountEnabled: $falsealone, and don't consider a "disabled" integration closed out until the role assignment itself is gone. - Set an expiration where you can. Because service principals can only hold active assignments, use the assignment's end-date option at grant time so an admin role doesn't silently become permanent by default.
- Gate workload identities with Conditional Access. Conditional Access for workload identities can block sign-in for a single-tenant service principal by location or Identity Protection risk state — it won't force MFA (service principals can't do that), but it closes part of the gap MFA can't reach.
- Review external-organization service principals on their own merits. For each
SP_EXTERNAL_ORGANIZATIONhit holding an admin role, confirm the vendor relationship is current and the role is still justified — the same scrutiny you'd apply to a guest account with standing access, applied to an application instead of a person.
This complements the review most teams already run for human privileged access — see Azure Privileged Access: Too Many Global Admins and How to Audit Microsoft Entra ID Security — and it's distinct from, but related to, over-privileged Graph API permissions on the app registration itself, which is a separate exposure from the service principal's directory role.
How EtcSec Detects This
EtcSec's Azure audit checks PA_SERVICE_PRINCIPAL_ADMIN and SP_HIGH_PRIVILEGE against every directory role assignment held by a service principal, and separately flags SP_NO_OWNER, SP_DISABLED_WITH_PERMISSIONS, and SP_EXTERNAL_ORGANIZATION so the ownership, lifecycle, and cross-tenant gaps above surface as findings instead of requiring a manual PowerShell sweep every quarter across every role definition in the tenant.
ℹ️ Note: EtcSec automatically checks for this vulnerability during every Azure audit. Run a free audit to verify your environment.
Explore the identity security pages that support this topic
