Systems Admin

Remove Orphaned SIDs with PowerShell

Part of pathway: Windows Server Administration

Open the Security tab of any AD object that has been live for a few years and you will eventually find an ACL entry that lists the principal as a raw SID — S-1-5-21-901-2456-1110 — with no name beside it. That is an orphaned SID: an access control entry whose underlying user, group, or computer object has been deleted, leaving the permission attached to the parent object but unable to resolve back to anything human-readable.

Orphaned SIDs are a low-grade hygiene problem most of the time. They do not break access control (a deleted account cannot use the permission) and they do not consume meaningful disk space (one ACE is a few hundred bytes). What they do is junk up your security audits, complicate compliance reports, and occasionally cause permission errors when something tries to enumerate or copy ACLs that contain unresolvable principals.

Walking thousands of OUs and objects to remove these by hand is impractical. The right tool is a PowerShell script that recurses through Active Directory, identifies every ACE whose IdentityReference is a domain-prefixed SID that no longer resolves, and either lists them or strips them out. This article shows that script and walks the two-pass workflow you should always run: list first, remove second.

What an Orphaned SID Actually Is

A SID is the immutable identifier Windows assigns to every security principal. Inside an Access Control List, every Access Control Entry references a principal by its SID, not its name — the human-readable name you see in the GUI is resolved on the fly by looking up the SID against the directory.

When you delete a user, group, or computer object, Active Directory removes the directory entry but not the references to it in every ACL across every object. Each of those ACEs is now an orphan: the SID is still there, but it no longer resolves to anything. The GUI shows the raw SID instead of the friendly name; tools that scan the directory occasionally throw errors when they hit one.

An orphaned SID is not a security risk on its own — the underlying principal is gone, so nothing can authenticate as it. But it is a clear signal that the directory has not been cleaned up, and it is worth removing as part of a regular audit.

Setup

On the domain controller (or any RSAT-equipped workstation logged in as a domain admin), create two folders on the C: drive:

New-Item -ItemType Directory -Path "C:\scripts" -Force | Out-Null
New-Item -ItemType Directory -Path "C:\temp"    -Force | Out-Null

Save the script below as C:\scripts\RemoveOrphanedSID-AD.ps1. The transcript log writes to C:\temp\RemoveOrphanedSID-AD.txt; that file is the audit trail you should keep with your change-control evidence.

The Script

The script scans Active Directory objects for ACEs whose IdentityReference begins with the domain SID prefix and either lists them (default) or removes them (with -Remove). A -WhatIf switch lets you do a dry-run of the remove pass without committing changes.

<#
    .SYNOPSIS
    Removes or lists orphaned SIDs from Active Directory objects.

    .DESCRIPTION
    This script scans Active Directory objects for access control entries
    (ACEs) that reference SIDs which no longer exist in the domain. It
    can either just list these orphaned SIDs or remove them from the
    ACLs, based on the parameters provided.

    .PARAMETER Filter
    Specifies the starting point for the scan. Use "All" for the entire
    forest or provide a specific DN like "OU=Users,DC=example,DC=com".

    .PARAMETER Remove
    If specified, the script will remove the orphaned SIDs from the ACLs.
    Without this switch, it only shows them.

    .PARAMETER WhatIf
    When used with -Remove, shows what would happen if the script were
    to run without actually making changes.

    .EXAMPLE
    .\RemoveOrphanedSID-AD.ps1 -Filter "All"
    Scans the entire forest for orphaned SIDs without removing them.

    .EXAMPLE
    .\RemoveOrphanedSID-AD.ps1 -Filter "OU=Users,DC=example,DC=com" -Remove
    Removes orphaned SIDs from the specified OU.

    .EXAMPLE
    .\RemoveOrphanedSID-AD.ps1 -Filter "OU=Users,DC=example,DC=com" -Remove -WhatIf
    Shows what would be changed without actually altering the ACLs.
#>

param (
    [Parameter(Mandatory = $true)][string]$Filter,
    [switch]$Remove,
    [switch]$WhatIf
)

$Forest      = Get-ADRootDSE
$ForestName  = $Forest.rootDomainNamingContext
$domsid      = (Get-ADDomain -Identity $ForestName).DomainSID.ToString()

$Logs = "C:\temp\RemoveOrphanedSID-AD.txt"
Start-Transcript -Path $Logs -Append -Force

if ($Filter -eq "All") {
    $Folder = $ForestName
    Write-Host "Listing all objects in the forest: $ForestName" -ForegroundColor Cyan
}
else {
    $Folder = $Filter
    Write-Host "Analyzing the following object: $Folder" -ForegroundColor Cyan
}

function RemovePerms {
    param ([string]$fold)
    $fName       = $fold
    Write-Host $fName

    $acl         = Get-ACL "AD:$fName"
    $modified    = $false
    $previousSID = ""

    foreach ($ace in $acl.Access) {
        if ($ace.IdentityReference.Value -like "$domsid*") {
            $sid = $ace.IdentityReference.Value
            if ($previousSID -ne $sid) {
                Write-Host "Orphaned SID $sid on $fName" -ForegroundColor Yellow
                $previousSID = $sid
            }
            if ($Remove -and -not $WhatIf) {
                $acl.RemoveAccessRuleSpecific($ace)
                $modified = $true
            }
            elseif ($Remove -and $WhatIf) {
                Write-Host "Would remove orphaned SID $sid on $fName" -ForegroundColor Green
            }
        }
    }
    if ($modified -and -not $WhatIf) {
        Set-ACL -Path "AD:$fName" -AclObject $acl
        Write-Host "Orphaned SID removed on $fName" -ForegroundColor Red
    }
}

function RecurseFolder {
    param ([string]$fold)
    $folders = Get-ADObject -LDAPFilter "(objectClass=*)" -SearchBase $fold -SearchScope OneLevel
    foreach ($entry in $folders) {
        $dn = $entry.DistinguishedName
        RemovePerms -fold $dn
    }
    foreach ($entry in $folders) {
        RecurseFolder -fold $entry.DistinguishedName
    }
}

RemovePerms   -fold $Folder
RecurseFolder -fold $Folder

Stop-Transcript
Source code of the RemoveOrphanedSID-AD.ps1 script in a code editor
The script RemoveOrphanedSID-AD.ps1 — comment-based help, two functions, and a recursive walk over the AD tree.

The script does four things worth understanding before running it:

  • It pulls the domain SID prefix from Get-ADDomain. Every principal in the domain has a SID that starts with this prefix; a SID with this prefix that no longer resolves is, by definition, an orphaned SID for one of your deleted principals (not a built-in or external trust SID).
  • It uses Get-ACL "AD:$fName" — the AD: PowerShell drive provider. That is the right way to read AD object ACLs from PowerShell; it returns a real System.DirectoryServices.ActiveDirectorySecurity object that you can manipulate and write back with Set-ACL.
  • It uses RemoveAccessRuleSpecific rather than RemoveAccessRule. The Specific variant removes the exact ACE, not all ACEs that match the rule. This matters because some objects have multiple ACEs for the same orphaned SID (different access masks); the Specific form removes one at a time, in a loop, until they are all gone.
  • It logs everything via Start-Transcript. The transcript file is the audit trail and is the artifact your change-control process will want to see.

The Two-Pass Workflow

Always do two passes: list, then remove. The list pass surfaces what is actually there; the remove pass commits the change. Doing them separately gives you a chance to spot anything weird (e.g. a SID prefix that does not look like one of your deleted accounts) before stripping the ACL.

Pass 1 — List All Orphaned SIDs

Open Windows PowerShell as administrator. Change to the scripts directory:

cd C:\scripts

Run the script with -Filter All — this scans the entire forest from the root downward and lists every orphaned SID it finds. No changes are made.

.\RemoveOrphanedSID-AD.ps1 -Filter "All"
PowerShell output listing orphaned SIDs found in Active Directory
Running the script with -Filter All walks every AD object and prints orphaned SIDs in yellow. Nothing is changed yet.

The script writes the path of every AD object as it walks, and prints orphaned SIDs in yellow. The same output is written to the transcript log:

Contents of RemoveOrphanedSID-AD.txt transcript file
Same output mirrored to C:\\temp\\RemoveOrphanedSID-AD.txt by Start-Transcript. Useful for change-control evidence.

Read the log carefully before continuing. If you see SIDs that look unusual — very short prefixes, well-known SIDs (S-1-5-32-* for built-in groups), or trust SIDs from another domain — those are not what this script is meant to handle and you should narrow the filter to a specific OU before running with -Remove.

If you want to limit the scan to one OU first (recommended for the very first run on a real domain):

.\RemoveOrphanedSID-AD.ps1 -Filter "OU=Users,DC=corp,DC=local"

Pass 2 — Remove the Orphaned SIDs

Once the list looks reasonable, re-run with -Remove to commit the changes:

.\RemoveOrphanedSID-AD.ps1 -Filter "All" -Remove
PowerShell output showing orphaned SIDs being removed from AD ACLs
The same scan with -Remove — the script now strips each orphan from its parent ACL. Removed entries are shown in red.

If you want to do a dry-run of the remove pass first, add -WhatIf. The script then walks every object the way it would for a real removal but prints Would remove orphaned SID… instead of actually changing the ACL:

.\RemoveOrphanedSID-AD.ps1 -Filter "All" -Remove -WhatIf

The transcript log captures the change set, including which object each removal touched:

Contents of the transcript log after the remove operation
The transcript captures every removal — this is the audit trail you keep.

Verify the Cleanup

Open the Security tab on any AD object that previously had orphaned SIDs (Active Directory Users and Computers → right-click → PropertiesSecurity tab). The numeric SIDs are gone; only resolvable principals remain.

Security tab of an AD object showing entries before and after orphaned SIDs were removed
Before / after view of the Security tab on an AD object. The numeric SIDs are gone; only resolvable principals remain.

For programmatic verification, re-run the list pass and confirm no orphaned SIDs are reported:

.\RemoveOrphanedSID-AD.ps1 -Filter "All"

The output should now be just the AD walk with no yellow lines.

Common Pitfalls

  • Running -Remove without running the list pass first. The list pass shows you what the script will touch. Skipping it is the fastest way to remove a SID you did not actually want to remove (a misconfigured permission attached to a still-valid trust principal, for example).
  • Running on the entire forest before testing on one OU. The first run should always be scoped to a single OU you understand. Once that works, expand the filter.
  • Forgetting that this only handles SIDs from your own domain. The script filters by the domain SID prefix, so SIDs from external trusts, well-known groups, or built-in principals are intentionally skipped. If you need to clean those, that is a different problem and a different script.
  • No backup of the directory. Run the script during a maintenance window with a recent system-state backup of at least one DC. The change is reversible (you can re-add the principal and the ACL is gone, so you would need to re-grant permissions), but a snapshot lets you revert quickly if something goes wrong.
  • Confusing this with file-system ACL cleanup. This script targets AD object ACLs (the security on the directory entries themselves, like the OU permissions). For file shares with orphaned SIDs in NTFS ACLs, you need a different script that walks the file system, not the directory.

Conclusion

Orphaned SIDs are inevitable in any AD that has been around long enough for accounts to be created and deleted. Removing them is not strictly required for security, but it makes audits cleaner, reports more accurate, and reduces the noise that hides genuine permission problems. The two-pass workflow — list first, remove second — gives you a chance to review what the script will change before it changes anything.

Run RemoveOrphanedSID-AD.ps1 as part of your quarterly AD hygiene routine. Keep the transcript logs with your change-control evidence. The directory stays clean, the audits stay quiet, and the next admin who opens the Security tab on an OU sees names instead of raw SIDs.

Leave a Reply