PowerShell scripts to deploy a 4-tier Active Directory administrative model based on Microsoft ESAE best practices.
| Tier | Purpose | Example Assets |
|---|---|---|
| Tier 0 | Identity & Domain Controllers | AD DS, PKI, ADFS, AAD Connect |
| Tier 1 | Servers & Applications | Member servers, SQL, Exchange, file servers |
| Tier 2 | User & Workstation Administration | Administers standard users/computers in separate OUs; some servers classified as T2 |
| Tier 3 | Unprivileged Jumphost Accounts | Bastion/PAW-only accounts, no direct resource access |
| Component | Switch | Description |
|---|---|---|
| OU Hierarchy | -CreateOUs |
Top-level Security OU with per-tier sub-OUs and a categorized Groups hierarchy |
| Security Groups | -CreateGroups |
t0/t1/t2/t3 admin, service account, and resource groups |
| GPOs | -CreateGPOs |
Logon restriction policies (deny cross-tier logon) |
| Delegation | -SetDelegation |
ACLs granting each tier's admins control over their OU |
| Everything | -All |
All of the above |
- RSAT: ActiveDirectory and GroupPolicy PowerShell modules
- Permissions: Domain Admin or equivalent
- Run from a domain-joined machine
# Preview everything (no changes)
.\Deploy-ADTiering.ps1 -All -WhatIf
# Full deployment (default top OU = "Security")
.\Deploy-ADTiering.ps1 -All
# Only OUs and groups
.\Deploy-ADTiering.ps1 -CreateOUs -CreateGroups
# Custom top-level OU name
.\Deploy-ADTiering.ps1 -All -TopOU "Tiering"
# Test/teardown mode -- skip "Protect from accidental deletion" so you can
# rip the structure out cleanly with Remove-ADOrganizationalUnit -Recursive
.\Deploy-ADTiering.ps1 -All -ProtectFromAccidentalDeletion:$falseOU=Security,DC=contoso,DC=com
|-- Tier 0
| |-- Users
| |-- Servers
| |-- Service Accounts
| |-- Jumphosts
| +-- Groups
| |-- Application
| |-- Delegation (t0.ou.admins)
| |-- gMSA
| |-- GPO
| |-- Local Admin (t0.local.admin.all.servers, t0.local.admin.<host>)
| |-- RDP (t0.rdp.<host>)
| |-- Role
| |-- Security
| |-- SQL
| +-- Tier (t0.deny.logon, t0.admin.users, t0.all.resources,
| t0.servers, t0.jumphosts, t0.service.accounts)
|-- Tier 1 (same Groups layout as Tier 0)
|-- Tier 2 (same Groups layout as Tier 0)
+-- Tier 3
|-- Users
|-- Jumphosts
+-- Groups
|-- Application
|-- Delegation (t3.ou.admins)
|-- GPO
|-- Local Admin (t3.local.admin.<host>)
|-- RDP (t3.rdp.<host>)
|-- Role
|-- Security
+-- Tier (t3.deny.logon, t3.admin.users, t3.jumphosts, t3.all.resources)
Note: Standard (non-admin) users and workstations live in their own OUs outside this structure. Tier 2 admins are delegated rights to manage those objects.
Sub-OUs are recursive: under
Jumphosts,Servers,Service AccountsandUsersyou can create additional sub-OUs over time. The sync script scans them recursively (-SearchScope Subtree).
The key principle: credentials from one tier must never be exposed on another tier's assets.
- Tier 0 assets deny logon from T1, T2, T3 accounts
- Tier 1 assets deny logon from T0, T2, T3 accounts
- Tier 2 assets deny logon from T0, T1, T3 accounts
- Tier 3 assets deny logon from T0, T1, T2 accounts
- Domain Controllers get an additional hardened GPO denying all non-T0 logon types
All tier GPOs deny: interactive logon, RDP, network logon, batch logon, and service logon.
Sync-TierGroupMembership.ps1 automatically populates tier groups based on OU placement (recursively). All scans use -SearchScope Subtree, so any sub-OU you create later under Users, Servers, Service Accounts or Jumphosts is included.
| OU (recursive) | Group |
|---|---|
Tier X\Users |
tx.admin.users |
Tier X\Users + Service Accounts |
tx.deny.logon (deny boundary) |
Tier X\Servers |
tx.servers |
Tier X\Service Accounts |
tx.service.accounts |
Tier X\Jumphosts |
tx.jumphosts |
| (all sub-groups) | tx.all.resources (nested groups) |
tx.ou.admins is manually managed -- it is used solely for OU delegation. Do not nest access groups into it (that would grant all members full control over the tier OU).
Two GPOs per tier manage host access. Both rely on environment variable expansion (%COMPUTERNAME%) inside Group Policy Preferences instead of item-level targeting -- one static XML covers every host in the OU.
Group Policy Preferences -> Local Users and Groups entries:
| Built-in target | Member added |
|---|---|
| Administrators | DOMAIN\tx.local.admin.all.servers (tier-wide) |
| Remote Desktop Users | DOMAIN\tx.local.admin.all.servers (tier-wide) |
| Administrators | DOMAIN\tx.local.admin.%COMPUTERNAME% |
| Remote Desktop Users | DOMAIN\tx.rdp.%COMPUTERNAME% |
The sync script auto-creates the per-host groups (tx.local.admin.<host>, tx.rdp.<host>) for every computer object found under the Servers OU. Add real users to those groups to grant per-server access.
Same pattern. T0-T2 use tx.local.admin.all.servers for tier-wide entries. T3 uses t3.admin.users as the tier-wide RDP group only (no tier-wide local admin); per-host t3.local.admin.<host> and t3.rdp.<host> groups are created the same way as the other tiers.
The previous design used GPP item-level targeting with one filter per host. That meant rewriting the GPO every sync. With %COMPUTERNAME% expansion the XML is static -- adding a host means creating its tx.local.admin.<host> group; the GPO itself doesn't change.
Workstation --[NTLM]--> T3 jumphost --[Kerberos]--> T1 jumphost --[Kerberos]--> T1 server
(standard user) (frelun.t3) (frelun.t1) (frelun.t1)
- T3 = airlock: no Protected Users, NTLM allowed for cross-domain/untrusted network first hop
- T0/T1/T2 in Protected Users: Kerberos enforced from the domain-joined T3 jumphost onward
- Deny-logon GPOs enforce tier boundaries
# Install with defaults (SYSTEM, 15 min interval)
.\Install-SyncTask.ps1
# Custom interval
.\Install-SyncTask.ps1 -Interval 5# Preview changes
.\Sync-TierGroupMembership.ps1 -WhatIf
# Run with logging
.\Sync-TierGroupMembership.ps1 -LogPath C:\Logs\tier-sync.log
# Custom top OU
.\Sync-TierGroupMembership.ps1 -TopOU "Tiering"To iterate quickly during testing:
# Deploy without deletion protection
.\Deploy-ADTiering.ps1 -All -ProtectFromAccidentalDeletion:$false
.\Sync-TierGroupMembership.ps1 -ProtectFromAccidentalDeletion:$false
# ... test ...
# Tear it all down (groups + OUs)
Get-ADGroup -Filter "Name -like 't?.*'" | Remove-ADGroup -Confirm:$false
Get-ADOrganizationalUnit -Filter "Name -eq 'Security'" | Remove-ADOrganizationalUnit -Recursive -Confirm:$false
Get-GPO -All | Where-Object { $_.DisplayName -like 'T?-Computer-*' } | Remove-GPO -Confirm:$falseRe-enable protection in production with
-ProtectFromAccidentalDeletion:$true(the default).
- Move existing admin accounts into the appropriate
Tier X\UsersOUs (sub-OUs allowed) - Move servers into the appropriate
Tier X\ServersOUs - Move jumphost computers into the appropriate
Tier X\JumphostsOUs - Add users to
tx.local.admin.all.serversfor tier-wide server admin access - Create dedicated Tier 3 jumphost accounts in
Tier 3\Users - Run
Sync-TierGroupMembership.ps1to populate auto groups and create per-host groups - Add real users to per-host groups (
tx.local.admin.<host>,tx.rdp.<host>) as needed - Run
gpupdate /forceor wait for policy refresh - Test logon restrictions before relying on them in production
- Consider Authentication Policies & Silos (Server 2012 R2+) for Tier 0