TopNet247Independent notes for Windows admins

How-to · Active Directory Management & Auditing

Find and clean up stale computer accounts in Active Directory

A reversible disable, move and delete routine for stale AD computer objects with RSAT PowerShell, including the timestamp caveats and BitLocker trap.

Most domains carry dead weight. Laptops were reimaged under new names, lab VMs were deleted from the hypervisor but never from AD, and a whole branch office closed years ago. Stale computer objects make reports lie, clutter the OUs where GPOs apply, and give you “enabled” accounts nobody is watching. Cleaning them up is simple, but it has to be done in the right order. Deleting too early can cost you BitLocker recovery keys or break a cluster. This guide uses a three-stage approach (report, disable and move, delete later) with the ActiveDirectory module from RSAT.

What you need

How “inactive” is measured, and why it’s fuzzy

AD has two last-logon attributes. lastLogon is precise but isn’t replicated: each DC keeps its own. lastLogonTimestamp is replicated, but on purpose it only updates when the stored value is roughly 9–14 days old. That’s the default msDS-LogonTimeSyncInterval of 14 days, minus a random offset. Search-ADAccount -AccountInactive and the LastLogonDate property both use the replicated value. That makes them fine for “not seen in 90 days” and useless for “not seen since Tuesday”.

A useful second signal is PasswordLastSet. Domain members change their machine account password every 30 days by default. A computer whose password hasn’t changed in 90+ days probably hasn’t been talking to a DC.

Step 1 — Report (change nothing yet)

Import-Module ActiveDirectory
$cutoff = (Get-Date).AddDays(-90)

$stale = Get-ADComputer -Filter { Enabled -eq $true -and LastLogonTimeStamp -lt $cutoff } `
    -Properties LastLogonDate, PasswordLastSet, OperatingSystem, Description, DistinguishedName |
    Where-Object { $_.PasswordLastSet -lt $cutoff }

$stale | Select-Object Name, OperatingSystem, LastLogonDate, PasswordLastSet, DistinguishedName |
    Sort-Object LastLogonDate |
    Export-Csv C:\Admin\stale-computers.csv -NoTypeInformation

The same query with the purpose-built cmdlet, if you prefer:

Search-ADAccount -AccountInactive -ComputersOnly -TimeSpan 90.00:00:00 `
    -SearchBase 'OU=Workstations,DC=corp,DC=example,DC=com'

Computers that have never logged on have an empty LastLogonTimeStamp, so the first filter skips them. List them separately by whenCreated. A PC created three days ago and still in the build room isn’t stale.

Step 2 — Remove the exceptions

Send the CSV to the people who know the estate before you touch anything. Take out:

  1. Cluster name objects and virtual computer objects (failover clusters, SQL availability groups). Their “logon” pattern looks idle but they are in use.
  2. Anything under a Domain Controllers or server OU, until a server owner confirms it.
  3. Devices known to be on long leave: spare laptops in a cupboard, shipboard or seasonal-site machines.
  4. Objects with BitLocker recovery information you might still need (see the mistakes section).

Step 3 — Disable, stamp and move

Disabling is reversible within seconds. The description stamp tells the next admin what happened and when:

$approved = Import-Csv C:\Admin\stale-computers-approved.csv
$target   = 'OU=Disabled Computers,DC=corp,DC=example,DC=com'
$stamp    = "Disabled $(Get-Date -Format yyyy-MM-dd) by $env:USERNAME - stale cleanup"

foreach ($c in $approved) {
    $obj = Get-ADComputer -Identity $c.Name
    Disable-ADAccount -Identity $obj -WhatIf
    Set-ADComputer    -Identity $obj -Description $stamp -WhatIf
    Move-ADObject     -Identity $obj.DistinguishedName -TargetPath $target -WhatIf
}

Run it with -WhatIf first, read the output, then remove the switch. If someone calls to say their PC “lost trust”, re-enable the account and move it back. The machine password hasn’t changed, so the secure channel usually recovers after a reboot.

Step 4 — Delete after a holding period

Wait a fixed period, 30 to 60 days is common, then delete what’s still in quarantine:

Get-ADComputer -SearchBase 'OU=Disabled Computers,DC=corp,DC=example,DC=com' -Filter { Enabled -eq $false } -Properties whenChanged |
    Where-Object { $_.whenChanged -lt (Get-Date).AddDays(-45) } |
    ForEach-Object { Remove-ADObject -Identity $_.DistinguishedName -Recursive -Confirm:$false -WhatIf }

Use Remove-ADObject -Recursive rather than Remove-ADComputer. Computer objects often have child objects (BitLocker recovery entries, service connection points), and Remove-ADComputer refuses to delete a non-leaf object.

Common mistakes

Tools that make this a recurring job

RSAT and PowerShell cost nothing and cover all of this; see our RSAT review. If you’d rather have scheduled stale-object reports and cleanup driven from a web console with approvals, ManageEngine ADManager Plus lists stale-account cleanup and inactive-object reports among its features. To keep an independent record of who disabled or deleted what, pair either approach with a change-auditing product such as Netwrix Auditor. The difference between those two products is covered in ADManager Plus vs Netwrix Auditor, and the wider field is in Active Directory Management & Auditing.

Tool used in this how-to