Service accounts are the quietest identities in your estate—and the most dangerous when forgotten. They run backups, talk to databases, deploy code, and glue systems together. When those identities become stale (unused) or orphaned (no clear owner), you inherit invisible risk: standing permissions with no sponsor, credentials that never rotate, and endpoints that still trust them by habit. That is precisely the attack surface modern adversaries love.
Offense keeps getting easier: credential dumps, basic auth fallbacks, and evergreen Kerberos paths mean an old account can still open a lot of doors. Defense must be faster and more disciplined. The goal of this guide is to help you build a repeatable detection pipeline that finds, proves, and retires these identities without causing outages.
What people mean by stale and orphaned—and why that isn’t enough
Definitions you can use in policy and audits:
- Stale service account: an enabled non-human identity with no recent legitimate usage within a defined window (for example, 90–180 days depending on workload cadence).
- Orphaned service account: an identity with no accountable owner (person or team) on record.
- Zombie identity: credentials still accepted by something, despite no declared need or recorded ownership.
These states overlap but are not identical. A service account may appear inactive in the directory yet still be used by a third-party system via cached tickets. Conversely, it may have recent sign-ins but nobody who will vouch for its continued existence. Your detection model must evaluate both usage and ownership, not just one.
A common anti-pattern is to pick a 30-day inactivity window and purge. That misses workloads with monthly, quarterly, or seasonal cadence. It also ignores ownerlessness, expiring secrets, and unmanaged SPNs—three drivers of breach risk that often hide behind “looks quiet.”
First principles: four axes of service identity health
Every non-human identity can be evaluated across four irreducible facets. If any facet is missing or stale, risk increases sharply:
- Owner: a person or team that attests the identity is needed now.
- Purpose: a plain-English description you can read back: “Runs SQL Agent jobs for the data warehouse nightly.”
- Path: where the credential is accepted—SPNs, app registrations, scopes, roles, and trust paths.
- Proof: objective recent activity tied to that path—sign-ins, tickets, API calls, and process logs.
Design your detection pipeline to score each account across these axes. “Inactive” is only one axis (proof). Ownerless is primarily the owner axis. High-privilege with broad delegation strikes the path axis. A password last set five years ago with DONT_EXPIRE_PASSWORD is a hygiene smell that cuts across all four.
Two consequences fall out of these principles:
- Managed beats managed-by-hand. Group Managed Service Accounts (gMSA) eliminate human-managed passwords and standardize SPN lifecycle. Where gMSA is feasible, drift and detection complexity both fall.
- Telemetry must match the plane. AD logons, Entra service principal sign-ins, and application telemetry are different planes. Align your evidence to the plane where the identity actually authenticates.
Practical primers you may find helpful as you apply these principles:
- Find a user’s last logon time (understanding last logon attributes).
- Computer delegation & SPNs explained (mapping path and delegation risk).
- Configure gMSA for Defender for Identity (adopting managed identities).
- Active Directory maintenance checklist (related hygiene tasks that surface the same identities).
The hands-on core: build a detection pipeline you can run this week
This section is the technical “hero” content: step-by-step, copy-pastable building blocks for a robust stale/orphaned service account program across AD and Microsoft Entra. Adapt names and thresholds to your environment.
```1) Build a trustworthy inventory in Active Directory
Heuristics that reliably surface service accounts:
- User objects with one or more SPNs (
servicePrincipalNameattribute). - Accounts configured to run Windows services, IIS app pools, tasks, or agents.
userAccountControlflags:DONT_EXPIRE_PASSWORD,PASSWORD_NOT_REQUIRED,TRUSTED_FOR_DELEGATION,ACCOUNTDISABLE.- Explicit gMSA objects (
msDS-ManagedServiceAccountclass). - Naming conventions (
svc_,sa_) and OU placement.
# Likely service accounts by SPN or password policy hints
```
Get-ADUser -LDAPFilter "(&(objectClass=user)(|(servicePrincipalName=*))(userAccountControl:1.2.840.113556.1.4.803:=65536))" `
-Properties servicePrincipalName, userAccountControl, lastLogonTimestamp, pwdLastSet, whenCreated, memberOf |
Select-Object SamAccountName, Enabled,
@{n='HasSPN';e={([bool]($_.servicePrincipalName))}},
@{n='PwdNeverExpires';e={($*.userAccountControl -band 0x10000) -ne 0}},
@{n='LastLogonTS';e={$*.lastLogonTimestamp}},
@{n='PwdLastSet';e={$_.pwdLastSet}},
whenCreated, memberOf
# All SPN-bearing principals (users & computers)
Get-ADObject -LDAPFilter "(servicePrincipalName=*)" -Properties servicePrincipalName |
Select-Object Name, ObjectClass, servicePrincipalName
# All gMSA objects
Get-ADServiceAccount -Filter * -Properties PrincipalsAllowedToRetrieveManagedPassword,ServicePrincipalNames Tip: lastLogonTimestamp is replicated and updates coarsely; use it for staleness screening, not precise forensics. For precision, aggregate per-DC lastLogon or rely on KDC logs and SIEM evidence.
2) Attach owners—and expose the ownerless
Every service identity should have a current owner and purpose. If your directory lacks a dedicated attribute for ownership, start simple: use managedBy or a dedicated mail field pointing to an accountable distribution list. Anything enabled without an owner is high risk by default.
$svc = Get-ADUser -LDAPFilter "(&(objectClass=user)(servicePrincipalName=*))" -Properties mail,description,managedBy
```
$ownerless = $svc | Where-Object { -not $*.managedBy -and -not $*.mail -and $_.Enabled }
$ownerless | Select SamAccountName,Description | Export-Csv .\ownerless-service-accounts.csv -NoType Run a lightweight access-certification round with application owners using this export. If nobody claims an identity, move it to a decommissioning queue with a staged disable and observation window.
```3) Prove recent activity on the AD plane
Don’t rely on logon timestamps alone. Cross-check multiple signals:
- KDC issuance of tickets to the account.
- Server event logs where the service should run (Service Control Manager, application logs).
- Network telemetry for SPN-targeted connections.
- SIEM queries (for example, Windows Security 4624 logons by the account).
# 90 days stale by replicated lastLogonTimestamp
```
$days = 90
Search-ADAccount -UsersOnly -AccountInactive -TimeSpan "$days.00:00:00" |
Where-Object { $*.Enabled -and $*.servicePrincipalName } |
Select-Object Name,SamAccountName,LastLogonDate |
Export-Csv .\suspect-stale-service-accounts.csv -NoType
```
4) Prove recent activity on the Entra plane (service principals)
For Microsoft Entra service principals and app registrations, track four things: sign-in activity, credential expiry, ownership, and “unused app” signals. Export audit data to your SIEM; tenant retention may be short for service principal sign-ins.
# Requires: Install-Module Microsoft.Graph -Scope CurrentUser
```
Import-Module Microsoft.Graph -MinimumVersion 2.0
Connect-MgGraph -Scopes "AuditLog.Read.All","Application.Read.All","Policy.Read.All","Directory.Read.All"
# Ownerless service principals
$allSp = Get-MgServicePrincipal -All
$ownerless = foreach ($sp in $allSp) {
$owners = Get-MgServicePrincipalOwner -ServicePrincipalId $sp.Id -ErrorAction SilentlyContinue
if (-not $owners) { $sp }
}
$ownerless | Select DisplayName, AppId, Id | Export-Csv .\ownerless-serviceprincipals.csv -NoType
# Expiring secrets/certs in next 30 days
$soon = (Get-Date).AddDays(30)
$expiring = $allSp | ForEach-Object {
$*.PasswordCredentials + $*.KeyCredentials |
Where-Object { $*.EndDateTime -and $*.EndDateTime -lt $soon } |
Select-Object @{n='ServicePrincipal';e={$*.AdditionalProperties['displayName']}},
@{n='ObjectId';e={$*.AdditionalProperties['id']}},
EndDateTime
}
$expiring | Export-Csv .\expiring-sp-credentials.csv -NoType
# Recent sign-ins by appId (last 30 days)
$appId = ""
Get-MgAuditLogSignIn -Filter "appId eq '$appId'" -All |
Sort-Object CreatedDateTime -Descending |
Select-Object AppDisplayName, CreatedDateTime, IPAddress, ResourceDisplayName |
Export-Csv .\sp-signins-$appId.csv -NoType
```
5) Prefer gMSA where possible, document exceptions
If a Windows service, IIS app pool, or scheduled task can run under gMSA, switch it. Password rotation becomes automatic, SPN management simplifies, and your detection code shrinks. Keep a register of approved exceptions with justification and review them quarterly.
Helpful primer: configure gMSA for Defender for Identity.
6) Compute a risk score and triage queue
Not all stale accounts are equal. Compute a weighted score and sort descending. Suggested dimensions:
- Privilege: domain or local admin, high-impact roles, sensitive group membership.
- Delegation: unconstrained or broad constrained delegation; presence of sensitive SPNs.
- Password posture:
DONT_EXPIRE_PASSWORDset;pwdLastSetolder than one year. - Usage: no sign-ins, tickets, or app logs within your calibrated window.
- Ownerlessness: no
managedBy, no ticket, no sponsor. - Surface: reachable from outside; used by critical apps or in cross-forest trusts.
7) Certify and decommission safely
- Notify owners (or candidate owners) with evidence: last activity, where used, privileges.
- Stage disable: remove high privileges; deny interactive logon; disable delegation; leave enabled a few days.
- Observe: SIEM for failures; app teams confirm no impact.
- Disable the account.
- Retire: delete after retention window; archive attributes and notes.
Entra access reviews provide a first-class analog for human users and guests. Mirror the same cadence for non-human identities using your exports and ticketing system.
Field-tested patterns you’ll actually use
# Accounts with SPN but no logon in 120 days
```
$cut = (Get-Date).AddDays(-120)
Get-ADUser -LDAPFilter "(servicePrincipalName=*)" -Properties lastLogonTimestamp, userAccountControl |
Where-Object {
$*.Enabled -and
([DateTime]::FromFileTime($*.lastLogonTimestamp)) -lt $cut
} |
Select SamAccountName,
@{n='LastLogonAgeDays';e={ [int]((Get-Date) - [DateTime]::FromFileTime($*.lastLogonTimestamp)).TotalDays }},
@{n='PwdNeverExpires';e={($*.userAccountControl -band 0x10000) -ne 0}}
# Non-gMSA service accounts with PasswordNeverExpires
Get-ADUser -LDAPFilter "(&(objectClass=user)(servicePrincipalName=*)(!(objectClass=msDS-GroupManagedServiceAccount)))" `
-Properties userAccountControl |
Where-Object { $*.Enabled -and (($*.userAccountControl -band 0x10000) -ne 0) } |
Select SamAccountName
# Unconstrained or risky delegation
Get-ADUser -LDAPFilter "(&(objectCategory=Person)(objectClass=User))" -Properties userAccountControl, msDS-AllowedToDelegateTo |
Where-Object {
(($*.userAccountControl -band 0x80000) -ne 0) -or
$*.msDS_AllowedToDelegateTo
} |
Select SamAccountName, msDS_AllowedToDelegateTo
```
# Entra: ownerless, expiring, unused
```
$spAll = Get-MgServicePrincipal -All
# Ownerless
$ownerless = $spAll | Where-Object {
(Get-MgServicePrincipalOwner -ServicePrincipalId $_.Id -ErrorAction SilentlyContinue).Count -eq 0
}
$ownerless | Select DisplayName,AppId,Id | Export-Csv .\ownerless.csv -NoType
# Expiring secrets/certs in < 45 days
$soon = (Get-Date).AddDays(45)
$expiring = foreach ($sp in $spAll) {
foreach ($c in @($sp.PasswordCredentials + $sp.KeyCredentials)) {
if ($c.EndDateTime -and $c.EndDateTime -lt $soon) {
[pscustomobject]@{SP=$sp.DisplayName; Type=($c.GetType().Name); Ends=$c.EndDateTime; SPId=$sp.Id}
}
}
}
$expiring | Sort Ends | Format-Table -Auto
# Sign-ins grouped by resource (quick activity shape)
Get-MgAuditLogSignIn -Filter "appDisplayName eq 'MyAutomationApp'" -All |
Group-Object ResourceDisplayName |
Select Name,Count
Inherent tendencies you must design around
- Privilege drifts upward. Emergency fixes grant extra rights that rarely get revoked. Penalize stale privileges in your score.
- Ownership decays. Turnover and vendor churn make owner mappings stale. Add ownership attestation to quarterly reviews.
- Batch jobs lie low. Scheduled services can run without interactive sign-ins. Look for service tickets and app logs, not just user sign-ins.
- Legacy protocols persist. NTLM and basic auth paths can keep accepting old credentials. Enforce Kerberos and modern app auth; deprecate basic auth.
- Audit data ages out. Export service principal sign-ins to SIEM daily so audits and cleanups have evidence.
Mental models experts use
- Four-axis health. Owner × Purpose × Path × Proof. Any zero multiplies risk.
- Replace human memory with machine schedules. Password rotation, access reviews, and audit exports must be automated, not remembered.
- Managed-by-default. If gMSA or workload identities aren’t the default for new services, drift is guaranteed.
- Evidence-first decommissioning. No breakage when you show proof of non-use from multiple planes (AD, Entra, app).
Subtle pitfalls, risks, and correctives
- Mistaking “no user sign-in” for “no service use.” Cross-check KDC tickets and app logs; correlate with SPNs.
- Purging before owners can react. Always stage disable with a short observation window.
- Ignoring expiring secrets. Query Graph for
PasswordCredentials/KeyCredentialsend dates and renew ahead of time. - Over-trusting
lastLogonTimestamp. Treat it as a hint; understand replication cadence; augment with per-DC metrics. - Letting audit data expire. Export service principal sign-ins to SIEM nightly.
Expert essentials checklist
- [ ] Every service identity has a current owner and purpose.
- [ ] gMSA or workload identities used wherever possible; exceptions documented.
- [ ] AD & Entra activity proofs exported to SIEM (tickets and sign-ins).
- [ ] Expiring credentials caught and rotated ahead of time.
- [ ] Quarterly access reviews mirrored for non-human identities.
- [ ] Staged decommissioning with rollback window and evidence pack.
Applications, consequences, and what’s next
Compliance without the drama. Auditors are now laser-focused on identity governance for non-human identities. Demonstrating owner mapping, review cadence, and expiring-credential coverage closes entire finding categories.
Operational clarity. Regular ownerless/stale discovery accelerates modernization. Teams refactor toward managed identities, gMSA, and fewer shared secrets. Start with the most sensitive services; the value compounds.
Threat-led tuning. Treat the top 10% risk-scored identities as hunt leads. Look for anomalous source IPs, outdated basic auth, unexpected SPNs, and surprise delegation edges.
Cloud-first future. Expect stronger native signals in Entra (unused apps, expiring credentials, over-privileged roles) and tighter integrations across ITDR and PAM. Keep folding these into your score.
Key takeaways and next steps
- “Stale” and “orphaned” are different failure modes. Measure both.
- Trust multiple planes of evidence: directory timestamps, tickets/sign-ins, and application logs.
- Prefer managed identities (gMSA/workload identities) to reduce drift and false positives.
- Use Entra recommendations and access reviews as built-in signals for non-human identity health.
- Automate ownership attestation and staged decommissioning to avoid outages.
Call to action: Want a jump-start? Grab our Service Accounts Management Tool and run the ownerless + expiring + inactive triad this week. Subscribe to our deep-dive series for the reference pack with scripts, KQL, and report templates—or reach out if you want this converted into a printable checklist or an Elementor-ready HTML block.
