Most Entra ID tenants hold hundreds of service principals that nobody has signed in with for months, and each one can still carry credentials, admin consent and app role assignments. A stale service principal is free persistence for an attacker: add a secret, request a token, and the activity looks like a normal application. This runbook shows how to find stale service principals with the sign-in activity reports and the Entra recommendations, how to disable them first, delete them safely and restore them inside the 30 day window if something breaks. It is written for Entra administrators and identity engineers who need a repeatable clean-up, not a one-off purge. For the background on applications, service principals and consent, see the Entra ID Applications and Consent Guide.
Key takeaways
- Use the activity reports, not guesswork. The Graph beta reports
servicePrincipalSignInActivitiesandappCredentialSignInActivitiesgive the last sign-in per service principal and per credential. - Disable before you delete. Setting
accountEnabledto false stops all token issuance in the tenant while keeping the configuration, so a missed dependency is a one-line fix. - Deletion is reversible for 30 days. Deleted service principals sit in the recycle bin and can be restored, but Conditional Access policies tied to them are not brought back.
- Filter out what you cannot or should not touch. Microsoft first-party apps and managed identities behave differently and need their own handling.
- Remove credentials even when you keep the app. Unused secrets and certificates on live apps are the easier attacker target, and Entra flags credentials unused for 30 days.
When you need this
Run this procedure when any of the following apply:
- Tenant hygiene: the Entra admin center shows the Remove unused applications recommendation (API name
staleApps), which flags apps not used for over 90 days, or Remove unused credentials from applications (staleAppCreds), which flags credentials unused for 30 days. - Incident follow-up: after an investigation found an attacker adding credentials to an existing application (ATT&CK T1098.001 Additional Cloud Credentials). Storm-1283 did exactly this to line-of-business apps and then used them to run cryptomining VMs, as covered in the Storm-1283 OAuth apps abuse analysis.
- Legacy and test tenants: the Microsoft Midnight Blizzard breach started from a forgotten test tenant and a legacy test OAuth application that still had elevated access to the corporate environment.
- Audit findings: an access review or audit asks for evidence that dormant non-human accounts are disabled (NIST SP 800-53 AC-2(3) Disable Accounts).
Before you start
| Requirement | Detail |
|---|---|
| Read-only discovery | Graph permission AuditLog.Read.All (least privileged) for the two activity reports, plus a role such as Reports Reader, Security Reader or Security Administrator for delegated use |
| Recommendations | Reports Reader, Security Reader or Global Reader to view; Security Administrator to update status |
| Disable, delete, restore | Cloud Application Administrator, Application Administrator or owner of the service principal; scope Application.ReadWrite.All |
| Tooling | Microsoft Graph PowerShell (Install-Module Microsoft.Graph) or Microsoft Entra PowerShell; Graph Explorer for one-off calls |
| API status | The activity reports are on the /beta endpoint and Microsoft does not support beta for production use. Treat results as evidence to review, not as an automated delete trigger |
| Change record | A ticket listing each service principal object ID, appId, owners and permissions before any change |
Agree a dormancy threshold before you query. Entra's own recommendation uses 90 days for apps; many organisations add a quarter-end or annual job buffer for batch integrations and pick 180 days for automatic disable. The NHI Dormancy Detection guide covers how to choose thresholds across platforms.
Step-by-step
Steps 1 to 4 are read-only. Step 5 changes the tenant but is reversible. Step 6 is destructive after the 30 day recycle bin period.
- Export every service principal with its type and owner tenant. The
servicePrincipalTypeproperty separatesApplication,ManagedIdentity,LegacyandServiceIdentity(agent identities).appOwnerOrganizationIdholds the tenant where the application is registered, which lets you split your own apps from third-party and Microsoft apps.Connect-MgGraph -Scopes "Application.Read.All","AuditLog.Read.All" Get-MgServicePrincipal -All -Property Id,AppId,DisplayName,ServicePrincipalType,AppOwnerOrganizationId,AccountEnabled,KeyCredentials,PasswordCredentials | Select-Object Id,AppId,DisplayName,ServicePrincipalType,AppOwnerOrganizationId,AccountEnabled | Export-Csv sp_inventory.csv -NoTypeInformation - Pull last sign-in activity per service principal. The report returns
lastSignInActivityand four breakdowns:delegatedClientSignInActivity,delegatedResourceSignInActivity,applicationAuthenticationClientSignInActivityandapplicationAuthenticationResourceSignInActivity, each withlastSignInDateTime.
Join onGET https://graph.microsoft.com/beta/reports/servicePrincipalSignInActivities GET https://graph.microsoft.com/beta/reports/servicePrincipalSignInActivities?$filter=appId eq '{appId}'appId. A service principal with no row, or with alastSignInDateTimeolder than your threshold, is a candidate. Check the resource-side fields too: an API that other apps call shows activity there, not on the client side. - Pull credential-level activity.
appCredentialSignInActivitiesreturns one row per credential withkeyId,keyType(certificate or secret),credentialOrigin(application or servicePrincipal), the expiry andsignInActivity.lastSignInDateTime.
This finds the common case of a live app carrying an old secret nobody uses.GET https://graph.microsoft.com/beta/reports/appCredentialSignInActivities?$filter=appId eq '{appId}' - Cross-check the recommendations and enrich. In the Microsoft Entra admin center go to Entra ID > Overview > Recommendations and open Remove unused applications; the Impacted resources list should agree with your query. For each candidate record owners (
GET /servicePrincipals/{id}/owners), app role assignments, delegated grants (oauth2PermissionGrants) and Azure RBAC assignments. Contact owners with a deadline. No owner and high privilege moves a candidate to the front of the queue. - Disable the candidates (reversible). Disabling prevents all tokens being issued for the application in this tenant. Portal: Entra ID > Enterprise apps > All applications > select the app > Properties > Enabled for users to sign-in? = No > Save. Graph PowerShell:
Or Graph:$sp = Get-MgServicePrincipal -Filter "appId eq '{appId}'" Update-MgServicePrincipal -ServicePrincipalId $sp.Id -AccountEnabled:$falsePATCH /v1.0/servicePrincipals/{id}with{"accountEnabled": false}. Record the change in the service principal'snotesfield (up to 1,024 characters) or a tag so the next reviewer sees why. Leave it disabled for a full business cycle, usually 30 to 90 days. - Delete (destructive after 30 days). When the disable period passes with no complaints, delete. Portal: Enterprise apps > All applications > app > Properties > Delete. Graph PowerShell:
Remove-MgServicePrincipal -ServicePrincipalId '{id}', orDELETE /v1.0/servicePrincipals/{id}. For apps registered in your own tenant, decide whether to delete the app registration too: deleting the registration also deletes its service principal. Remove unused credentials from apps you keep through App registrations > app > Certificates & secrets.
Verify it worked
- Disabled state:
Get-MgServicePrincipal -ServicePrincipalId '{id}' -Property AccountEnabledreturns False. - No new tokens: the service principal sign-in log (the log that records apps authenticating with their own certificate or secret) shows failures, not successes, for the appId. Managed identities sign in to a separate managed identity log, so check that one for those.
- Deletion landed in the recycle bin:
GET /v1.0/directory/deletedItems/microsoft.graph.servicePrincipal?$filter=appId eq '{appId}'returns the object. - Recommendation status: the Impacted resources count for Remove unused applications falls on the next refresh. Mark anything you deliberately keep as Dismissed with a reason.
- Audit trail: the Entra audit log shows the update and delete operations against your change ticket.
Rollback and common errors
| Situation | Fix |
|---|---|
| A business process broke after disable | Set accountEnabled back to true; record the owner and the real usage pattern so the next review does not repeat it |
| Deleted the wrong service principal (within 30 days) | Restore-MgDirectoryDeletedItem -DirectoryObjectId {id} or POST /v1.0/directory/deletedItems/{id}/restore. Re-create any Conditional Access targeting manually, because restore does not bring back previous policies |
| Deleted more than 30 days ago | Not recoverable. Re-register or re-consent the app and re-grant permissions from your pre-change export |
| Name filter matched two objects | Display names are not unique. Always filter on appId or object Id |
| Managed identity appears stale | Managed identity service principals cannot be deleted or restored by customers in Entra; remove the managed identity from the Azure resource instead |
| Microsoft app flagged by your script | Exclude Microsoft-owned apps; Entra's own recommendation already does. Disable a first-party app only after reading Microsoft guidance for that app |
| Beta call returns 403 | The caller lacks AuditLog.Read.All or the delegated user lacks a reports role |
| Multitenant app you publish | Disabling the service principal affects your tenant only. To stop it everywhere, deactivate the application registration instead |
Do not permanently purge from the recycle bin (Remove-MgDirectoryDeletedItem) as part of routine clean-up. Let the 30 day window run unless a security incident requires immediate removal.
Hardening afterwards
- Schedule it: run steps 1 to 3 monthly as an automation using an application with
AuditLog.Read.AllandApplication.Read.Allonly. Keep write permissions in a separate, human-approved step. - Require two owners: Microsoft's Graph reference recommends at least two owners per service principal. Make ownership a creation gate and re-attest it in access reviews.
- Prefer certificates or federation over secrets: for workloads outside Azure, replace client secrets with workload identity federation. See the Workload Identity Federation Guide and the Azure Entra Client Secrets Security Guide.
- Alert on credential additions: new secrets or certificates on dormant apps are a classic persistence signal. The Entra ID Sign-In and Audit Log Detection Guide covers the audit events to watch.
- Close the loop with inventory: feed results into your NHI inventory so a deleted principal is retired everywhere. See NHI Offboarding and Decommissioning Guide.
Metrics that show it is working
| Metric | Target to aim for |
|---|---|
| Service principals with no sign-in beyond threshold and still enabled | Zero after each cycle |
| Applications with credentials unused for 30 days | Falling month on month |
| Service principals with fewer than two owners | Zero for privileged apps |
| Restores within 30 days of deletion | Low; a high rate means the disable period is too short |
| Median days from flag to disable | Under 30 |
Framework mapping
| Framework | Reference | How this runbook supports it |
|---|---|---|
| NIST SP 800-53 Rev 5 | AC-2, AC-2(3), IA-4 | Account management, disabling inactive accounts, identifier retirement |
| NIST CSF 2.0 | PR.AA-01, PR.AA-05 | Identities and credentials for services are managed; permissions reviewed |
| MITRE ATT&CK | T1098.001, T1078.004 | Removes dormant apps that attackers add credentials to or use as cloud accounts |
| CIS Controls v8.1 | 5.3 Disable Dormant Accounts | Same intent applied to workload identities |
Small tenant or large estate
A tenant with a few dozen custom apps can run this by hand each quarter from the portal recommendation. An estate with thousands of service principals should script steps 1 to 3, group candidates by owning team and privilege, and disable in waves (low privilege first, then apps with Microsoft Graph application permissions or Azure subscription roles once owners have confirmed). In both cases, apps holding high-impact application permissions deserve a named reviewer, because a stale app with directory or mailbox write permissions is the highest-value target.
Practitioner checklist
- Export all service principals with type, owner tenant, owners and credentials before any change.
- Join the two Graph activity reports on appId and apply an agreed dormancy threshold.
- Exclude Microsoft-owned apps and handle managed identities on the Azure resource.
- Notify owners with a deadline; treat ownerless privileged apps as top priority.
- Disable with
accountEnabledfalse and record the reason in notes or tags. - Delete only after a full business cycle with no breakage.
- Remove unused secrets and certificates from apps you keep.
- Keep the pre-change export so a restore or re-consent is quick.
Standards and references
- Microsoft Learn: List servicePrincipalSignInActivities
- Microsoft Learn: List appCredentialSignInActivities
- Microsoft Learn: Remove unused applications recommendation
- Microsoft Learn: Remove unused credentials from apps recommendation
- Microsoft Learn: Disable user sign-in for an application
- Microsoft Learn: Delete an enterprise application
- Microsoft Learn: Restore an enterprise application
- Microsoft Learn: servicePrincipal resource type
Related NHI Mgmt Group resources: Entra ID Applications and Consent Guide · Azure RBAC and Service Principals Guide · NHI Dormancy Detection · NHI Offboarding and Decommissioning Guide