Manage Active Directory Like an IT Admin

Build an enterprise AD environment with OU design, RBAC, and delegation.

Introduction

30 Second Summary

Think about what happens when a new employee joins a company. Someone has to create their account, give them access to the right files, and make sure they cannot accidentally reach things they should not see.

In this project, you will build a fully structured Active Directory environment that mirrors how enterprise IT teams organize and manage identity. You'll create an OU hierarchy, bulk-provision users with PowerShell, implement role-based access control, and delegate helpdesk permissions without granting Domain Admin privileges.

What You'll Build

A governed Active Directory domain where users are organized by department, access is controlled through role-based security groups, and helpdesk staff can perform common support tasks without elevated privileges.

By the end of this project, you'll have:

  • An enterprise OU structure with separate branches for Users, Computers, Groups, and Admin accounts designed for delegation and Group Policy scoping.
  • A set of bulk-provisioned users created from a CSV file using PowerShell, organized into department OUs with proper attributes.
  • Role-based access control implemented with nested security groups following the AGDLP pattern, plus delegated helpdesk permissions scoped to a specific OU.
  • Secret Mission: Implement a Fine-Grained Password Policy enforcing stricter security requirements on privileged admin accounts.

Are there any prerequisites?

This project assumes you already have a Windows Server 2022 VM running on Hyper-V with Active Directory Domain Services promoted to a Domain Controller.

If you have not completed that setup yet, finish the AD DS installation and domain promotion first, then come back here.

Design and Build the OU Structure

Every enterprise Active Directory environment starts with a well-designed Organizational Unit (OU) structure. OUs are containers that organize your domain objects (users, computers, groups) into a logical hierarchy.

In this step, you will design and build a production-style OU tree for a fictional company called Contoso Corp. This structure separates objects by type so you can apply different Group Policy settings to each branch and delegate administrative control precisely.

In this step, get ready to:
  • Plan the OU hierarchy for Contoso Corp with separate branches for Users, Computers, Groups, and Admin Accounts.
  • Create the full OU structure using a PowerShell script.
  • Validate the structure in Active Directory Users and Computers (ADUC).
Plan the OU hierarchy

Before creating anything in Active Directory, you need a plan. The goal is to separate objects by type and function so that Group Policy and delegation are clean and predictable.

Here is the OU hierarchy you will build:

  • Contoso (top-level OU under the domain root)

Why separate Users from Computers?

Keeping Users and Computers in different OU branches lets you link different GPOs to each branch independently. For example, you might enforce software restrictions on Workstations but not on Servers.

Designing OUs based on delegation and policy needs (not org chart) follows Microsoft's best practice of keeping depth manageable and scope clear.

Create the OU structure using PowerShell

You will create a PowerShell script that builds the entire OU tree in one run. This is how enterprise IT teams work. They script repeatable infrastructure instead of clicking through GUIs.

  • Open Notepad from your PowerShell session by running this command:
notepad C:\Scripts\Create-OUStructure.ps1

Notepad opens with a blank file. You will now add the script content in sections.

  • Start building your script by pasting this first section into the Notepad window:
# Create-OUStructure.ps1
# Creates the enterprise OU hierarchy for Contoso Corp

$domain = (Get-ADDomain).DistinguishedName

# Top-level OU
New-ADOrganizationalUnit -Name "Contoso" -Path $domain -ProtectedFromAccidentalDeletion $true

$contosoPath = "OU=Contoso,$domain"

What does this code do?

  • `$domain = (Get-ADDomain).DistinguishedName` retrieves your domain's distinguished name (e.g., DC=yourdomain,DC=com) so the script works on any domain without hardcoding.
  • `New-ADOrganizationalUnit` creates the top-level Contoso OU directly under the domain root.
  • `-ProtectedFromAccidentalDeletion $true` prevents anyone from accidentally deleting the OU. This is a safety guard used in production environments.
  • `$contosoPath` stores the full path to the new OU so subsequent commands can reference it easily.
  • Below the code you just pasted, add this section to create the four second-level OUs:
# Second-level OUs
New-ADOrganizationalUnit -Name "Users" -Path $contosoPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Computers" -Path $contosoPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Groups" -Path $contosoPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Admin Accounts" -Path $contosoPath -ProtectedFromAccidentalDeletion $true

What does this code do?

Each New-ADOrganizationalUnit call creates one child OU inside the Contoso OU. The -Path parameter points to $contosoPath, which places them one level below Contoso. You now have four branches: Users (for people), Computers (for machines), Groups (for security groups), and Admin Accounts (for privileged identities).

  • Continue building the script by pasting this section below the second-level OUs to create department sub-OUs:
# Department OUs under Users
$usersPath = "OU=Users,$contosoPath"
New-ADOrganizationalUnit -Name "IT" -Path $usersPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "HR" -Path $usersPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Finance" -Path $usersPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Sales" -Path $usersPath -ProtectedFromAccidentalDeletion $true

What does this code do?

`$usersPath` builds the distinguished name for the Users OU by prepending OU=Users, to $contosoPath. The four department OUs (IT, HR, Finance, Sales) are created inside Users. Later, you will place user accounts into these OUs based on their department.

  • Finish the script by pasting this final section below the department OUs:
# Computer sub-OUs
$computersPath = "OU=Computers,$contosoPath"
New-ADOrganizationalUnit -Name "Workstations" -Path $computersPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Servers" -Path $computersPath -ProtectedFromAccidentalDeletion $true

Write-Host "OU structure created successfully." -ForegroundColor Green
Get-ADOrganizationalUnit -Filter * -SearchBase $contosoPath | Format-Table Name, DistinguishedName

What does this code do?

  • The Workstations and Servers OUs go inside the Computers branch, separating end-user machines from infrastructure.
  • `Write-Host` prints a success message after all OUs are created.
  • `Get-ADOrganizationalUnit -Filter *` lists every OU under Contoso in a table so you can verify the structure at a glance.
  • Save the file in Notepad by pressing Ctrl+S, then close Notepad.

✔️ Awesome, I've got everything!

Great. Double check you have saved C:\Scripts\Create-OUStructure.ps1 before moving on.

ⓧ I'd like to double check the full code

Here is the complete script for comparison:

# Create-OUStructure.ps1
# Creates the enterprise OU hierarchy for Contoso Corp

$domain = (Get-ADDomain).DistinguishedName

# Top-level OU
New-ADOrganizationalUnit -Name "Contoso" -Path $domain -ProtectedFromAccidentalDeletion $true

$contosoPath = "OU=Contoso,$domain"

# Second-level OUs
New-ADOrganizationalUnit -Name "Users" -Path $contosoPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Computers" -Path $contosoPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Groups" -Path $contosoPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Admin Accounts" -Path $contosoPath -ProtectedFromAccidentalDeletion $true

# Department OUs under Users
$usersPath = "OU=Users,$contosoPath"
New-ADOrganizationalUnit -Name "IT" -Path $usersPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "HR" -Path $usersPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Finance" -Path $usersPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Sales" -Path $usersPath -ProtectedFromAccidentalDeletion $true

# Computer sub-OUs
$computersPath = "OU=Computers,$contosoPath"
New-ADOrganizationalUnit -Name "Workstations" -Path $computersPath -ProtectedFromAccidentalDeletion $true
New-ADOrganizationalUnit -Name "Servers" -Path $computersPath -ProtectedFromAccidentalDeletion $true

Write-Host "OU structure created successfully." -ForegroundColor Green
Get-ADOrganizationalUnit -Filter * -SearchBase $contosoPath | Format-Table Name, DistinguishedName
  • Run the script from your PowerShell session by executing this command:
.\Scripts\Create-OUStructure.ps1
  • You should see a green "OU structure created successfully." message followed by a table listing all the OUs and their distinguished names.

Seeing an error about the OU already existing?

If you see a message like "The specified directory service attribute or value already exists", it means the script was already run once. You can safely ignore the error for OUs that already exist, or delete the Contoso OU in ADUC first and re-run the script.

If you see "Access is denied", make sure you are running PowerShell as Administrator on the Domain Controller.

Help me troubleshoot my OU creation script errors.

Validate the structure in ADUC

The script output gives you a quick text-based view. Now confirm everything looks right in the graphical management console.

  • Open Active Directory Users and Computers by running this command in PowerShell:
dsa.msc
  • In the ADUC console, expand your domain name in the left panel.
  • Expand the Contoso OU. You should see four child OUs: Users, Computers, Groups, and Admin Accounts.
  • Expand the Users OU. You should see the four department sub-OUs: IT, HR, Finance, and Sales.
  • Expand the Computers OU. You should see Workstations and Servers.

Why does this layout matter for GPOs?

Group Policy Objects linked to the Users OU will apply to all user accounts in every department sub-OU beneath it. If you need a policy that targets only the Finance department, you link it to the Finance sub-OU instead.

The same principle applies to Computers. You can enforce stricter policies on Servers without affecting Workstations. This granular control is only possible because you separated them into distinct branches.

🙋‍♀️ Don't see the Contoso OU in ADUC?

If the Contoso OU does not appear, right-click your domain name in ADUC and select Refresh. The console sometimes needs a manual refresh to show newly created objects.

If it still does not appear, switch back to PowerShell and run Get-ADOrganizationalUnit -Filter * | Format-Table Name to check whether the OUs exist in the directory. If the command returns nothing, re-run the script.

Help me figure out why my OUs are not showing up in ADUC.

Your OU foundation is in place. Next up, you will populate this structure with real user accounts by bulk-creating them from a CSV file using PowerShell automation.

Bulk-Create Users from a CSV File

Your OU structure is built, but right now every department folder is empty. In a real enterprise, onboarding even 15 users one-by-one through a GUI would take an unreasonable amount of time.

Enterprise IT teams solve this with automation. You will create a CSV file containing your user data, then write a PowerShell script that reads each row and creates the corresponding Active Directory account automatically.

In this step, get ready to:
  • Create a CSV file with 15 users spread across four departments.
  • Write a PowerShell script that bulk-creates user accounts with error handling.
  • Run the script and verify users landed in their correct department OUs.
Create the CSV data file

The CSV file acts as your single source of truth for new hires. Each row represents one user with their name, department, job title, and username. The script you write next will read this file and create each account in the correct OU.

  • Open Notepad by pressing the Windows key, typing Notepad, and pressing Enter.
  • Paste the following CSV content into the new file:
FirstName,LastName,Department,Title,Username
Alice,Johnson,IT,Systems Administrator,ajohnson
Bob,Smith,IT,Network Engineer,bsmith
Carol,Williams,IT,Security Analyst,cwilliams
David,Brown,IT,Helpdesk Technician,dbrown
Emma,Davis,HR,HR Manager,edavis
Frank,Miller,HR,Recruiter,fmiller
Grace,Wilson,HR,HR Coordinator,gwilson
Henry,Moore,Finance,Financial Analyst,hmoore
Isabel,Taylor,Finance,Accountant,itaylor
James,Anderson,Finance,Finance Manager,janderson
Karen,Thomas,Finance,Accounts Payable,kthomas
Leo,Jackson,Sales,Sales Manager,ljackson
Maria,White,Sales,Account Executive,mwhite
Nathan,Harris,Sales,Sales Representative,nharris
Olivia,Martin,Sales,Business Development,omartin

What does this file contain?

The file defines 15 users across four departments: 4 in IT, 3 in HR, 4 in Finance, and 4 in Sales. Each row maps directly to the parameters the script will pass to the New-ADUser cmdlet.

The Department column is especially important. The script uses it to calculate which OU each user belongs in.

  • Click File, then Save As.
  • Navigate to C:\Scripts.
  • In the Save as type dropdown, select All Files (*.*).
  • Name the file users.csv and click Save.
  • Verify the file was saved correctly by switching back to your PowerShell window and running:
Import-Csv -Path C:\Scripts\users.csv | Format-Table

You should see a formatted table showing all 15 users with their FirstName, LastName, Department, Title, and Username columns.

Seeing an error about the path?

Make sure you selected All Files in the Save as type dropdown. Without this, Notepad may have saved the file as users.csv.txt instead of users.csv.

Run ls C:\Scripts to see exactly what files exist. If you see users.csv.txt, rename it by running Rename-Item C:\Scripts\users.csv.txt users.csv.

Still stuck? Help me troubleshoot my users.csv file not being found by Import-Csv.

✔️ Awesome, I've got everything!

Great. Make sure your file is saved before moving on.

ⓧ I'd like to double check the full code

FirstName,LastName,Department,Title,Username
Alice,Johnson,IT,Systems Administrator,ajohnson
Bob,Smith,IT,Network Engineer,bsmith
Carol,Williams,IT,Security Analyst,cwilliams
David,Brown,IT,Helpdesk Technician,dbrown
Emma,Davis,HR,HR Manager,edavis
Frank,Miller,HR,Recruiter,fmiller
Grace,Wilson,HR,HR Coordinator,gwilson
Henry,Moore,Finance,Financial Analyst,hmoore
Isabel,Taylor,Finance,Accountant,itaylor
James,Anderson,Finance,Finance Manager,janderson
Karen,Thomas,Finance,Accounts Payable,kthomas
Leo,Jackson,Sales,Sales Manager,ljackson
Maria,White,Sales,Account Executive,mwhite
Nathan,Harris,Sales,Sales Representative,nharris
Olivia,Martin,Sales,Business Development,omartin
Write the bulk-creation script

The script needs to read each CSV row, calculate the correct OU path from the Department column, and call New-ADUser with all the required attributes. It also needs error handling so one bad row does not stop the entire batch.

Why PowerShell over the GUI?

Enterprise IT teams manage hundreds to thousands of users. Manual GUI creation through Active Directory Users and Computers is not scalable for batch onboarding.

CSV-based bulk creation with logging mirrors real-world onboarding workflows. When HR hands you a spreadsheet of new hires, you run a script, not a click-through wizard.

  • In Notepad, open a new file by pressing Ctrl+N.
  • Paste the following script into the file:
# Bulk-CreateUsers.ps1
# Creates AD users from a CSV file and places them in department OUs

$domain = (Get-ADDomain).DistinguishedName
$contosoPath = "OU=Contoso,$domain"
$usersPath = "OU=Users,$contosoPath"
$domainSuffix = (Get-ADDomain).DNSRoot
$defaultPassword = ConvertTo-SecureString "P@ssw0rd2024!" -AsPlainText -Force

$csvPath = "C:\Scripts\users.csv"
$logPath = "C:\Scripts\user-creation-log.csv"
$results = @()

$users = Import-Csv -Path $csvPath

foreach ($user in $users) {
    $ouPath = "OU=$($user.Department),$usersPath"
    $displayName = "$($user.FirstName) $($user.LastName)"
    $upn = "$($user.Username)@$domainSuffix"

    try {
        New-ADUser `
            -Name $displayName `
            -GivenName $user.FirstName `
            -Surname $user.LastName `
            -SamAccountName $user.Username `
            -UserPrincipalName $upn `
            -DisplayName $displayName `
            -Department $user.Department `
            -Title $user.Title `
            -Path $ouPath `
            -AccountPassword $defaultPassword `
            -Enabled $true `
            -ChangePasswordAtLogon $true

        $results += [PSCustomObject]@{
            Username = $user.Username
            DisplayName = $displayName
            Department = $user.Department
            OU = $ouPath
            Status = "Success"
            Error = ""
        }
        Write-Host "Created: $displayName ($($user.Username))" -ForegroundColor Green
    }
    catch {
        $results += [PSCustomObject]@{
            Username = $user.Username
            DisplayName = $displayName
            Department = $user.Department
            OU = $ouPath
            Status = "Failed"
            Error = $_.Exception.Message
        }
        Write-Host "Failed: $displayName - $($_.Exception.Message)" -ForegroundColor Red
    }
}

$results | Export-Csv -Path $logPath -NoTypeInformation
Write-Host "`nUser creation complete. Log saved to: $logPath" -ForegroundColor Cyan
Write-Host "Total: $($results.Count) | Success: $(($results | Where-Object Status -eq 'Success').Count) | Failed: $(($results | Where-Object Status -eq 'Failed').Count)"

What does this script do?

  • Lines 4-8 configure the domain paths and set a default password. The script calculates the domain's distinguished name dynamically so it works regardless of your domain name.
  • Line 14 reads the CSV with Import-Csv, turning each row into a PowerShell object with properties matching the column headers.
  • Line 17 builds the target OU path dynamically from the Department column. A user in IT goes to OU=IT,OU=Users,OU=Contoso,....
  • The try/catch block ensures one failed user does not stop the entire batch. Each result (success or failure) is logged to a CSV for auditing.
  • Save the file as Bulk-CreateUsers.ps1 in C:\Scripts using the same method: File > Save As, set type to All Files (*.*), navigate to C:\Scripts, and name it Bulk-CreateUsers.ps1.

You will not see any change yet. The next substep runs the script and verifies the results.

✔️ Awesome, I've got everything!

Great. Double check you saved the file before continuing.

ⓧ I'd like to double check the full code

# Bulk-CreateUsers.ps1
# Creates AD users from a CSV file and places them in department OUs

$domain = (Get-ADDomain).DistinguishedName
$contosoPath = "OU=Contoso,$domain"
$usersPath = "OU=Users,$contosoPath"
$domainSuffix = (Get-ADDomain).DNSRoot
$defaultPassword = ConvertTo-SecureString "P@ssw0rd2024!" -AsPlainText -Force

$csvPath = "C:\Scripts\users.csv"
$logPath = "C:\Scripts\user-creation-log.csv"
$results = @()

$users = Import-Csv -Path $csvPath

foreach ($user in $users) {
    $ouPath = "OU=$($user.Department),$usersPath"
    $displayName = "$($user.FirstName) $($user.LastName)"
    $upn = "$($user.Username)@$domainSuffix"

    try {
        New-ADUser `
            -Name $displayName `
            -GivenName $user.FirstName `
            -Surname $user.LastName `
            -SamAccountName $user.Username `
            -UserPrincipalName $upn `
            -DisplayName $displayName `
            -Department $user.Department `
            -Title $user.Title `
            -Path $ouPath `
            -AccountPassword $defaultPassword `
            -Enabled $true `
            -ChangePasswordAtLogon $true

        $results += [PSCustomObject]@{
            Username = $user.Username
            DisplayName = $displayName
            Department = $user.Department
            OU = $ouPath
            Status = "Success"
            Error = ""
        }
        Write-Host "Created: $displayName ($($user.Username))" -ForegroundColor Green
    }
    catch {
        $results += [PSCustomObject]@{
            Username = $user.Username
            DisplayName = $displayName
            Department = $user.Department
            OU = $ouPath
            Status = "Failed"
            Error = $_.Exception.Message
        }
        Write-Host "Failed: $displayName - $($_.Exception.Message)" -ForegroundColor Red
    }
}

$results | Export-Csv -Path $logPath -NoTypeInformation
Write-Host "`nUser creation complete. Log saved to: $logPath" -ForegroundColor Cyan
Write-Host "Total: $($results.Count) | Success: $(($results | Where-Object Status -eq 'Success').Count) | Failed: $(($results | Where-Object Status -eq 'Failed').Count)"
Run the script and verify user placement

Time to execute the script and confirm that all 15 accounts landed in their correct department OUs with the right attributes.

  • Run the bulk-creation script by executing this command in your PowerShell window:
C:\Scripts\Bulk-CreateUsers.ps1

You should see a series of green lines reading "Created:" followed by each user's display name and username. At the end, a summary shows the total count, successes, and failures.

Seeing red "Failed" lines or execution policy errors?

If PowerShell blocks the script with an execution policy error, run Set-ExecutionPolicy RemoteSigned and confirm with Y. Then re-run the script.

If you see "Directory object not found" errors, confirm your OU structure includes all four department OUs (IT, HR, Finance, Sales) under Users. The script tries to place users into OUs that match the Department column exactly.

Still stuck? Help me troubleshoot my Bulk-CreateUsers.ps1 script failing on my Windows Server 2022 domain controller.

  • Verify users were created in the correct OUs by running this command:
Get-ADUser -Filter * -SearchBase "OU=Users,OU=Contoso,$((Get-ADDomain).DistinguishedName)" -Properties Department | Format-Table Name, Department, DistinguishedName

You should see all 15 users listed with their display name, department, and the full OU path confirming correct placement.

What should I see?

Each user's DistinguishedName should include their department OU. For example, Alice Johnson should show CN=Alice Johnson,OU=IT,OU=Users,OU=Contoso,... while Leo Jackson should show CN=Leo Jackson,OU=Sales,OU=Users,OU=Contoso,....

  • Open Active Directory Users and Computers by running:
dsa.msc
  • Expand your domain, then expand Contoso > Users.
  • Click on each department OU (IT, HR, Finance, Sales) to confirm users appear in the correct folder.

You should see 4 users in IT, 3 in HR, 4 in Finance, and 4 in Sales.

What about the creation log?

The script saved a detailed log to C:\Scripts\user-creation-log.csv. You can open it anytime with Import-Csv C:\Scripts\user-creation-log.csv | Format-Table to review which accounts succeeded or failed. In production, this log is what you hand back to HR as proof of onboarding completion.

Your domain now has 15 users organized neatly by department. Next up, you will create security groups that control what these users can access using the AGDLP pattern.

Implement Role-Based Access Control with Security Groups

Your 15 users are now sitting in their department OUs, but having users is only half the equation. Without a structured way to control what each person can access, you would need to assign permissions individually to every single user.

In this step, you will implement Role-Based Access Control using security groups that follow the AGDLP nesting pattern. This is how enterprise IT teams grant access to resources at scale.

In this step, get ready to:
  • Create Global and Domain Local security groups following the AGDLP pattern.
  • Add users to their department groups and nest groups for resource access.
  • Verify the full group nesting chain from user to resource permission.
Create Global and Domain Local security groups

The AGDLP pattern is the Microsoft-recommended approach for managing resource permissions in Active Directory. It defines a clear chain: Accounts go into Global groups, Global groups go into Domain Local groups, and Domain Local groups get Permissions on resources.

What is the AGDLP pattern?

AGDLP stands for Accounts, Global groups, Domain Local groups, Permissions. It separates "who belongs together" (Global groups organized by role) from "what can be accessed" (Domain Local groups tied to specific resources).

When a new employee joins Sales, you add them to the GG-Sales-Staff Global group. They immediately inherit all resource access that group is nested into. No per-user permission changes needed.

  • Create a new file called Create-SecurityGroups.ps1 in C:\Scripts by opening Notepad or your text editor.
  • Paste the following code that creates the Global and Domain Local security groups:
# Create-SecurityGroups.ps1
# Creates security groups following the AGDLP pattern and assigns users

$domain = (Get-ADDomain).DistinguishedName
$contosoPath = "OU=Contoso,$domain"
$groupsPath = "OU=Groups,$contosoPath"
$usersPath = "OU=Users,$contosoPath"

# Create Global groups for each department
$departments = @("IT", "HR", "Finance", "Sales")
foreach ($dept in $departments) {
    New-ADGroup -Name "GG-$dept-Staff" `
        -GroupScope Global `
        -GroupCategory Security `
        -Path $groupsPath `
        -Description "Global group for $dept department staff"
    Write-Host "Created Global group: GG-$dept-Staff" -ForegroundColor Green
}

# Create Helpdesk group
New-ADGroup -Name "GG-Helpdesk" `
    -GroupScope Global `
    -GroupCategory Security `
    -Path $groupsPath `
    -Description "Helpdesk staff with delegated password reset permissions"
Write-Host "Created Global group: GG-Helpdesk" -ForegroundColor Green

# Create Domain Local groups for resource access
$resources = @(
    @{Name="DL-FileShare-IT-ReadWrite"; Desc="Read/Write access to IT file share"},
    @{Name="DL-FileShare-HR-Read"; Desc="Read access to HR file share"},
    @{Name="DL-FileShare-Finance-ReadWrite"; Desc="Read/Write access to Finance file share"},
    @{Name="DL-FileShare-Sales-Read"; Desc="Read access to Sales file share"}
)

foreach ($res in $resources) {
    New-ADGroup -Name $res.Name `
        -GroupScope DomainLocal `
        -GroupCategory Security `
        -Path $groupsPath `
        -Description $res.Desc
    Write-Host "Created Domain Local group: $($res.Name)" -ForegroundColor Green
}

What does this code do?

  • The $departments loop creates a Global group for each department (e.g., GG-IT-Staff). These represent "who belongs together" by role.
  • GG-Helpdesk is a separate Global group for helpdesk staff. You will use this in the next step to delegate administrative control.
  • The $resources loop creates Domain Local groups (e.g., DL-FileShare-IT-ReadWrite). These represent "what can be accessed" and will eventually receive file share permissions.
  • The -GroupScope parameter distinguishes Global groups (portable across domains) from DomainLocal groups (scoped to this domain for resource permissions).
Add users to groups and nest for AGDLP

Creating the groups is just the container setup. Now you need to populate the Global groups with their department users and then nest those Global groups inside the Domain Local groups to complete the AGDLP chain.

  • In Create-SecurityGroups.ps1, add the following code below the Domain Local group creation loop:
# Add users to their department Global groups
foreach ($dept in $departments) {
    $deptUsers = Get-ADUser -Filter * -SearchBase "OU=$dept,$usersPath"
    foreach ($user in $deptUsers) {
        Add-ADGroupMember -Identity "GG-$dept-Staff" -Members $user
    }
    Write-Host "Added $($deptUsers.Count) users to GG-$dept-Staff" -ForegroundColor Cyan
}

# Add helpdesk user to GG-Helpdesk
Add-ADGroupMember -Identity "GG-Helpdesk" -Members "dbrown"
Write-Host "Added dbrown to GG-Helpdesk" -ForegroundColor Cyan

# Nest Global groups into Domain Local groups (AGDLP)
Add-ADGroupMember -Identity "DL-FileShare-IT-ReadWrite" -Members "GG-IT-Staff"
Add-ADGroupMember -Identity "DL-FileShare-HR-Read" -Members "GG-HR-Staff"
Add-ADGroupMember -Identity "DL-FileShare-Finance-ReadWrite" -Members "GG-Finance-Staff"
Add-ADGroupMember -Identity "DL-FileShare-Sales-Read" -Members "GG-Sales-Staff"

Write-Host "`nAGDLP nesting complete." -ForegroundColor Green
Write-Host "Verifying nesting..."
foreach ($dept in $departments) {
    $members = Get-ADGroupMember -Identity "GG-$dept-Staff"
    Write-Host "GG-$dept-Staff has $($members.Count) members" -ForegroundColor Cyan
}

What does this code do?

  • The first loop queries each department OU with Get-ADUser -Filter * -SearchBase and adds every user it finds to that department's Global group.
  • David Brown (dbrown) is added to GG-Helpdesk so he can act as helpdesk staff in the next step.
  • The final four Add-ADGroupMember calls nest each Global group inside its corresponding Domain Local group. This is the key AGDLP link. Any user in GG-IT-Staff automatically inherits whatever permissions are assigned to DL-FileShare-IT-ReadWrite.

💡 Why AGDLP instead of adding users directly to Domain Local groups?

Adding users directly to resource groups works for small environments but becomes unmanageable at scale. With AGDLP, onboarding a new Sales hire means one action: add them to GG-Sales-Staff. They immediately inherit access to every resource that group is nested into.

Without nesting, you would need to manually add the new user to every individual resource group. Multiply that by hundreds of employees and dozens of resources, and you can see why the pattern matters.

  • Save C:\Scripts\Create-SecurityGroups.ps1.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before running it.

ⓧ I'd like to double check the full code

# Create-SecurityGroups.ps1
# Creates security groups following the AGDLP pattern and assigns users

$domain = (Get-ADDomain).DistinguishedName
$contosoPath = "OU=Contoso,$domain"
$groupsPath = "OU=Groups,$contosoPath"
$usersPath = "OU=Users,$contosoPath"

# Create Global groups for each department
$departments = @("IT", "HR", "Finance", "Sales")
foreach ($dept in $departments) {
    New-ADGroup -Name "GG-$dept-Staff" `
        -GroupScope Global `
        -GroupCategory Security `
        -Path $groupsPath `
        -Description "Global group for $dept department staff"
    Write-Host "Created Global group: GG-$dept-Staff" -ForegroundColor Green
}

# Create Helpdesk group
New-ADGroup -Name "GG-Helpdesk" `
    -GroupScope Global `
    -GroupCategory Security `
    -Path $groupsPath `
    -Description "Helpdesk staff with delegated password reset permissions"
Write-Host "Created Global group: GG-Helpdesk" -ForegroundColor Green

# Create Domain Local groups for resource access
$resources = @(
    @{Name="DL-FileShare-IT-ReadWrite"; Desc="Read/Write access to IT file share"},
    @{Name="DL-FileShare-HR-Read"; Desc="Read access to HR file share"},
    @{Name="DL-FileShare-Finance-ReadWrite"; Desc="Read/Write access to Finance file share"},
    @{Name="DL-FileShare-Sales-Read"; Desc="Read access to Sales file share"}
)

foreach ($res in $resources) {
    New-ADGroup -Name $res.Name `
        -GroupScope DomainLocal `
        -GroupCategory Security `
        -Path $groupsPath `
        -Description $res.Desc
    Write-Host "Created Domain Local group: $($res.Name)" -ForegroundColor Green
}

# Add users to their department Global groups
foreach ($dept in $departments) {
    $deptUsers = Get-ADUser -Filter * -SearchBase "OU=$dept,$usersPath"
    foreach ($user in $deptUsers) {
        Add-ADGroupMember -Identity "GG-$dept-Staff" -Members $user
    }
    Write-Host "Added $($deptUsers.Count) users to GG-$dept-Staff" -ForegroundColor Cyan
}

# Add helpdesk user to GG-Helpdesk
Add-ADGroupMember -Identity "GG-Helpdesk" -Members "dbrown"
Write-Host "Added dbrown to GG-Helpdesk" -ForegroundColor Cyan

# Nest Global groups into Domain Local groups (AGDLP)
Add-ADGroupMember -Identity "DL-FileShare-IT-ReadWrite" -Members "GG-IT-Staff"
Add-ADGroupMember -Identity "DL-FileShare-HR-Read" -Members "GG-HR-Staff"
Add-ADGroupMember -Identity "DL-FileShare-Finance-ReadWrite" -Members "GG-Finance-Staff"
Add-ADGroupMember -Identity "DL-FileShare-Sales-Read" -Members "GG-Sales-Staff"

Write-Host "`nAGDLP nesting complete." -ForegroundColor Green
Write-Host "Verifying nesting..."
foreach ($dept in $departments) {
    $members = Get-ADGroupMember -Identity "GG-$dept-Staff"
    Write-Host "GG-$dept-Staff has $($members.Count) members" -ForegroundColor Cyan
}
  • Run the script in your PowerShell session by running this command:
C:\Scripts\Create-SecurityGroups.ps1
  • You should see green text confirming each group was created, followed by cyan text confirming users were added and member counts for each Global group.

Seeing an access denied error?

  • Make sure you are running PowerShell as Administrator on the Domain Controller.
  • If you see a "group already exists" error, the script may have partially run before. Delete the existing groups from the Groups OU in ADUC and re-run.
  • If users are not being found, confirm your 15 users exist in the correct department OUs by running Get-ADUser -Filter * -SearchBase "OU=Users,OU=Contoso,$((Get-ADDomain).DistinguishedName)".

Help me troubleshoot my Create-SecurityGroups.ps1 script errors.

Verify the group nesting

The script output gives you a quick count, but you need to confirm the full AGDLP chain is connected correctly. You will check three things: that Domain Local groups contain their nested Global group, that Global groups contain the right users, and that an individual user can trace their membership up the chain.

  • Verify that the Domain Local group contains the nested Global group by running this command:
Get-ADGroupMember -Identity "DL-FileShare-IT-ReadWrite"
  • You should see GG-IT-Staff listed as a member with an objectClass of group. This confirms the nesting is in place.
  • Verify the individual users inside the Global group by running this command:
Get-ADGroupMember -Identity "GG-IT-Staff"
  • You should see four users listed: ajohnson, bsmith, cwilliams, and dbrown.
  • Verify a specific user's full group membership chain by running this command:
Get-ADPrincipalGroupMembership -Identity "ajohnson" | Format-Table Name, GroupScope
  • You should see GG-IT-Staff (Global) and Domain Users in the output. This confirms that ajohnson is properly connected through the AGDLP chain.

Why three separate verification checks?

Each check verifies a different link in the AGDLP chain. The first confirms Domain Local groups contain their nested Global group (the DL-to-G link). The second confirms Global groups contain the right users (the G-to-A link). The third traces a single user's memberships upward to confirm the full path is intact.

🙋‍♀️ Not seeing the expected members?

  • If Get-ADGroupMember returns nothing, confirm the script ran to completion without errors. Re-run C:\Scripts\Create-SecurityGroups.ps1 and watch for red error text.
  • If Get-ADPrincipalGroupMembership only shows Domain Users, the user was not added to their department group. Check that the user exists in the correct department OU (not in the parent Users OU).

Help me verify my AGDLP group nesting in Active Directory.

Your RBAC structure is now fully wired. Users belong to role-based Global groups, and those groups are nested into resource-scoped Domain Local groups. Next up, you will delegate helpdesk permissions so a non-admin account can perform common support tasks without needing Domain Admin access.

Delegate Administrative Control to Helpdesk

Your users are organized and your RBAC groups are in place. But there is still a problem: every time someone forgets a password or gets locked out, a Domain Admin has to fix it.

In a real enterprise, helpdesk staff handle password resets and account unlocks dozens of times a day. Granting them Domain Admin privileges for this would be wildly unsafe. Active Directory's Delegation of Control feature lets you hand out specific permissions scoped to a specific OU, so helpdesk staff can do their jobs without touching anything else.

In this step, get ready to:
  • Confirm the Helpdesk group is ready for delegation.
  • Delegate password reset and account unlock permissions using the Delegation of Control Wizard.
  • Test that delegation works by running commands as a non-admin helpdesk user.
Verify the Helpdesk group is ready for delegation

Before delegating permissions, confirm that the GG-Helpdesk group from Step 3 has the right membership. The user dbrown (David Brown, Helpdesk Technician) is the member who will act as your helpdesk staff.

  • Verify the membership of GG-Helpdesk by running this command in PowerShell:
# Confirm the Helpdesk group has the correct member
Get-ADGroupMember -Identity "GG-Helpdesk" | Format-Table Name, SamAccountName

What does this command do?

Get-ADGroupMember retrieves the members of a specified group. You should see dbrown (David Brown) listed as the only member.

  • Confirm you see David Brown with SamAccountName dbrown in the output.

Don't see dbrown in the group?

If the group appears empty, the security groups script from Step 3 may not have completed successfully. Re-run the add command:

Add-ADGroupMember -Identity "GG-Helpdesk" -Members "dbrown"

Then re-run the Get-ADGroupMember command to confirm.

Delegate control using the Delegation of Control Wizard in ADUC

The Delegation of Control Wizard modifies the Access Control List (ACL) on an OU. You will grant GG-Helpdesk the ability to reset passwords and read user information on the Users OU only.

  • Open Active Directory Users and Computers by running this command:
dsa.msc
  • In the left panel, expand your domain node.
  • Expand the Contoso OU.
  • Right-click the Users OU (under Contoso) and select Delegate Control....

Why delegate on the Users OU specifically?

Delegation is scoped to the OU you right-click and all objects beneath it. By choosing OU=Users,OU=Contoso, the helpdesk group can reset passwords for users in IT, HR, Finance, and Sales sub-OUs. They cannot touch anything in the Computers, Groups, or Admin Accounts OUs.

  • The Delegation of Control Wizard opens. Click Next on the welcome page.
  • On the Users or Groups page, click Add.
  • Type GG-Helpdesk in the object names field and click Check Names.
  • The name resolves with an underline. Click OK, then click Next.
  • On the Tasks to Delegate page, check these two boxes:
  • Reset user passwords and force password change at next logon.
  • Read all user information.
  • Click Next.
  • Review the summary and click Finish.

What just happened?

The wizard wrote new Access Control Entries (ACEs) on the Users OU. Any member of GG-Helpdesk can now reset passwords and read user attributes for every user object inside that OU and its child OUs. They have no other permissions.

This is the principle of least privilege in action. The helpdesk can do their daily work without being able to create, delete, or move user accounts.

  • Confirm the wizard completed by looking for the GG-Helpdesk entry. You can verify the delegation was applied by right-clicking the Users OU, selecting Properties, going to the Security tab, and checking that GG-Helpdesk appears in the permissions list.

Enable the Security tab in ADUC

If you do not see the Security tab on the OU properties, click View in the ADUC menu bar and enable Advanced Features. Then right-click the OU and select Properties again.

🙋‍♀️ Wizard did not complete or showed an error?

Make sure you are logged in as a Domain Admin. The Delegation of Control Wizard requires Domain Admins membership or equivalent delegated permissions to modify ACLs on OUs.

Also confirm you right-clicked the Users OU under Contoso, not the default Users container at the domain level. The default container does not support delegation the same way a custom OU does.

Test the delegation with a helpdesk user account

Configuring delegation means nothing if you do not verify it actually works. You will create a test script that attempts three actions as the dbrown helpdesk user: a password reset (should succeed), an account unlock (should succeed), and a user creation (should be denied).

  • Create the test script by running this command to open a new file:
notepad C:\Scripts\Test-Delegation.ps1
  • Paste the following script into Notepad:
# Test-Delegation.ps1
# Run this script as the helpdesk user to verify delegated permissions
# Usage: runas /user:yourdomain\dbrown powershell
# Then run this script from that session

$domain = (Get-ADDomain).DistinguishedName
$testUser = "ajohnson"

Write-Host "Testing delegation as: $env:USERNAME" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan

# Test 1: Reset a password (should succeed)
Write-Host "`nTest 1: Resetting password for $testUser..."
try {
    Set-ADAccountPassword -Identity $testUser -Reset -NewPassword (ConvertTo-SecureString "NewP@ss2024!" -AsPlainText -Force)
    Write-Host "SUCCESS: Password reset completed." -ForegroundColor Green
}
catch {
    Write-Host "FAILED: $($_.Exception.Message)" -ForegroundColor Red
}

# Test 2: Unlock an account (should succeed)
Write-Host "`nTest 2: Unlocking account $testUser..."
try {
    Unlock-ADAccount -Identity $testUser
    Write-Host "SUCCESS: Account unlocked." -ForegroundColor Green
}
catch {
    Write-Host "FAILED: $($_.Exception.Message)" -ForegroundColor Red
}

# Test 3: Try to create a user (should fail - not delegated)
Write-Host "`nTest 3: Attempting to create a new user (should fail)..."
try {
    New-ADUser -Name "Test Unauthorized" -SamAccountName "tunauth" -Path "OU=IT,OU=Users,OU=Contoso,$domain" -Enabled $false
    Write-Host "UNEXPECTED SUCCESS: This should have been denied!" -ForegroundColor Yellow
}
catch {
    Write-Host "EXPECTED FAILURE: Access denied - delegation is working correctly." -ForegroundColor Green
}

Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "Delegation testing complete." -ForegroundColor Cyan

What does this script do?

  • Test 1 uses Set-ADAccountPassword to reset ajohnson's password. This should succeed because you delegated password reset rights.
  • Test 2 uses Unlock-ADAccount to unlock ajohnson's account. This should also succeed under the password reset delegation.
  • Test 3 tries to create a new user with New-ADUser. This should fail with an access denied error because you did NOT delegate user creation rights.
  • Save and close Notepad.
  • Before running the test, the dbrown account needs a known password so you can log in as that user. Reset it by running:
# Set a known password for dbrown so you can use runas
Set-ADAccountPassword -Identity "dbrown" -Reset -NewPassword (ConvertTo-SecureString "Helpdesk2024!" -AsPlainText -Force)
Set-ADUser -Identity "dbrown" -ChangePasswordAtLogon $false

Why reset dbrown's password first?

When you bulk-created users in Step 2, every account was set to require a password change at next logon. The runas command cannot handle that interactive password change flow. Setting a known password and disabling the change-at-logon flag lets runas authenticate cleanly.

  • Now open a new PowerShell session as dbrown by running:
runas /user:[[YOURDOMAIN="your domain name (e.g. contoso.local)"]]/dbrown powershell
  • When prompted, enter the password Helpdesk2024! that you just set.
  • A new PowerShell window opens running as dbrown. In that new window, run the test script:
C:\Scripts\Test-Delegation.ps1
  • Review the output. You should see:
  • Test 1: SUCCESS: Password reset completed. in green.
  • Test 2: SUCCESS: Account unlocked. in green.
  • Test 3: EXPECTED FAILURE: Access denied - delegation is working correctly. in green.

This confirms least-privilege delegation is working. The helpdesk user can perform their daily tasks without having any extra power over the domain.

Test 1 or Test 2 shows FAILED?

If the password reset or unlock failed, the delegation may not have applied correctly. Go back to ADUC and verify you delegated on the correct OU (Users under Contoso, not the default Users container at the domain root).

Also confirm you selected both Reset user passwords and force password change at next logon and Read all user information in the wizard. If you missed the Read task, the helpdesk user cannot locate the target account to reset it.

🙋‍♀️ Test 3 shows UNEXPECTED SUCCESS?

If dbrown was able to create a user, the account may have inherited broader permissions. Check whether dbrown is a member of any built-in privileged groups (Account Operators, Domain Admins) by running Get-ADPrincipalGroupMembership -Identity "dbrown" from your admin session. Remove any unintended group memberships.

✔️ Awesome, I've got everything!

Great. Make sure you have saved C:\Scripts\Test-Delegation.ps1 and close the dbrown PowerShell window.

ⓧ I'd like to double check the full code

Here is the complete Test-Delegation.ps1 script for comparison:

# Test-Delegation.ps1
# Run this script as the helpdesk user to verify delegated permissions
# Usage: runas /user:yourdomain\dbrown powershell
# Then run this script from that session

$domain = (Get-ADDomain).DistinguishedName
$testUser = "ajohnson"

Write-Host "Testing delegation as: $env:USERNAME" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan

# Test 1: Reset a password (should succeed)
Write-Host "`nTest 1: Resetting password for $testUser..."
try {
    Set-ADAccountPassword -Identity $testUser -Reset -NewPassword (ConvertTo-SecureString "NewP@ss2024!" -AsPlainText -Force)
    Write-Host "SUCCESS: Password reset completed." -ForegroundColor Green
}
catch {
    Write-Host "FAILED: $($_.Exception.Message)" -ForegroundColor Red
}

# Test 2: Unlock an account (should succeed)
Write-Host "`nTest 2: Unlocking account $testUser..."
try {
    Unlock-ADAccount -Identity $testUser
    Write-Host "SUCCESS: Account unlocked." -ForegroundColor Green
}
catch {
    Write-Host "FAILED: $($_.Exception.Message)" -ForegroundColor Red
}

# Test 3: Try to create a user (should fail - not delegated)
Write-Host "`nTest 3: Attempting to create a new user (should fail)..."
try {
    New-ADUser -Name "Test Unauthorized" -SamAccountName "tunauth" -Path "OU=IT,OU=Users,OU=Contoso,$domain" -Enabled $false
    Write-Host "UNEXPECTED SUCCESS: This should have been denied!" -ForegroundColor Yellow
}
catch {
    Write-Host "EXPECTED FAILURE: Access denied - delegation is working correctly." -ForegroundColor Green
}

Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "Delegation testing complete." -ForegroundColor Cyan

Your helpdesk delegation is verified and working. You have proven that a non-admin user can handle the most common support tasks without needing dangerous Domain Admin privileges. Next up, take on the Secret Mission to lock down your admin accounts with stricter password requirements.

Secret mission

Implement Fine-Grained Password Policies for Admins

Your domain has a single password policy that applies to everyone equally. In the real world, privileged admin accounts need stricter security than standard users. Can you create a Fine-Grained Password Policy that enforces tighter requirements on admin accounts without affecting the rest of the domain?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely on your local Hyper-V VM, so there are no ongoing cloud costs.

Resources you used:

  • Contoso OU structure with all child OUs, user accounts, and security groups.
  • Fine-Grained Password Policy (AdminsPSO) applied to the GG-PrivilegedAdmins group.
  • Delegated control ACL granting GG-Helpdesk password reset rights on the Users OU.
  • PowerShell scripts in C:\Scripts\ (CSV file, creation scripts, test script, log file).

Keep everything running

No action needed. Choose this if you are still actively building on your Active Directory environment or plan to experiment further with group policies, delegation, or additional OUs.

  • Your Contoso OU structure, users, groups, and FGPP remain in place for continued use.
  • Your PowerShell scripts in C:\Scripts\ stay available for re-running or modifying.
  • Your Hyper-V VM continues to use disk space and memory when running, but there is no cloud billing.

Pause - I'll come back to this later

Shut down the VM to free up system resources while preserving all your AD objects and scripts.

  • In Hyper-V Manager, right-click your Windows Server 2022 VM and select Shut Down.
  • When you are ready to return, right-click the VM and select Start. All your OUs, users, groups, delegation, and scripts will be exactly as you left them.

Delete - I don't want to use this again

Remove all project resources from Active Directory and delete your scripts. Run these commands in an elevated PowerShell session on your Domain Controller.

  • Remove the Fine-Grained Password Policy by running:
Remove-ADFineGrainedPasswordPolicy -Identity "AdminsPSO" -Confirm:$false
  • Disable accidental deletion protection on all Contoso OUs so they can be removed:
$domain = (Get-ADDomain).DistinguishedName
Get-ADOrganizationalUnit -Filter * -SearchBase "OU=Contoso,$domain" | Set-ADOrganizationalUnit -ProtectedFromAccidentalDeletion $false
  • Delete the entire Contoso OU tree (this removes all child OUs, users, groups, and the delegation ACL):
Remove-ADObject -Identity "OU=Contoso,$domain" -Recursive -Confirm:$false
  • Delete the scripts folder:
Remove-Item -Path "C:\Scripts" -Recurse -Force
  • Verify the Contoso OU no longer exists:
Get-ADOrganizationalUnit -Filter 'Name -eq "Contoso"'

If no results are returned, all Active Directory objects from this project have been removed.

Nice Work!

Nice Work!

Nice work! You just built and managed a fully structured Active Directory environment the same way enterprise IT teams handle identity and access at scale.

You've learned how to:

  • Built a production-style OU structure and bulk-provisioned 15+ users from a CSV file using PowerShell automation.
  • Implemented Role-Based Access Control using the AGDLP security group nesting pattern to control resource access across departments.
  • Delegated helpdesk permissions using least-privilege principles and verified that scoped access works correctly without granting Domain Admin privileges.
  • Secret Mission: Implemented a Fine-Grained Password Policy enforcing stricter security requirements on privileged admin accounts.

Ready to quiz yourself?