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 PeriodRecommendation
30 daysMonitor
60 daysInvestigate
90 daysDisable (soft delete)
180 daysDelete (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

  1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com)
  2. Go to IdentityApplicationsEnterprise applications
  3. Click ActivitySign-ins
  4. Filter by Application and review last sign-in dates
  5. 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

  1. Find the owner in application properties
  2. 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

  1. Navigate to Enterprise applications → Select the application
  2. Go to Properties
  3. Set Enabled for users to sign-in to No
  4. 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:

  1. User complaints - Ticket system, email, Teams messages
  2. Failed sign-in attempts - Check sign-in logs for the disabled app
  3. 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

  1. Navigate to Enterprise applications → Select the application
  2. Click Delete in the top menu
  3. Read the warning carefully
  4. Type the application name to confirm
  5. 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

  1. Document the expected usage pattern
  2. Set calendar reminder for expected activity
  3. Tag as "Seasonal" in your inventory
  4. Review before and after expected usage period

Disaster Recovery Tools

  1. Test the application quarterly
  2. Document as "DR Tool - Expected Low Activity"
  3. Include in DR runbooks
  4. Do not disable without DR team approval

Migration Tools

  1. Confirm migration is complete
  2. Get written confirmation from project owner
  3. Wait 90 days post-migration before disabling
  4. 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:

  1. Verify you have Entra ID P1 or P2 licensing
  2. Sign-in logs are retained for 30 days (P1) or 90 days (P2)
  3. For historical data, check Azure Monitor or SIEM if configured
  4. 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:

  1. Re-enable immediately:
    Update-MgServicePrincipal -ServicePrincipalId $spId -AccountEnabled:$true
    
  2. Document the use case
  3. Add to exceptions list with owner and review date
  4. Implement alerting for future disabling attempts

Issue: Cannot delete service principal - "In use" error

Cause: Service principal may have dependencies or be managed.

Solution:

  1. Check if it's a managed identity (cannot be deleted separately)
  2. Look for linked resources (Logic Apps, Azure Functions)
  3. Review if it's a first-party Microsoft service
  4. 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:

  1. The enterprise app is deleted; app registration may still exist
  2. Check App registrations for the corresponding app
  3. User's cached tokens will expire
  4. 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:

  1. Check AppOwnerOrganizationId:
    • Microsoft main tenant: f8cdef31-a31e-4b4a-93e4-5f571e91255a
    • Other Microsoft tenants may also be first-party
  2. Check if the publisher is "Microsoft"
  3. First-party apps typically cannot be deleted
  4. 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:

  1. Check Azure Automation, Logic Apps, and scheduled tasks
  2. Review associated resources in Azure portal
  3. Ask application owners about batch processes
  4. 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:

  1. Prioritize by risk:
    • High permissions + no activity = high priority
    • No owner + no activity = high priority
    • Low permissions + some activity = low priority
  2. Use the automated script to generate prioritized lists
  3. Consider Microsoft Defender for Cloud Apps for automated insights
  4. Set up a phased cleanup schedule

Related Resources


Last updated: January 2025