← Back to Blog

Service Principal Ownership Is the Attack Path Nobody Governs

12 May 2026 Identity Security 8 min read
SERVICE PRINCIPAL OWNERSHIP — FROM LOW-PRIVILEGE USER TO TENANT TAKEOVER COMPROMISED USER Low-privilege account with SP ownership ADD CREDENTIAL New client secret on the owned SP AUTHENTICATE AS SP App-only context Inherits all SP perms TENANT COMPROMISE Reset GA password, issue TAP Full interactive admin access WHY THIS WORKS Ownership is a takeover primitive. An owner can add credentials, and credentials inherit all permissions. 99% of tenants have ≥1 privileged SP Silverfort research, April 2026 ↗ Most organizations don't audit SP ownership No built-in alert for ownership changes

Two weeks ago, Silverfort published research showing that the Agent ID Administrator role — a new Entra ID role designed to manage AI agent identities — could take over any service principal in a tenant. Not just agent service principals. Any service principal. The attack path was straightforward: an account with the Agent ID Administrator role assigns itself ownership of a high-privilege service principal, adds a client secret, authenticates as that service principal, and inherits every permission the service principal holds. If the target has a directory role like Global Administrator, that's full tenant takeover. (Silverfort: Agent ID Administrator scope overreach)

Microsoft patched the specific scope overreach in April 2026. The Agent ID Administrator role can no longer take ownership of non-agent service principals. But the patch fixed the scoping gap — not the underlying attack primitive. Service principal ownership abuse has been a known technique for years. The Silverfort research just proved, again, that owning a service principal means owning its permissions. Semperis documented the same primitive in their EntraGoat attack scenario research earlier this year — a compromised low-privilege user discovers ownership of a service principal with a privileged directory role, adds a client secret, and escalates to Global Administrator.

The question for your environment isn't whether Microsoft patched this specific flaw. The question is: do you know which service principals in your tenant hold privileged permissions, who owns them, and whether those ownership assignments are still appropriate?

The attack path that governance prevents

The ownership takeover works because of how Entra ID's permission model operates for application identities. A service principal owner can add credentials — client secrets or certificates — to the owned service principal. Once a credential exists, anyone who has it can authenticate as that service principal in an app-only context. The app-only context inherits every permission the service principal holds: Graph API permissions, directory role assignments, consent grants.

This isn't a vulnerability in the traditional sense. It's how the system was designed. Ownership is a management function — the owner maintains the application. The security problem is that most organizations treat ownership as a directory attribute that was set at creation time and never reviewed. The developer who registered the application is still listed as the owner even though they left the organization 18 months ago. Nobody checked whether the application's permissions are still needed. Nobody verified that the ownership assignment is still appropriate.

The Silverfort disclosure matters because it highlights how the proliferation of new Entra ID roles creates new paths to the same primitive. Today it was Agent ID Administrator. Tomorrow it could be another role with scoping that doesn't match its documented intent. The defense isn't patching each individual flaw — it's governing the primitive: service principal ownership.

Three things to audit this week

You don't need a governance framework to start. You need three queries and 30 minutes.

1. Find your privileged service principals

These are the service principals that matter most — the ones where ownership abuse leads to privilege escalation.

Connect-MgGraph -Scopes "Application.Read.All","Directory.Read.All","RoleManagement.Read.Directory"

$roleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -All
$spRoles = $roleAssignments | Where-Object { $_.PrincipalId -ne $null }

$privilegedSPs = @()
foreach ($assignment in $spRoles) {
    $sp = Get-MgServicePrincipal -ServicePrincipalId $assignment.PrincipalId -ErrorAction SilentlyContinue
    if ($sp -and $sp.ServicePrincipalType -eq "Application") {
        $roleDef = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $assignment.RoleDefinitionId
        $privilegedSPs += [PSCustomObject]@{
            DisplayName = $sp.DisplayName
            AppId       = $sp.AppId
            Role        = $roleDef.DisplayName
            OwnerOrg    = $sp.AppOwnerOrganizationId
        }
    }
}

$privilegedSPs | Format-Table DisplayName, Role, OwnerOrg -AutoSize

If any service principal holds Global Administrator, Privileged Role Administrator, Application Administrator, or Cloud Application Administrator, it's a Tier 0 target. The attacker who owns it owns your tenant.

2. Check ownership on those privileged service principals

For every service principal in the output above, check who currently owns it.

foreach ($sp in $privilegedSPs) {
    $spObj = Get-MgServicePrincipal -Filter "appId eq '$($sp.AppId)'"
    $owners = Get-MgServicePrincipalOwner -ServicePrincipalId $spObj.Id -All

    if ($owners.Count -eq 0) {
        Write-Host "$($sp.DisplayName): NO OWNER — orphaned privileged SP" -ForegroundColor Red
    } else {
        foreach ($owner in $owners) {
            $user = Get-MgUser -UserId $owner.Id -ErrorAction SilentlyContinue
            if ($user) {
                Write-Host "$($sp.DisplayName): Owner = $($user.DisplayName) ($($user.UserPrincipalName))"
            } else {
                Write-Host "$($sp.DisplayName): Owner = $($owner.Id) (not a user — investigate)"
            }
        }
    }
}

What you're looking for:

  • No owner. An orphaned privileged service principal is the highest-risk finding. Nobody is accountable for the credentials, and nobody attests that the permissions are still needed. If an attacker gains ownership through any path — a role scoping flaw, a compromised admin account, a misconfigured app management permission — there's nobody who would notice the ownership change.
  • Owner who left the organization. The user account exists but is disabled. The ownership assignment persists after the account is disabled. An attacker who reactivates or compromises the disabled account inherits the ownership.
  • Owner with no relationship to the application. The owner was assigned at creation time and has no current operational role with the application. They wouldn't notice if someone added a credential because they don't monitor the application.

3. Check for credential additions in the last 90 days

Credential additions to service principals are the operational step in the ownership abuse chain. Every legitimate credential addition should correlate to a documented rotation event.

$auditLogs = Get-MgAuditLogDirectoryAudit -Filter "activityDisplayName eq 'Add service principal credentials' and activityDateTime ge $((Get-Date).AddDays(-90).ToString('yyyy-MM-ddTHH:mm:ssZ'))" -All

foreach ($log in $auditLogs) {
    $target = $log.TargetResources | Where-Object { $_.Type -eq "ServicePrincipal" }
    Write-Host "$(($log.ActivityDateTime).ToString('yyyy-MM-dd')) | $($log.InitiatedBy.User.UserPrincipalName ?? 'App context') | Target: $($target.DisplayName)"
}

Every entry should map to a known rotation event. A credential addition you can't explain is either shadow IT or an attacker establishing persistence.

Where to check this in the portal

Before you run any script, you can see the ownership and permission state of any service principal directly in the Entra admin center.

Entra Admin Center

IdentityApplicationsEnterprise applications → select an application → Owners
Shows current owners. If this list is empty, the service principal is orphaned — nobody is accountable for its credentials or permissions. Check Roles and administrators on the same blade to see whether the SP holds directory roles.

Entra Admin Center

IdentityApplicationsApp registrationsAll applications → select an application → Certificates & secrets
Shows all active credentials — client secrets and certificates — with creation date, expiry, and description. If you see credentials you can't account for, that's either uncontrolled shadow IT or an attacker's persistence mechanism.

The portal gives you the state of one application at a time. The PowerShell scripts above audit the entire population. Use the portal to investigate specific findings from the script output.

Detecting the attack path with KQL

Entra ID doesn't alert on service principal ownership changes or credential additions by default. The audit log records both events, but nobody's watching unless you've built the detection. If you have Sentinel or any SIEM ingesting Entra audit logs, these two rules close the gap.

Detect ownership changes on service principals

This rule fires when anyone adds an owner to a service principal. In most tenants, ownership changes are rare — a few per quarter during legitimate app management. Any ownership addition outside a known change window is worth investigating.

// Detect: Service principal ownership changes
// Data source: AuditLogs (Entra ID)
// MITRE ATT&CK: T1098.001 — Account Manipulation: Additional Cloud Credentials
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName in ("Add owner to service principal", "Add owner to application")
| extend ActorUPN = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend TargetSP = tostring(TargetResources[0].displayName)
| extend TargetId = tostring(TargetResources[0].id)
| extend NewOwner = tostring(TargetResources[0].modifiedProperties[0].newValue)
| project TimeGenerated, ActorUPN, ActorApp, OperationName, TargetSP, TargetId, NewOwner
| sort by TimeGenerated desc

Tuning: baseline your tenant's normal ownership change rate over 30 days. Exclude service accounts that manage app registrations as part of a CI/CD pipeline. Any ownership addition by a user account to a service principal with directory roles is a Tier 1 alert.

Detect credential additions to service principals

This rule fires when a new client secret or certificate is added to any service principal. Legitimate credential additions happen during scheduled rotation. Anything outside rotation is suspicious.

// Detect: New credentials added to service principals
// Data source: AuditLogs (Entra ID)
// MITRE ATT&CK: T1098.001 — Account Manipulation: Additional Cloud Credentials
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName in ("Add service principal credentials", "Update application – Certificates and secrets management")
| extend ActorUPN = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend TargetSP = tostring(TargetResources[0].displayName)
| extend TargetId = tostring(TargetResources[0].id)
| project TimeGenerated, ActorUPN, ActorApp, OperationName, TargetSP, TargetId
| sort by TimeGenerated desc

Tuning: join against your privileged service principal list. A credential addition to any SP with directory roles should be a high-severity alert regardless of who performed it. Credential additions by accounts that aren't listed as owners of the target application are always suspicious.

Why this matters more than the patch

The detection gap exists because Microsoft built the audit logging but left the detection logic to the customer. Defender for Cloud Apps can flag anomalous service principal behavior if you have the license and enable the policy. Sentinel can detect ownership changes and credential additions — but only if you deploy the rules above. Out of the box, the attack path is unmonitored.

The Silverfort disclosure fixed one scoping gap. The KQL rules above detect the primitive regardless of which role or path the attacker uses to reach it. That's the difference between patching one vulnerability and governing the attack surface.

The governance layer that makes this sustainable

The three queries above are a point-in-time assessment. They tell you the current state. What they don't do is prevent the next orphaned service principal from accumulating privileged permissions, or the next developer departure from creating an unmonitored ownership gap.

Sustainable governance requires recurring cadences: a quarterly ownership audit that verifies every privileged service principal has an active, appropriate owner. A credential health check that flags secrets older than your maximum lifetime policy. A permission review that validates whether each service principal's permissions are still justified by its current business function.

The organizations that get compromised through service principal abuse aren't the ones that failed to patch a specific vulnerability. They're the ones that never governed the primitive — ownership, credentials, permissions, lifecycle — in the first place.

The script to take with you

This consolidated script runs all three checks in sequence and produces a summary you can hand to your security lead.

# Service Principal Governance Quick Audit
# Run: Connect-MgGraph -Scopes "Application.Read.All","Directory.Read.All",
#      "RoleManagement.Read.Directory","AuditLog.Read.All"

Write-Host "`n=== PRIVILEGED SERVICE PRINCIPALS ===" -ForegroundColor Cyan
$roleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -All
$privilegedSPs = @()

foreach ($ra in $roleAssignments) {
    $sp = Get-MgServicePrincipal -ServicePrincipalId $ra.PrincipalId -ErrorAction SilentlyContinue
    if ($sp -and $sp.ServicePrincipalType -eq "Application") {
        $role = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $ra.RoleDefinitionId
        $privilegedSPs += [PSCustomObject]@{
            DisplayName = $sp.DisplayName
            AppId       = $sp.AppId
            SPId        = $sp.Id
            Role        = $role.DisplayName
        }
    }
}

$privilegedSPs | Format-Table DisplayName, Role -AutoSize
Write-Host "Total privileged service principals: $($privilegedSPs.Count)"

Write-Host "`n=== OWNERSHIP AUDIT ===" -ForegroundColor Cyan
$orphanCount = 0
foreach ($psp in $privilegedSPs) {
    $owners = Get-MgServicePrincipalOwner -ServicePrincipalId $psp.SPId -All
    if ($owners.Count -eq 0) {
        Write-Host "[CRITICAL] $($psp.DisplayName) ($($psp.Role)) — NO OWNER" -ForegroundColor Red
        $orphanCount++
    } else {
        foreach ($o in $owners) {
            $user = Get-MgUser -UserId $o.Id -ErrorAction SilentlyContinue
            $status = if ($user.AccountEnabled) { "Active" } else { "DISABLED" }
            Write-Host "$($psp.DisplayName): $($user.DisplayName) [$status]"
        }
    }
}
Write-Host "Orphaned privileged SPs: $orphanCount / $($privilegedSPs.Count)"

Write-Host "`n=== RECENT CREDENTIAL ADDITIONS (90 days) ===" -ForegroundColor Cyan
$cutoff = (Get-Date).AddDays(-90).ToString("yyyy-MM-ddTHH:mm:ssZ")
$credLogs = Get-MgAuditLogDirectoryAudit -Filter "activityDisplayName eq 'Add service principal credentials' and activityDateTime ge $cutoff" -All

if ($credLogs.Count -eq 0) {
    Write-Host "No credential additions in the last 90 days."
} else {
    foreach ($cl in $credLogs) {
        $target = $cl.TargetResources | Where-Object { $_.Type -eq "ServicePrincipal" }
        $actor = $cl.InitiatedBy.User.UserPrincipalName ?? "App context"
        Write-Host "$(($cl.ActivityDateTime).ToString('yyyy-MM-dd')) | $actor | $($target.DisplayName)"
    }
    Write-Host "Total credential additions: $($credLogs.Count)"
}

Write-Host "`n=== SUMMARY ===" -ForegroundColor Cyan
Write-Host "Privileged SPs: $($privilegedSPs.Count)"
Write-Host "Orphaned (no owner): $orphanCount"
Write-Host "Credential additions (90d): $($credLogs.Count)"
if ($orphanCount -gt 0) {
    Write-Host "`nACTION REQUIRED: Assign owners to orphaned privileged service principals." -ForegroundColor Yellow
}

Copy the script. Run it against your tenant. If the orphan count is greater than zero, you have privileged service principals with no accountable owner — the exact condition that makes the ownership takeover chain exploitable.


Service principal governance is one module in Ridgeline's Identity and Access Management course — covering the full non-human identity lifecycle from inventory through credential governance to operational cadences. If you're working with the M365 identity stack daily, the Entra ID Security course covers the detection side of identity compromise.

Ridgeline Cyber Defence Written by security practitioners. Published weekly on Tuesdays.

Get security ops insights weekly

One email every Tuesday. Detection techniques, investigation methods, and operational security. Unsubscribe anytime.

Ridgeline Training

Want to go deeper?

Hands-on courses covering Identity Security with labs, deployable artifacts, and free foundation modules.

Identity and Access Management → Entra ID Security →