APP-07: Identifying and Disabling Unused Service Principals
Overview
Unused service principals represent a hidden attack surface in your Microsoft 365 environment. These dormant identities may retain permissions from their original purpose, making them attractive targets for attackers. This guide helps you systematically identify service principals that are no longer in use and safely decommission them.
Prerequisites
Required Roles
- Global Administrator or Application Administrator - Required to disable/delete applications
- Cloud Application Administrator - Can manage most applications
- Security Reader - Can view sign-in logs and application data (read-only)
Required Licenses
- Microsoft Entra ID P1 or P2 (required for service principal sign-in logs)
- Microsoft Defender for Cloud Apps (optional, for enhanced visibility)
Time Estimate
- Initial Identification: 30-45 minutes
- Per-Application Assessment: 5-10 minutes
- Disabling/Deletion: 5 minutes per application
Step-by-Step Instructions
Step 1: Define "Unused" Criteria
Establish thresholds for identifying unused service principals:
| Inactivity Period | Recommendation |
|---|---|
| 30 days | Monitor |
| 60 days | Investigate |
| 90 days | Disable (soft delete) |
| 180 days | Delete (if disabled without issues) |
Consider these factors:
- Seasonal applications (e.g., tax software used annually)
- Disaster recovery tools (used only in emergencies)
- Migration tools (may be needed again)
Step 2: Gather Sign-in Activity Data
Method A: Entra Admin Center
- Navigate to Microsoft Entra admin center (https://entra.microsoft.com)
- Go to Identity → Applications → Enterprise applications
- Click Activity → Sign-ins
- Filter by Application and review last sign-in dates
- Look for applications with no recent activity
Method B: PowerShell Comprehensive Analysis
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.Read.All", "AuditLog.Read.All", "Directory.Read.All"
# Set inactivity threshold
$inactivityDays = 90
$cutoffDate = (Get-Date).AddDays(-$inactivityDays)
# Get all service principals
$servicePrincipals = Get-MgServicePrincipal -All
$results = @()
foreach ($sp in $servicePrincipals) {
# Skip Microsoft first-party apps (they may not have typical sign-in patterns)
if ($sp.AppOwnerOrganizationId -eq "f8cdef31-a31e-4b4a-93e4-5f571e91255a") {
continue # Microsoft's tenant ID
}
# Get last sign-in activity
try {
$signIns = Get-MgAuditLogSignIn -Filter "appId eq '$($sp.AppId)'" -Top 1 -OrderBy "createdDateTime desc" -ErrorAction SilentlyContinue
$lastSignIn = $signIns.CreatedDateTime
}
catch {
$lastSignIn = $null
}
# Determine status
$daysSinceActivity = if ($lastSignIn) { ((Get-Date) - $lastSignIn).Days } else { 999 }
$status = switch ($daysSinceActivity) {
{ $_ -lt 30 } { "Active" }
{ $_ -lt 60 } { "Monitor" }
{ $_ -lt 90 } { "Investigate" }
{ $_ -lt 180 } { "Disable Candidate" }
default { "Delete Candidate" }
}
if ($daysSinceActivity -ge 60) {
# Get owners for notification
$owners = Get-MgServicePrincipalOwner -ServicePrincipalId $sp.Id -ErrorAction SilentlyContinue
$ownerEmails = ($owners | ForEach-Object {
$user = Get-MgUser -UserId $_.Id -ErrorAction SilentlyContinue
$user.Mail
}) -join "; "
# Get permissions for risk assessment
$appRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id
$permissionCount = $appRoles.Count
$results += [PSCustomObject]@{
DisplayName = $sp.DisplayName
AppId = $sp.AppId
ServicePrincipalId = $sp.Id
CreatedDateTime = $sp.CreatedDateTime
LastSignIn = $lastSignIn
DaysSinceActivity = $daysSinceActivity
Status = $status
PermissionCount = $permissionCount
Owners = $ownerEmails
Enabled = $sp.AccountEnabled
AppType = $sp.ServicePrincipalType
}
}
}
# Display results
$results | Sort-Object DaysSinceActivity -Descending | Format-Table DisplayName, DaysSinceActivity, Status, PermissionCount, Enabled -AutoSize
# Export for review
$results | Export-Csv -Path "UnusedServicePrincipals.csv" -NoTypeInformation
Write-Host "`nSummary:"
Write-Host "Monitor (30-60 days): $($results | Where-Object Status -eq 'Monitor' | Measure-Object | Select-Object -ExpandProperty Count)"
Write-Host "Investigate (60-90 days): $($results | Where-Object Status -eq 'Investigate' | Measure-Object | Select-Object -ExpandProperty Count)"
Write-Host "Disable Candidates (90-180 days): $($results | Where-Object Status -eq 'Disable Candidate' | Measure-Object | Select-Object -ExpandProperty Count)"
Write-Host "Delete Candidates (180+ days): $($results | Where-Object Status -eq 'Delete Candidate' | Measure-Object | Select-Object -ExpandProperty Count)"
Step 3: Validate Unused Status
Before disabling, verify the service principal is truly unused:
Check Sign-in Logs
# Check sign-in attempts (successful and failed)
$appId = "the-app-id"
$signIns = Get-MgAuditLogSignIn -Filter "appId eq '$appId'" -Top 100
$signIns | Select-Object CreatedDateTime, Status, IPAddress, ResourceDisplayName | Format-Table
Check Audit Logs for API Calls
# Check for API activity
$spId = "service-principal-id"
$auditLogs = Get-MgAuditLogDirectoryAudit -Filter "initiatedBy/app/servicePrincipalId eq '$spId'" -Top 100
$auditLogs | Select-Object ActivityDateTime, ActivityDisplayName, Result | Format-Table
Contact Application Owner
- Find the owner in application properties
- Send notification:
- Application name and ID
- Last activity date
- Planned action (disable on X date)
- Request for business justification
Step 4: Disable Unused Service Principals
Disabling is a reversible action - preferred before deletion.
Via Entra Admin Center
- Navigate to Enterprise applications → Select the application
- Go to Properties
- Set Enabled for users to sign-in to No
- Click Save
Via PowerShell (Single Application)
Connect-MgGraph -Scopes "Application.ReadWrite.All"
$servicePrincipalId = "your-service-principal-id"
Update-MgServicePrincipal -ServicePrincipalId $servicePrincipalId -AccountEnabled:$false
Write-Host "Service principal disabled"
Via PowerShell (Bulk Disable from CSV)
# CSV format: ServicePrincipalId, DisplayName
$toDisable = Import-Csv -Path "SPsToDisable.csv"
foreach ($sp in $toDisable) {
try {
Update-MgServicePrincipal -ServicePrincipalId $sp.ServicePrincipalId -AccountEnabled:$false
Write-Host "Disabled: $($sp.DisplayName)" -ForegroundColor Green
}
catch {
Write-Host "Failed to disable $($sp.DisplayName): $_" -ForegroundColor Red
}
}
Step 5: Monitor for Breakage
After disabling, watch for:
- User complaints - Ticket system, email, Teams messages
- Failed sign-in attempts - Check sign-in logs for the disabled app
- Automation failures - Monitor scheduled tasks, pipelines
Set Up Monitoring Alert
# Run daily to check for failed sign-ins from disabled apps
$disabledApps = Get-MgServicePrincipal -Filter "accountEnabled eq false"
foreach ($app in $disabledApps) {
$failedSignIns = Get-MgAuditLogSignIn -Filter "appId eq '$($app.AppId)' and status/errorCode ne 0" -Top 10
if ($failedSignIns.Count -gt 0) {
Write-Host "Warning: Disabled app '$($app.DisplayName)' has $($failedSignIns.Count) failed sign-in attempts"
}
}
Step 6: Delete After Verification Period
After 90 days of being disabled without issues, delete the service principal.
Via Entra Admin Center
- Navigate to Enterprise applications → Select the application
- Click Delete in the top menu
- Read the warning carefully
- Type the application name to confirm
- Click Delete
Via PowerShell
Connect-MgGraph -Scopes "Application.ReadWrite.All"
$servicePrincipalId = "your-service-principal-id"
# Delete service principal
Remove-MgServicePrincipal -ServicePrincipalId $servicePrincipalId
# If there's also an app registration, delete it
$appId = "app-object-id"
Remove-MgApplication -ApplicationId $appId
Write-Host "Service principal and application deleted"
Step 7: Handle Special Cases
Seasonal Applications
- Document the expected usage pattern
- Set calendar reminder for expected activity
- Tag as "Seasonal" in your inventory
- Review before and after expected usage period
Disaster Recovery Tools
- Test the application quarterly
- Document as "DR Tool - Expected Low Activity"
- Include in DR runbooks
- Do not disable without DR team approval
Migration Tools
- Confirm migration is complete
- Get written confirmation from project owner
- Wait 90 days post-migration before disabling
- Keep documentation for potential rollback
Step 8: Establish Ongoing Governance
Automate Discovery
Set up a monthly report:
# Schedule via Azure Automation or Task Scheduler
$report = # ... (use the discovery script from Step 2)
# Email report to security team
$htmlReport = $report | ConvertTo-Html -Title "Unused Service Principal Report"
Send-MailMessage -To "security@company.com" -Subject "Monthly Unused SP Report" -Body $htmlReport -BodyAsHtml
Implement Tagging
Use application notes or custom attributes to track status:
# Add a note to track review status
$notes = "Reviewed $(Get-Date -Format 'yyyy-MM-dd') - Approved for continued use by JSmith"
Update-MgServicePrincipal -ServicePrincipalId $spId -Notes $notes
Verification Checklist
After completing the cleanup:
- All service principals inactive for 60+ days are identified
- Owners have been notified of planned actions
- Business justification is documented for active, low-usage apps
- Unused service principals have been disabled (not immediately deleted)
- Monitoring is in place to detect breakage
- 90-day waiting period before deletion is enforced
- Seasonal/DR applications are documented and excluded
- Monthly automated discovery is scheduled
- Cleanup actions are logged in change management system
- Security team has been briefed on findings
Troubleshooting
Issue: Sign-in logs don't show service principal activity
Cause: Service principal sign-in logs require Entra ID P1/P2 license.
Solution:
- Verify you have Entra ID P1 or P2 licensing
- Sign-in logs are retained for 30 days (P1) or 90 days (P2)
- For historical data, check Azure Monitor or SIEM if configured
- Use audit logs as an alternative indicator of activity
Issue: Disabled application was needed
Cause: Application was used infrequently or by a different team.
Solution:
- Re-enable immediately:
Update-MgServicePrincipal -ServicePrincipalId $spId -AccountEnabled:$true - Document the use case
- Add to exceptions list with owner and review date
- Implement alerting for future disabling attempts
Issue: Cannot delete service principal - "In use" error
Cause: Service principal may have dependencies or be managed.
Solution:
- Check if it's a managed identity (cannot be deleted separately)
- Look for linked resources (Logic Apps, Azure Functions)
- Review if it's a first-party Microsoft service
- Some SPs are recreated automatically - block consent instead
Issue: Application was deleted but users still see it
Cause: Browser caching or the application consent still exists.
Solution:
- The enterprise app is deleted; app registration may still exist
- Check App registrations for the corresponding app
- User's cached tokens will expire
- Clear browser cache if immediate removal needed
Issue: Cannot determine if application is Microsoft first-party
Cause: Microsoft apps use various tenant IDs and naming conventions.
Solution:
- Check
AppOwnerOrganizationId:- Microsoft main tenant:
f8cdef31-a31e-4b4a-93e4-5f571e91255a - Other Microsoft tenants may also be first-party
- Microsoft main tenant:
- Check if the publisher is "Microsoft"
- First-party apps typically cannot be deleted
- When in doubt, search the App ID online
Issue: Scheduled job runs under a service principal being evaluated
Cause: Background jobs don't always generate sign-in logs.
Solution:
- Check Azure Automation, Logic Apps, and scheduled tasks
- Review associated resources in Azure portal
- Ask application owners about batch processes
- Check for API activity in audit logs instead of sign-in logs
Issue: Too many applications to review manually
Cause: Enterprise environments may have hundreds of service principals.
Solution:
- Prioritize by risk:
- High permissions + no activity = high priority
- No owner + no activity = high priority
- Low permissions + some activity = low priority
- Use the automated script to generate prioritized lists
- Consider Microsoft Defender for Cloud Apps for automated insights
- Set up a phased cleanup schedule
Related Resources
- Service principal sign-in activity
- Delete enterprise applications
- Application management best practices
- App governance overview
Last updated: January 2025