Skip to content

Quarterly Stale Service Principal Cleanup

Status: Runbook Owner: Infrastructure (Kobe executes, Rory approves all deletions) Schedule: First business day of January, April, July, October Related: quarterly-sp-permission-review.md

Purpose

Detect and remove Entra service principals that are no longer used. Stale SPs are a credential-compromise risk and a compliance finding under HIPAA least-privilege.

Cleanup Criteria

An SP is a cleanup candidate if ALL of the following are true:

  1. No sign-in activity in 90+ days (per AuditLogs / AADServicePrincipalSignInLogs)
  2. No documented automation use (not listed in bedrock-docs/compliance/lpriv-evidence-mapping.md)
  3. No active federated credentials referenced by a live GitHub Actions workflow
  4. Not a break-glass account (never delete)

Cleanup candidates require Rory's explicit approval before deletion.

Procedure

1. List all service principals (both tenants)

PROD tenant:

az login --tenant d477c9f8-<...>.onmicrosoft.com
az ad sp list --query "[].{appId:appId, displayName:displayName, notes:notes, createdDateTime:createdDateTime}" -o json > /tmp/sp-prod.json

DDE tenant:

az login --tenant ff1c5d68-<...>.onmicrosoft.com
az ad sp list --query "[].{appId:appId, displayName:displayName, notes:notes, createdDateTime:createdDateTime}" -o json > /tmp/sp-dde.json

2. Check last sign-in per SP

KQL:

AADServicePrincipalSignInLogs
| where TimeGenerated >= ago(90d)
| summarize LastSignIn = max(TimeGenerated) by ServicePrincipalId

CLI:

az monitor log-analytics query -w 5d76d1f2-<...> --analytics-query '<query above>' -o json > /tmp/sp-signins.json

3. Join datasets and build candidate list

Use the Python helper in scripts/sp-cleanup-candidates.py (template to be added to ops-automation). For each candidate, gather:

  • Display name, appId, creation date, owner (if any), notes field
  • Assigned roles (az role assignment list --assignee <appId> --all)
  • Federated credentials (az ad app federated-credential list --id <appId>)
  • Any secrets or certs attached (az ad app credential list --id <appId>)

Write report to /tmp/sp-cleanup-candidates-<YYYYQN>.md and attach to SecOps change ticket for Rory's review.

4. Get approval

  • Open a SecOps change ticket: POST /api/changes with body describing proposed deletions
  • Tag Rory — wait for explicit approval reply

5. Perform deletions (only after approval)

For each approved SP:

az role assignment list --assignee <appId> --all --query "[].id" -o tsv | xargs -n1 az role assignment delete --ids
az ad app federated-credential list --id <appId> --query "[].id" -o tsv | xargs -I{} az ad app federated-credential delete --id <appId> --federated-credential-id {}
az ad sp delete --id <appId>
az ad app delete --id <appId>

6. Evidence

aws s3 cp /tmp/sp-cleanup-candidates-<YYYYQN>.md s3://cirius-hipaa-evidence/sp-cleanup/<YYYYQN>/
curl -s -X POST -H "X-API-Key: $SECOPS_API_KEY" -H "Content-Type: application/json" \
  -d '{"evidence_type":"sp-cleanup","period_start":"<ISO>","period_end":"<ISO>","s3_key":"sp-cleanup/<YYYYQN>/sp-cleanup-candidates-<YYYYQN>.md","source_system":"PROD"}' \
  https://soc.bedrockcybersecurity.org/api/evidence

Success Criteria

  • [ ] All SPs in both tenants enumerated
  • [ ] Sign-in data pulled for 90-day window
  • [ ] Candidate list compiled and sent to Rory
  • [ ] Approved deletions executed; unapproved retained
  • [ ] Evidence uploaded to S3 and registered in SecOps

Schedule

  • Q1: January first business day
  • Q2: April first business day
  • Q3: July first business day
  • Q4: October first business day

Change Log

DateChangeAuthor
2026-04-16Initial runbookKobe

Internal use only — Cirius Group