Skip to content
10 · Trusts & Forest DesignPart 3 of 4Intermediate

Cleaning up SIDHistory after AD migrations

Find, assess and safely remove sIDHistory left over from domain migrations: dangerous SIDs, ACL re-permissioning, staged removal, rollback limits and detection.

Florian Amette7 min read

sIDHistory exists for one good reason: when a user or group moves from one domain to another, it keeps its old SIDs so that resources still permissioned for the old identity keep working. In practice, migrations finish, source domains are decommissioned, and the old SIDs stay on thousands of objects for a decade. Every one of those values is an extra identity carried in the user's token, and a place where an attacker with enough rights can hide privilege: a SID history value pointing at Domain Admins grants Domain Admins rights without appearing in the group's member list.

The trusts and forest boundaries pillar covers filtering SID history at the trust. This guide covers the objects themselves: finding every sIDHistory value, separating dangerous entries from migration leftovers, re-permissioning resources so old SIDs are no longer needed, removing values in stages, and monitoring so new ones do not appear unnoticed.

Before touching anything, agree on ownership. SID history cleanup crosses identity, file server, application and security teams, and the failure mode is visible to end users. Name one owner for the programme, one contact per application, and a communication plan that tells pilot users what to report and where.

Why leftover SID history is a risk

  • Hidden privilege. Tokens include SID history, so a regular user with the SID of a privileged group in sIDHistory is effectively a member. Group membership reviews, adminCount and most admin reports do not show it.
  • Persistence. Adding SID history normally requires the migration API with Domain Admin rights in the target domain, but an attacker who has already reached Tier 0 can write it directly with tools such as Mimikatz or DSInternals. It survives password resets and is easy to miss during incident response.
  • Token bloat. Users with many old SIDs plus many groups can exceed Kerberos token size limits, causing intermittent authentication failures.
  • Trust exposure. If you keep SID history working across a trust, you have to leave SID filtering relaxed, which weakens the trust itself. See SID filtering and selective authentication.

Measure: inventory every value

PowerShell
$forestSids = (Get-ADForest).Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value }

Get-ADObject -LDAPFilter "(sIDHistory=*)" -Properties sIDHistory, objectClass, samAccountName, whenChanged |
  ForEach-Object {
    $obj = $_
    foreach ($sid in $obj.sIDHistory) {
        $prefix = $sid.AccountDomainSid.Value
        [pscustomobject]@{
            Object       = $obj.samAccountName
            Class        = $obj.objectClass
            HistorySid   = $sid.Value
            SourceDomain = $prefix
            Rid          = [int]($sid.Value.Split('-')[-1])
            Builtin      = $sid.Value.StartsWith('S-1-5-32-')
            SameForest   = $forestSids -contains $prefix
            WhenChanged  = $obj.whenChanged
        }
    }
  } | Export-Csv .\sidhistory-inventory.csv -NoTypeInformation

Run it in every domain of the forest. The CSV is your baseline and the evidence you will need later, since removed values cannot simply be written back.

Group the results by SourceDomain. You will usually see one or two old domain SIDs, one per historic migration, and a count of objects each. Map each prefix to a known source domain from migration records; if the source still exists and a trust is in place, Translate() on a sample SID will resolve it.

Audit: triage by risk

Sort every value into one of three categories.

Critical: investigate as an incident

  • SameForest is True. Legitimate migrations copy SIDs from the source domain into the target domain; a value from your own forest, and especially from the same domain, is not a migration artifact.
  • Rid is below 1000 from any domain, in particular 500, 512, 518 and 519, or Builtin is True, in particular S-1-5-32-544 (Administrators). Builtin SIDs have no domain prefix, so SourceDomain is empty for them. Built-in privileged RIDs and SIDs in SID history are a classic persistence technique.
  • Values on objects whose whenChanged falls long after the last known migration.

Do not just delete these. Capture the object, its metadata (repadmin /showobjmeta gives the originating DC and time of the sIDHistory change), and check your event logs for 4765 and 4766 around that time. Then remove the value and rotate credentials for the account.

Legacy with live dependencies

Values from a source domain that still has resources permissioned with its SIDs: file servers migrated with permissions intact, applications with old-SID ACLs, SQL logins. These need re-permissioning first.

Legacy with no dependencies

Values from a source domain that was decommissioned years ago and whose resources were rebuilt. Usually the majority, and safe to remove after a pilot.

Enforce: re-permission before removal

The goal is to replace every ACE that references an old SID with one referencing the current SID of the same principal. Tools:

  • ADMT security translation (Translate Objects wizard) works if ADMT and the migration database still exist.
  • icacls /substitute replaces one SID with another in NTFS ACLs: icacls D:\Shares /substitute <OldSID> <NewSID> /t /c. Use /save first to keep a restorable copy of the ACLs.
  • Application-level permissions (SQL logins mapped to old SIDs, Exchange mailbox permissions, SharePoint site permissions) need their own tooling.

Find what references old SIDs on file servers before and after translation:

PowerShell
$oldPrefix = "S-1-5-21-1111111111-2222222222-3333333333"   # old source domain SID
Get-ChildItem D:\Shares -Directory -Recurse -ErrorAction SilentlyContinue |
  ForEach-Object {
    $rules = (Get-Acl $_.FullName).GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])
    $hits  = $rules | Where-Object { $_.IdentityReference.Value -like "$oldPrefix-*" }
    if ($hits) { [pscustomobject]@{ Path = $_.FullName; Count = @($hits).Count } }
  }

An ACE for a principal whose only link to the old SID is SID history resolves in the GUI to the current account name, which is why ACL scans must work on raw SIDs rather than display names. Include AD object ACLs too: delegations on OUs sometimes reference migrated groups by old SID. The ACL audit guide covers dumping those.

Enforce: staged removal

Remove in waves, starting with a pilot of IT users, then departments, then groups last (group SID history affects every member).

PowerShell
# Remove all SID history from one object, logging what was removed
$user = Get-ADUser "jdoe" -Properties sIDHistory
$user.sIDHistory | ForEach-Object { "{0};{1}" -f $user.SamAccountName, $_.Value } |
  Add-Content .\sidhistory-removed.log
foreach ($sid in $user.sIDHistory) {
    Set-ADUser $user -Remove @{ sIDHistory = $sid.Value }
}

# Remove only values from a specific old domain, across a pilot group
$oldPrefix = "S-1-5-21-1111111111-2222222222-3333333333"
Get-ADGroupMember "SIDH-Pilot" | Get-ADUser -Properties sIDHistory |
  ForEach-Object {
    $u = $_
    $u.sIDHistory | Where-Object { $_.AccountDomainSid.Value -eq $oldPrefix } |
      ForEach-Object { Set-ADUser $u -Remove @{ sIDHistory = $_.Value } }
  }

For groups, use Set-ADGroup with the same -Remove syntax. Users pick up the change at their next logon, when a new TGT is issued; ask pilot users to sign out and back in, or wait for ticket expiry. Leave each wave in place for a full business cycle, including month-end processes, before moving on.

Rollback is limited. You cannot write sIDHistory back with Set-ADUser -Add. The options are re-running a migration with ADMT (only if the source domain still exists), or an authoritative restore of the affected objects from backup. That is why re-permissioning and pilots come first.

Plan the programme realistically

In a large estate, cleanup takes months, not days, and the order matters:

  1. Week 1: critical findings. Same-forest SIDs and privileged RIDs are handled immediately as security findings, independent of the rest.
  2. Month 1: evidence. Complete the inventory in every domain, map each source domain, and run ACL scans on file servers and application databases. Estimate the number of ACEs to translate per source domain; that number, not the count of objects, drives effort.
  3. Months 2-3: translation. Re-permission file servers first, since they carry most old-SID ACEs, then applications. Keep before-and-after ACL exports.
  4. Months 3-6: removal waves. Pilot, departments, then groups, with at least one month-end between waves.
  5. Finally: the trust. Once no values from a trusted source domain remain, re-enable SID filtering on that trust, or remove the trust entirely if it existed only for the migration.

Track progress with two numbers reported to leadership: objects with sIDHistory remaining, and ACEs still referencing old SIDs. Both should fall to zero; a flat line usually means an application team is blocked and needs help rather than a reminder.

Verify and monitor

PowerShell
# Remaining values, by source domain
Import-Csv .\sidhistory-inventory.csv | Group-Object SourceDomain | Select-Object Name, Count
(Get-ADObject -LDAPFilter "(sIDHistory=*)").Count

# Any same-domain SID history left? Should be zero
$domainSid = (Get-ADDomain).DomainSID.Value
Get-ADObject -LDAPFilter "(sIDHistory=*)" -Properties sIDHistory |
  Where-Object { $_.sIDHistory.AccountDomainSid.Value -contains $domainSid } | Select-Object Name

For ongoing detection, alert on:

  • 4765 (SID History was added to an account) and 4766 (an attempt to add SID History failed). Outside a planned migration, any 4765 is an incident.
  • 5136 on the sIDHistory attribute, which requires Directory Service Changes auditing and a SACL on user and group objects.
  • 4738 and 4742 where the SID History field changes.

Tools such as BloodHound and PingCastle also report SID history pointing at privileged groups; include those checks in your recurring assessment.

What it breaks

  • Access to resources still permissioned with old SIDs: file shares, printers, application roles and SQL logins return access denied for users whose SID history was removed before translation.
  • Group SID history removal affects every member at once; one missed ACL can lock out a whole department.
  • Cross-trust access that silently relied on SID history via a relaxed trust stops working, which is also the point at which you can re-enable filtering.
  • ADMT reruns for rollback need the source domain, trust and migration database, which are often gone.
  • Scripts and reports that resolved old SIDs to names through SID history show raw SIDs instead.

Related reading: the Trusts & Forest Design topic, the auditing and detection pillar for the audit policy behind 4765 and 5136, and attack path management for tracking hidden privilege alongside ACL paths.

Frequently asked questions

Can I restore sIDHistory after removing it?

Not by simply writing the old value back. Active Directory lets administrators remove sIDHistory values but only allows adding them through the migration API used by ADMT and similar tools, which requires the source domain to still exist, or through an authoritative restore of the object from backup. Treat removal as effectively one-way and complete ACL translation and testing before you remove anything.

Which sIDHistory values are the most dangerous?

Any value that resolves to a SID in your own domain or forest, especially one ending in a privileged RID such as 500 (Administrator), 512 (Domain Admins), 518 (Schema Admins), 519 (Enterprise Admins), or the builtin Administrators SID S-1-5-32-544, which has no domain prefix. Legitimate migrations add SIDs from the source domain, never from the target domain itself, so same-domain SID history is either a serious mistake or a persistence backdoor and should be investigated as an incident.

How do I know if anything still uses the old SIDs?

Scan the places that store SIDs in access control lists: NTFS and share permissions on file servers, registry and service ACLs, SQL Server logins, Exchange and SharePoint permissions, and AD object ACLs. Any ACE referencing the old domain's SID prefix still depends on SID history. Once scans show none left and a pilot removal generates no access complaints for a full business cycle, removal is safe.

Cleaning up SIDHistory after AD migrations

Related guides

Object & ACL Security

AdminSDHolder and SDProp: cleanup and monitoring

Baseline the AdminSDHolder ACL, find orphaned adminCount=1 accounts, reset their ACLs safely, trigger SDProp on demand, and alert on AdminSDHolder changes.

Intermediate
Trusts & Forest Design

Hardening AD trusts and forest boundaries

Why the forest — not the domain — is the AD security boundary, and how to lock down trusts with SID filtering, quarantine, and selective authentication.

Intermediate