Active Directory: audit and clean computer account ownership
Table of Contents
No matching heading
Most Active Directory hardening reviews cover the same objects: users, groups, GPO, sometimes the delegations on a few OUs. Computer objects are treated as inventory. They are security principals, with an owner and a DACL of their own.
In a default domain, any authenticated user can create up to 10 computer objects. Whoever creates an object becomes its owner. Ownership is not a label. It is a permission that survives every ACL cleanup done on the object.
This article covers how to find those objects, why the owner matters more than the ACE you can see, and how to reset them without breaking a tiering model.
Setting the quota to 0 is only half the job
ms-DS-MachineAccountQuota is a domain level attribute. Its long standing default is 10: any authenticated user can create up to 10 computer objects without any specific delegation. Microsoft documents the behaviour in Default limit to number of workstations a user can join to the domain.
Two properties of this quota are worth keeping in mind:
- It is a creation counter, not a permission model. It is never decremented when an administrator deletes a computer object later on.
- It says nothing about what the user can do with the objects afterwards.
The usual advice is to set the quota to 0 and delegate the Create Computer Objects permission to the accounts that need it. That advice is correct but incomplete. Setting the quota to 0 today does nothing about the objects created yesterday. Those objects still carry their original owner.
Two AD CS attacks start with the same step: create a machine account through the machine account quota, which is open by default. A machine account is a valid domain principal. That is all the attacker needs.
- Certifried, CVE-2022-26923, May 2022: the attacker creates a computer object and sets its
dNSHostNameto the one of a domain controller, then enrols for a certificate as that domain controller. Fixed by KB5014754 and strong certificate mapping. - Certighost, CVE-2026-54121, July 2026: the attacker abuses the certificate enrollment chase fallback. The request itself supplies two attributes.
cdcis the Client DC to contact.rmdis the Remote Domain object to resolve. The certification authority followed that host over SMB and LDAP without checking it was a real domain controller. Rogue LSA and LDAP services could then hand back theobjectSidanddNSHostNameof a domain controller. The certificate is used with PKINIT, then DCSync. Fixed in the July 2026 updates, which validate the chase target before the certification authority contacts it.
Patching fixes both. The quota is what you can change today, without waiting for a maintenance window. There is no good reason to leave it above 0. The machines that must join the domain go through a delegation. Set it to 0.
# Read the current value
Get-ADObject -Identity (Get-ADDomain).DistinguishedName -Properties 'ms-DS-MachineAccountQuota' | Select-Object -ExpandProperty 'ms-DS-MachineAccountQuota'
# Set it to 0
Set-ADDomain -Identity (Get-ADDomain).DistinguishedName -Replace @{'ms-DS-MachineAccountQuota' = '0'}Create Computer Objects on the target OU to the accounts that join machines. Then set the quota to 0. In the other order the deployment process can no longer join anything, and whoever gets the ticket raises the quota again to unblock it.The objects created before that change are the part nobody audits.
Two traces of the creator
There are two independent traces of who created a computer object. Most audit scripts only look at the first one.
| Trace | Populated when | Missing when |
|---|---|---|
ms-DS-CreatorSID |
A regular user creates the object through the machine account quota | The creator was a Domain Admin, or held a delegated Create Computer Objects permission at creation time |
Object owner in nTSecurityDescriptor |
Almost always: the creator becomes the owner | Never missing. The owner is always present in the security descriptor. It can be a SID that no longer resolves, or a group rather than an account when the creator was an administrator. |
Two consequences follow.
Auditing only ms-DS-CreatorSID misses every computer created by a delegated account. In a tiered model those are the Tier 1 and Tier 2 deployment accounts, the ones you care about.
Auditing only the owner misses nothing but returns a lot of legitimate noise. Everything created by an administrator shows a group as owner. Filter the output rather than read the raw list.
ms-DS-CreatorSID was never an audit feature. It exists so the domain can count objects against the quota. It is a by-product of that mechanism. As soon as the quota is not the mechanism used, the attribute stays empty.
Why the owner matters more than the ACE
In the Windows security model, the owner of an object implicitly holds WRITE_DAC and READ_CONTROL. The owner can rewrite the DACL and grant themselves full control, whatever the current permissions say.
In practice, user X owns computer object Y means user X has permanent control of Y. Removing their explicit ACE changes nothing while they remain the owner. They can put it back.
Control over a computer object enables several known primitives:
- writing
msDS-AllowedToActOnBehalfOfOtherIdentity, which is resource based constrained delegation; - writing
msDS-KeyCredentialLink, the Shadow Credentials technique, when the domain supports PKINIT; - reading the LAPS password. With legacy LAPS or Windows LAPS in clear text mode the ACL is enough. Windows LAPS encrypts by default, and only the authorized decryptor, Domain Admins unless configured otherwise, can read it;
- resetting the machine account password;
- editing
servicePrincipalNameandsAMAccountName, the primitive behind the noPac family, CVE-2021-42278 and CVE-2021-42287, November 2021.
The exploitation path is not the point here. The owner is what makes all of them reachable. An ACL review does not show the owner.
Match on the SID, never on the name
One practical trap before any script.
Domain Admins is Admins du domaine on a French domain and Domänen-Admins on a German one. Filtering on the display name works until the script runs on another forest or on a domain installed in another language.
The SID is also the only thing left when the account has been deleted or lives in an unreachable trusted domain. The SID is always readable. It is the translation to a name that fails.
Two references you will use constantly:
Domain Adminsis<domainSID>-512BUILTIN\Administratorsis the well knownS-1-5-32-544
The tooling
Both functions below live in the PSADDS module, which I wrote for several recurring Active Directory needs. They chain: audit, filter, remediate.
Get-ADComputerJoinedByUser
Returns one object per computer, with CreatorName and CreatorSID on one side, OwnerName and OwnerSID on the other.
The parameters that matter:
-SearchByacceptsAll(default, both columns, nothing filtered out),CreatorSID(quota based creations only) orOwner(owner only, which is what covers delegated creations).-Identityaccepts either a computer, in which case you get its creator and owner, or any other principal, in which case you get every computer that principal created. The object class is resolved automatically. Wildcards are supported. The parameter takes pipeline input.-SearchBasescopes the search to an OU. This matters on a large directory.-Servertargets a specific domain controller or domain.
-SearchBy CreatorSID filters server side with an LDAP filter, (&(objectClass=computer)(mS-DS-CreatorSID=*)) and does not retrieve security descriptors. It is by far the fastest mode. The other two modes cannot filter server side. The owner lives inside the security descriptor and can only be evaluated client side.
Reset-ADComputerAccountSecurity
Restores the default owner, the default permissions, or both.
-Scope Ownersets the owner back toDomain Admins. Override with-Owner.-Scope Permissionsreplaces the DACL with the default security descriptor defined in the schema for the computer class, read fromdefaultSecurityDescriptoronCN=Computer,<schemaNC>. The reference is therefore the one your own forest declares, not a hardcoded SDDL.-Scope All, the default, does both. Permissions first, then the owner, so that the owner change cannot be overwritten by the DACL write.
The function is idempotent by default. The owner is compared by SID. The explicit ACE are compared to the schema default through a normalized fingerprint: inherited ACE excluded, identities as SID, entries sorted. Compliant objects are skipped. -Force writes anyway.
-Simulation, aliased -DryRun, reports everything and writes nothing, including no event log entry. -WhatIf and -Confirm are supported as well. ConfirmImpact is set to High.
Operations are traced in the Application event log under a configurable source, -EventLogSource, default ComputerAccountCleanup. An empty string disables the logging.
Each computer produces a result object with a cumulative ResultCode: 0 for success or nothing to do, 1 if the owner failed, 10 if the computer could not be read, 100 if the permissions failed.
One warning before running this near production. Resetting the permissions removes every explicit ACE on the object, including tier model delegations applied directly on the computer account. ACE inherited from the parent OU are not stored on the object and are preserved, unless inheritance is blocked on that object. In a tiered environment -Scope Owner is the safe operation. -Scope Permissions needs a review object by object first, which is what -Simulation is for.
Ready to use examples
Full inventory, both traces, nothing filtered:
Import-Module PSADDS
Get-ADComputerJoinedByUserWho is close to burning their quota:
Get-ADComputerJoinedByUser -SearchBy CreatorSID | Group-Object CreatorSID | Sort-Object Count -Descending | Select-Object Count, @{Name='CreatorSID';Expression={$_.Name}}, @{Name='CreatorName';Expression={$_.Group[0].CreatorName}}The interesting population, computers whose owner is neither Domain Admins nor BUILTIN\Administrators. Note the filter on the SID rather than on the name, which keeps this working on any domain language:
$domainSID = (Get-ADDomain).DomainSID.Value
Get-ADComputerJoinedByUser -SearchBy Owner | Where-Object { $_.OwnerSID.Value -notin @("$domainSID-512", 'S-1-5-32-544') }The delegated creations, the ones invisible to a ms-DS-CreatorSID only audit:
Get-ADComputerJoinedByUser | Where-Object { $null -eq $_.CreatorSID }
Everything a given account created, whichever trace recorded it:
Get-ADComputerJoinedByUser -Identity 'jdoe'
Get-ADComputerJoinedByUser -Identity 'Tier1-Deploy*'
Get-ADGroupMember -Identity 'Helpdesk' | Get-ADComputerJoinedByUser -SearchBy Owner
Audit to remediation in a single pipeline. Dry run first:
$domainSID = (Get-ADDomain).DomainSID.Value
Get-ADComputerJoinedByUser -SearchBy Owner | Where-Object { $_.OwnerSID.Value -notin @("$domainSID-512", 'S-1-5-32-544') } | Reset-ADComputerAccountSecurity -Scope Owner -SimulationDrop -Simulation to apply. Keep only the failures:
Get-ADComputerJoinedByUser -SearchBy Owner | Where-Object { $_.OwnerSID.Value -notin @("$domainSID-512", 'S-1-5-32-544') } | Reset-ADComputerAccountSecurity -Scope Owner | Where-Object { $_.ResultCode -ne 0 }Export for the report:
Get-ADComputerJoinedByUser | Export-Csv -Path 'C:\temp\ComputersJoinedByUser.csv' -NoTypeInformation -Encoding UTF8 -Delimiter ';'Reading the results
The first run will return more rows than you expect. Most of them are normal.
Owner is Domain Admins or BUILTIN\Administrators. Normal. The object was created by an administrator. On a healthy domain this is the bulk of the output.
Owner is a Tier 0, Tier 1 or Tier 2 deployment account. CreatorSID is empty. The object was created through a delegation. This is not a finding by itself, provided the account is the one your deployment process legitimately uses. It becomes one when that account is reachable from a lower tier.
CreatorSID is populated with a standard user account. The machine was created through the machine account quota, whatever its current value. Worth investigating: was it a legitimate self service enrollment, or a machine nobody planned for?
Owner cannot be resolved, because the object was deleted or lives in an unreachable trusted domain. The SID is still there and is what you report. Stale ownership by a deleted account is a cleanup item, not an active risk.
If the domain uses a tiering model, expect to see computers created by the accounts holding tiering permissions. That is by design.
Remediation plan
- Audit first and export. Get the owners of the outliers reviewed by whoever runs the deployment process. Do not remediate blind.
- Fix ownership, with
-Scope Owner. This is the operation that actually removes the standing control. It is low risk and reversible. It does not touch delegations. - Only then, and only where a review says the explicit ACE are illegitimate, consider
-Scope Permissions. Always run-Simulation -Verbosefirst: it prints the current DACL and the schema default SDDL side by side. - Prevent the recurrence. Set
ms-DS-MachineAccountQuotato 0 and delegateCreate Computer Objectson the target OU to the accounts that need it. Note that a delegated creation still leaves the delegated account as owner, so the audit stays relevant afterwards. Some organizations pre-stage computer objects instead, which sidesteps the problem entirely. - Re-run the audit periodically. It is cheap in
-SearchBy CreatorSIDmode.
Takeaway
Setting ms-DS-MachineAccountQuota to 0 is a good control for the future. It does nothing for the past.
The objects created before you tightened the domain are still owned by whoever created them. The owner outlives every ACE you remove. Auditing ms-DS-CreatorSID alone will not show the delegated creations. In a tiered model those are the ones that matter.
Audit both traces. Filter on SID. Fix the owner first. Leave the permissions alone until someone has reviewed them.