2.4 Creating & Managing Users, Groups and Organizational Units at Scale
Key Takeaways
- The default CN=Users and CN=Computers objects are containers, not organizational units, so Group Policy Objects cannot be linked to them; redirusr and redircmp redirect new object creation to real OUs.
- New-ADUser creates a disabled account unless -Enabled $true and an -AccountPassword SecureString are supplied in the same command.
- Search-ADAccount surfaces stale, locked-out, disabled, expired and password-expired accounts without writing a custom LDAP filter.
- Alternative UPN suffixes are added at the forest level with Set-ADForest -UPNSuffixes before they can be selected on a user account.
- The Active Directory Administrative Center PowerShell History pane emits the exact cmdlet for any action performed in the GUI, which is the fastest route from a one-off click to a repeatable script.
Creating & Managing Users, Groups and Organizational Units at Scale
The previous sections covered what security principals are and how their scopes behave across domains. This one covers the operational reality: an administrator provisions dozens of accounts from a human-resources export, restructures organizational units so that Group Policy and delegated administration both land where they should, and periodically hunts down the accounts that stopped being used two years ago. Every one of those tasks is testable, and every one of them has a specific tool.
1. Organizational Unit Design: Containers Are Not OUs
An organizational unit (OU) is a directory object designed to hold other objects for two purposes only: Group Policy linkage and delegation of administration. It has nothing to do with permissions on files or membership in groups.
The distinction that trips people up is that a brand-new domain does not place objects in OUs. CN=Users and CN=Computers are containers, a different object class, and they carry a hard limitation:
| Object | Class | GPO can be linked? | Can be renamed or moved? |
|---|---|---|---|
CN=Users, CN=Computers, CN=Builtin | container | No | No |
OU=Corp, OU=Finance | organizationalUnit | Yes | Yes |
OU=Domain Controllers | organizationalUnit | Yes | Should not be moved |
Because a domain join with no target specified drops the computer object into CN=Computers, a freshly joined server receives no OU-linked Group Policy — only domain-linked and site-linked policy. The fix is to redirect the default creation targets, which requires a Windows Server 2003 or higher domain functional level:
:: Redirect the default creation containers to real OUs so GPOs apply to new objects
redircmp "OU=Servers,OU=Corp,DC=corp,DC=contoso,DC=com"
redirusr "OU=Staff,OU=Corp,DC=corp,DC=contoso,DC=com"
Two design patterns dominate: a geographic top level (OU=EMEA, OU=AMER) when administration is delegated by region, and a functional top level (OU=Finance, OU=Engineering) when policy differs by role. Most production forests use a hybrid — geography for delegation, function beneath it for policy. Depth is not free: every additional OU level is another link in the LSDOU processing chain and another place for an inheritance block to hide.
Protecting Objects from Accidental Deletion
Active Directory Users and Computers offers a "Protect object from accidental deletion" checkbox, and Active Directory Administrative Center selects it by default on new OUs. It is not a special flag — it writes an explicit Deny access control entry for Delete and Delete Subtree against Everyone on the object. That is why a Domain Admin who deletes a protected OU gets Access Denied, and why the protection must be removed before a legitimate restructure:
# Find every unprotected OU and protect it
Get-ADOrganizationalUnit -Filter * -Properties ProtectedFromAccidentalDeletion |
Where-Object { -not $_.ProtectedFromAccidentalDeletion } |
Set-ADObject -ProtectedFromAccidentalDeletion $true
# Temporarily unprotect a single OU so it can be moved
Set-ADObject -Identity 'OU=Legacy,DC=corp,DC=contoso,DC=com' -ProtectedFromAccidentalDeletion $false
Move-ADObject -Identity 'OU=Legacy,DC=corp,DC=contoso,DC=com' -TargetPath 'OU=Archive,DC=corp,DC=contoso,DC=com'
2. Delegation of Control
Delegation is what makes OUs worth designing. The Delegation of Control Wizard in Active Directory Users and Computers writes access control entries onto the OU's ACL, granting a group the right to perform specific operations on specific object classes within that OU — reset passwords on user objects, create and delete computer objects, modify group membership — without any membership in Domain Admins.
For anything the wizard's canned task list does not cover, or for auditing what has already been granted, use dsacls:
:: Show every ACE currently on the OU
dsacls "OU=Branch-Helpdesk,DC=corp,DC=contoso,DC=com"
:: Grant a group the right to reset passwords and force password change at next logon
dsacls "OU=Branch-Helpdesk,DC=corp,DC=contoso,DC=com" /I:S ^
/G "CORP\Tier1-HelpDesk:CA;Reset Password;user"
dsacls "OU=Branch-Helpdesk,DC=corp,DC=contoso,DC=com" /I:S ^
/G "CORP\Tier1-HelpDesk:WP;pwdLastSet;user"
[!TIP] Delegated rights are inherited down the OU tree, so a delegation applied at OU=Corp reaches every child OU. Moving a user object into a delegated OU therefore silently hands that object to a different administrative team — which is precisely why "move the account to the branch OU" is never a purely cosmetic change.
3. Bulk Provisioning and Attribute Management
The reference pattern for provisioning is a CSV export from the authoritative source system piped through New-ADUser. Two details cause most failures: the password must be a SecureString, and an account created without -Enabled $true is created disabled.
# newhires.csv columns: FirstName,LastName,SamAccountName,Department,Title,Manager
$DefaultPwd = ConvertTo-SecureString 'Tr@nsientP@ss2026!' -AsPlainText -Force
Import-Csv -Path 'C:\HR\newhires.csv' | ForEach-Object {
New-ADUser `
-Name "$($_.FirstName) $($_.LastName)" `
-GivenName $_.FirstName `
-Surname $_.LastName `
-SamAccountName $_.SamAccountName `
-UserPrincipalName "$($_.SamAccountName)@corp.contoso.com" `
-Path 'OU=Staff,OU=Corp,DC=corp,DC=contoso,DC=com' `
-Department $_.Department `
-Title $_.Title `
-AccountPassword $DefaultPwd `
-ChangePasswordAtLogon $true `
-Enabled $true
}
# Bulk-add the new hires to a role group in one operation
Add-ADGroupMember -Identity 'GG_Staff_AllUsers' `
-Members (Get-ADUser -Filter { Department -eq 'Finance' } -SearchBase 'OU=Staff,OU=Corp,DC=corp,DC=contoso,DC=com')
Alternative sign-in suffixes must exist at the forest level before a user object can use one:
Set-ADForest -Identity 'contoso.com' -UPNSuffixes @{ Add = 'contoso.co.uk','fabrikam-eu.com' }
Get-ADUser -Filter { Department -eq 'EMEA-Sales' } |
Set-ADUser -UserPrincipalName { "$($_.SamAccountName)@contoso.co.uk" }
4. Finding the Accounts That Should Not Exist
Stale accounts are both an audit finding and an attack surface. Search-ADAccount answers the common questions without hand-written LDAP filters:
| Command | Returns |
|---|---|
Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 -UsersOnly | User accounts with no logon in 90 days |
Search-ADAccount -LockedOut | Currently locked-out accounts |
Search-ADAccount -PasswordExpired | Accounts whose password has expired |
Search-ADAccount -AccountDisabled -ComputersOnly | Disabled computer objects |
Search-ADAccount -AccountExpiring -TimeSpan 14.00:00:00 | Accounts expiring in the next 14 days |
# Disable and move stale accounts rather than deleting them outright
Search-ADAccount -AccountInactive -TimeSpan 120.00:00:00 -UsersOnly |
Where-Object { $_.Enabled } |
ForEach-Object {
Disable-ADAccount -Identity $_.DistinguishedName
Move-ADObject -Identity $_.DistinguishedName -TargetPath 'OU=Disabled,OU=Corp,DC=corp,DC=contoso,DC=com'
}
Two properties behave unintuitively when auditing: LastLogon is not replicated and is only accurate on the domain controller that processed the logon, whereas LastLogonTimestamp is replicated but is deliberately imprecise — it updates only when the value is older than roughly 9 to 14 days. For inactivity reporting, LastLogonTimestamp (which is what Search-ADAccount -AccountInactive consumes) is the correct attribute; for a precise answer you must query every domain controller.
[!TIP] Open Active Directory Administrative Center, expand the Windows PowerShell History pane at the bottom, and perform the action in the GUI. ADAC prints the exact cmdlet and parameters it executed — the quickest way to convert an unfamiliar one-off task into a script you can schedule.
A newly joined member server receives the domain password policy and a site-linked security GPO, but none of the settings from a GPO linked to OU=Servers. The computer object is in CN=Computers. What is the most appropriate permanent fix?
An administrator runs New-ADUser with -Name, -SamAccountName, -UserPrincipalName and -Path, and the command completes without error. The new user cannot sign in. What is the most likely cause?
A security review asks for every user account that has not signed in for 90 days. Which approach produces the most reliable result with the least effort?