active directory hardening active directory hardening

Active Directory Hardening: A Hands-On Technical Guide

Active Directory is in the path of roughly 80% of enterprise breaches because every laptop, file share, service account, and printer queue trusts the same identity fabric. When attackers say “own the domain,” they mean reaching a position in that fabric from which everything else is downstream. There is no firewall between Domain Admin and your customer database — there is just trust.

This guide skips the philosophy. The expected reader has Domain Admin rights, the ActiveDirectory PowerShell module (Install-WindowsFeature RSAT-AD-PowerShell), and a maintenance window. Each section: the threat in two sentences, the defensive concept in a paragraph, then the PowerShell that implements it. Run everything from a domain controller or a privileged access workstation — never from your daily-driver laptop.

Before any of the controls below, two ground rules. Stage every destructive change. Authentication Policy Silos, krbtgt rotations, and LDAP signing enforcement against running production have broken more environments than any attacker. Audit mode first, monitor for a week, then enforce. Inventory before pruning. A “stale” account is often a service principal nobody documented; a “useless” delegation is sometimes load-bearing for a 2015 line-of-business app. Document what you’re about to break, then break it.

Map the Attack Surface First

Before touching anything, see what an attacker would see. Defenders picture AD as an org chart — users in OUs, groups containing users, GPOs applying to OUs. Attackers picture it as a graph of permissions, and the edges of that graph are the attack paths. Any authenticated user can run BloodHound against your domain and produce a complete graph of permission relationships — there is no patch for this, it is how LDAP works.

Install BloodHound Community Edition, then run the collector:

# SharpHound collector from a domain-joined workstation
Invoke-WebRequest -Uri 'https://github.com/BloodHoundAD/SharpHound/releases/latest/download/SharpHound.zip' `
    -OutFile 'C:\Tools\SharpHound.zip'
Expand-Archive 'C:\Tools\SharpHound.zip' -DestinationPath 'C:\Tools\SharpHound'

# Run full collection
C:\Tools\SharpHound\SharpHound.exe -c All --zipfilename ad-collection.zip

Import the ZIP into BloodHound and run “Shortest Paths to Domain Admins” and “Find Principals with DCSync Rights.” What you see is what a phished user gets in ten minutes. Pair this with PingCastle (PingCastle.exe --healthcheck) for a scored risk report — both tools take an hour to set up and shape every decision below.

ATTACK CHAIN
The Six Steps to Domain Admin
01
Initial Foothold
Phishing → user shell on workstation
02
Local Privilege Escalation
SYSTEM → dump LSASS for cached creds
03
Enumeration
BloodHound maps the graph
04
Lateral Movement
Reused local admin hash → server
05
Privilege Escalation
Kerberoast / ACL chain / ADCS ESC1
06
Persistence
DCSync krbtgt → Golden Tickets

Each step below breaks one or more links in this chain.

Inventory Privileged Accounts

The built-in privileged groups are almost always over-populated. Every member is a Tier 0 asset whether the org knows it or not. Goal state: Domain Admins, Enterprise Admins, and Schema Admins empty in steady state; membership granted just-in-time and removed when work is done. Enterprise Admins and Schema Admins in particular are forest-level groups that should have zero permanent members outside rare schema operations.

# Full privileged-group inventory with risk indicators
$privGroups = @(
    'Domain Admins','Enterprise Admins','Schema Admins',
    'Administrators','Account Operators','Backup Operators',
    'Server Operators','Print Operators','DnsAdmins',
    'Group Policy Creator Owners','Cert Publishers',
    'Protected Users','Key Admins','Enterprise Key Admins'
)

$report = foreach ($g in $privGroups) {
    Get-ADGroupMember -Identity $g -Recursive -ErrorAction SilentlyContinue |
        ForEach-Object {
            $u = if ($_.objectClass -eq 'user') {
                Get-ADUser $_ -Properties LastLogonDate, PasswordLastSet,
                    Enabled, PasswordNeverExpires, AccountNotDelegated
            }
            [PSCustomObject]@{
                Group           = $g
                Name            = $_.Name
                SamAccountName  = $_.SamAccountName
                Class           = $_.objectClass
                Enabled         = $u.Enabled
                LastLogon       = $u.LastLogonDate
                PwdLastSet      = $u.PasswordLastSet
                PwdNeverExpires = $u.PasswordNeverExpires
                NotDelegated    = $u.AccountNotDelegated
            }
        }
}

$report | Export-Csv C:\AD-Audit\privileged.csv -NoTypeInformation

# Stale accounts in privileged groups - immediate priority
$report | Where-Object {
    $_.Class -eq 'user' -and
    ($_.LastLogon -lt (Get-Date).AddDays(-90) -or $_.PwdLastSet -lt (Get-Date).AddDays(-365))
} | Format-Table Group, Name, LastLogon, PwdLastSet -AutoSize

Then prune. Don’t yank accounts on a Friday — a “temporary” Domain Admin from 2019 may be running a backup job.

# Remove an account from a privileged group
Remove-ADGroupMember -Identity 'Domain Admins' -Members 'jsmith' -Confirm:$false

# Disable, don't delete - 30-day tombstone before deletion
Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 -UsersOnly |
    Where-Object Enabled -eq $true |
    Disable-ADAccount -WhatIf  # Drop -WhatIf when ready

Harden Admin Accounts: Protected Users + Sensitive Flag

Admin accounts that remain need three protections. Protected Users is a built-in group that enforces AES-only Kerberos, blocks NTLM authentication entirely, blocks credential caching, and prevents the account from being delegated. The sensitive flag (AccountNotDelegated) stops the account’s credentials from being delegated by any service it touches — defense in depth against compromised intermediary servers.

# Add admin accounts to Protected Users (requires 2012 R2 DFL)
$admins = @('s0-jsmith','s0-jdoe','s0-mchen')
Add-ADGroupMember -Identity 'Protected Users' -Members $admins

# Mark accounts as sensitive and cannot be delegated
$admins | ForEach-Object {
    Set-ADUser -Identity $_ -AccountNotDelegated $true
}

# Verify
Get-ADUser -Filter * -Properties AccountNotDelegated, MemberOf |
    Where-Object { $_.MemberOf -match 'Protected Users' } |
    Select-Object Name, AccountNotDelegated

Caution: Protected Users breaks RC4 Kerberos, NTLM, and credential delegation. If you have legacy apps that depend on any of these, they will fail. Test on one admin account first. Never put service accounts in Protected Users — incoming auth requests will be rejected and the service will stop working.

Tier Separation with Authentication Policy Silos

Tier separation — the principle that a high-privilege credential never logs onto a lower-privilege system — is the structural defense against credential theft. The classic violation: a domain admin RDPs to a workstation to troubleshoot a problem. Their Tier 0 credentials are now cached in LSASS on a Tier 2 machine. An attacker on that machine harvests them. Domain compromise in one session.

Group Policy “Deny log on” rights work as a baseline but can be bypassed by a local administrator on a member server. Authentication Policy Silos enforce the same restriction at the Kerberos KDC level on the domain controller — the policy cannot be evaded by anything happening on the target machine.

Prerequisites: Domain Functional Level 2012 R2+, and KDC support for claims, compound authentication, and Kerberos armoring enabled in the Default Domain Controllers Policy (Computer Configuration → Administrative Templates → System → KDC).

Create a Tier 0 silo restricting admins to logon only from Tier 0 systems (DCs and PAWs):

# Step 1: Create the authentication policy with 2-hour TGT lifetime
New-ADAuthenticationPolicy -Name 'Tier0-Policy' `
    -Description 'Tier 0 admins - restricted logon' `
    -UserTGTLifetimeMins 120 `
    -Enforce:$false   # Audit mode first

# Step 2: Access control - admins can only authenticate from machines in the Tier0 silo
$condition = @"
O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Tier0-Silo"))
"@

Set-ADAuthenticationPolicy -Identity 'Tier0-Policy' `
    -UserAllowedToAuthenticateFrom $condition

# Step 3: Create the silo
New-ADAuthenticationPolicySilo -Name 'Tier0-Silo' `
    -UserAuthenticationPolicy 'Tier0-Policy' `
    -ComputerAuthenticationPolicy 'Tier0-Policy' `
    -ServiceAuthenticationPolicy 'Tier0-Policy' `
    -Enforce:$false   # Audit mode first

# Step 4: Grant the silo to admin accounts and Tier 0 computers
Grant-ADAuthenticationPolicySiloAccess -Identity 'Tier0-Silo' -Account 's0-jsmith'
Grant-ADAuthenticationPolicySiloAccess -Identity 'Tier0-Silo' -Account 'DC01$'

# Step 5: Assign the silo to the accounts
Set-ADUser -Identity 's0-jsmith' -AuthenticationPolicySilo 'Tier0-Silo'
Set-ADComputer -Identity 'DC01' -AuthenticationPolicySilo 'Tier0-Silo'

# Run in audit mode for 1-2 weeks. Monitor event 4820 on DCs for policy violations.
# When clean, flip to enforce:
Set-ADAuthenticationPolicy -Identity 'Tier0-Policy' -Enforce:$true
Set-ADAuthenticationPolicySilo -Identity 'Tier0-Silo' -Enforce:$true

Repeat for Tier 1 (server admins → Tier 1 systems) and Tier 2 (workstation admins → Tier 2 systems). Always start in audit mode. Enforced silos misconfigured against running production lock out legitimate admins immediately — recovery requires the DSRM account.

Service Accounts: Hunt Kerberoasting Targets

Any user account with a Service Principal Name can have a Kerberos service ticket requested for it by any authenticated user in the domain. That ticket is encrypted with the service account’s password hash and can be cracked offline at the attacker’s leisure. This is Kerberoasting (MITRE ATT&CK T1558.003), and it is one of the most reliable post-foothold escalations because service account passwords are often weak, never rotated, and over-privileged.

# Find every account with an SPN, sorted by risk
Get-ADUser -Filter {ServicePrincipalName -like "*"} `
    -Properties ServicePrincipalName, PasswordLastSet, PasswordNeverExpires,
                MemberOf, AdminCount, 'msDS-SupportedEncryptionTypes' |
    Where-Object SamAccountName -ne 'krbtgt' |
    Select-Object Name, SamAccountName, AdminCount, PasswordLastSet,
        PasswordNeverExpires,
        @{n='Privileged';e={ $null -ne ($_.MemberOf -match 'Admins|Operators') }},
        @{n='RC4Allowed';e={ ($_.'msDS-SupportedEncryptionTypes' -band 0x4) -ne 0 -or $null -eq $_.'msDS-SupportedEncryptionTypes' }},
        @{n='SPNs';e={ $_.ServicePrincipalName -join '; ' }} |
    Sort-Object PasswordLastSet |
    Export-Csv C:\AD-Audit\spn-accounts.csv -NoTypeInformation

Any privileged service account with RC4Allowed=$true and a password older than a year is an immediate emergency — these are the most kerberoastable accounts in your domain. Two fixes: force AES-only encryption and migrate to gMSA.

# Force AES-only on a service account (value 24 = AES128 + AES256)
Set-ADUser -Identity 'svc-sql01' -Replace @{'msDS-SupportedEncryptionTypes'=24}

# Find accounts vulnerable to AS-REP roasting (pre-auth disabled)
Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} |
    Select-Object Name, SamAccountName, Enabled

# Fix
Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} |
    Set-ADAccountControl -DoesNotRequirePreAuth $false

Replace Service Accounts with gMSAs

A Group Managed Service Account gets a 240-character password rotated by AD every 30 days. There is nothing for a human to know, write down, or fail to rotate — and nothing realistic to crack offline.

# One-time per forest: create the KDS root key
Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10))

# Create a gMSA restricted to a specific server group
New-ADServiceAccount -Name 'gmsa-sqlsvc01' `
    -DNSHostName 'sqlsvc01.corp.local' `
    -PrincipalsAllowedToRetrieveManagedPassword 'SQL-Servers' `
    -KerberosEncryptionType AES128,AES256

# On the target server
Install-ADServiceAccount -Identity 'gmsa-sqlsvc01'
Test-ADServiceAccount  -Identity 'gmsa-sqlsvc01'   # must return True

# Configure the service to log on as DOMAIN\gmsa-sqlsvc01$ with blank password

For services that don’t support gMSA (older line-of-business apps, some third-party agents): 25+ character random password, AccountNotDelegated=$true, AES-only encryption, annual rotation calendar entry.

Deploy Windows LAPS

Same local Administrator password on every workstation means one phished hash unlocks the entire fleet — the textbook pass-the-hash lateral movement scenario. Windows LAPS (in-box on Server 2019+, Win10 20H2+, Win11) sets a unique random password on each machine, stored in AD encrypted, retrievable only by authorized accounts.

# One-time: extend schema (run as Schema Admin)
Update-LapsADSchema

# Grant target OU permission for computers to write their own passwords
Set-LapsADComputerSelfPermission -Identity 'OU=Workstations,DC=corp,DC=local'

# Authorize a helpdesk group to read passwords
Set-LapsADReadPasswordPermission `
    -Identity 'OU=Workstations,DC=corp,DC=local' `
    -AllowedPrincipals 'CORP\Tier2-Helpdesk'

# Audit who currently has read permission
Find-LapsADExtendedRights -Identity 'OU=Workstations,DC=corp,DC=local'

# Retrieve a password
Get-LapsADPassword -Identity 'WKSTN-042' -AsPlainText

# Force immediate rotation (after use)
Reset-LapsPassword -Identity 'WKSTN-042'

Configure via GPO: PasswordComplexity=4, PasswordLength>=20, PasswordAgeDays=30. Pilot one OU before fleet-wide rollout — misconfigured LAPS scoping can leave machines without a known local admin password.

Rotate krbtgt — Twice, 10+ Hours Apart

The krbtgt account signs every Kerberos ticket in the domain. An attacker who DCSyncs its hash can forge Golden Tickets — authentication tickets valid for any user (including non-existent ones) for up to 10 years. Once they have the hash, the domain is functionally compromised forever unless rotated. Microsoft recommends resetting twice, waiting at least 10 hours between resets, to invalidate existing tickets and prevent Golden Ticket attacks.

The wait exists because Kerberos accepts tickets signed with the previous password until they expire — two resets less than 10 hours apart wipes the fallback before tickets expire and breaks authentication forest-wide.

Check current state:

Get-ADUser krbtgt -Properties PasswordLastSet, 'msds-keyversionnumber' |
    Select-Object Name, PasswordLastSet,
        @{n='KeyVersion';e={ $_.'msds-keyversionnumber' }},
        @{n='AgeDays';e={ (New-TimeSpan -Start $_.PasswordLastSet -End (Get-Date)).Days }}

If “never” or >180 days: schedule rotation. Use Microsoft’s official New-KrbtgtKeys.ps1 — it handles RWDC and RODC scenarios correctly. Don’t write your own.

# Rough sequence (read the official script's docs first)
.\New-KrbtgtKeys.ps1 -Mode Single   # First reset
# Wait 10-24 hours. Monitor event IDs 4769, 4771 on DCs for app breakage.
.\New-KrbtgtKeys.ps1 -Mode Single   # Second reset

Then schedule twice per year indefinitely, plus an immediate reset on Tier 0 admin departure or suspected compromise.

Reset DSRM (the local break-glass admin on each DC) the same season:

# On each DC, elevated
ntdsutil "set dsrm password" "reset password on server null" q q

Store the DSRM passwords in your secrets vault, segmented from production credentials — they’re your last line of recovery if a tier silo locks everyone out.

Kill Unconstrained Delegation

A host with unconstrained delegation receives a copy of every Kerberos TGT presented to it — including from domain admins who connect to that host. Compromise of any such host is a fast path to Tier 0 because an attacker can simply wait for a privileged user to authenticate, then extract their TGT.

# Find computers with unconstrained delegation, excluding DCs (PrimaryGroupID 516)
Get-ADComputer -Filter {TrustedForDelegation -eq $true -and PrimaryGroupID -ne 516} `
    -Properties TrustedForDelegation, OperatingSystem |
    Select-Object Name, OperatingSystem, TrustedForDelegation

# User accounts with unconstrained delegation
Get-ADUser -Filter {TrustedForDelegation -eq $true} `
    -Properties TrustedForDelegation, ServicePrincipalName |
    Select-Object Name, SamAccountName

# Remove unconstrained delegation from a computer
Set-ADComputer -Identity 'SRV-LEGACY01' -TrustedForDelegation $false

Convert to constrained delegation (specific SPNs allowed) or resource-based constrained delegation (RBCD) where the application supports it. Anything that “needs” unconstrained delegation deserves an architecture review.

Hunt DCSync Rights and ACL Abuse

DS-Replication-Get-Changes-All on the domain root is the right that lets a domain controller replicate AD data — including password hashes. Any account holding it can silently extract every hash in the domain. Find every account that holds it:

$dcsyncGuids = @(
    '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2',  # Get-Changes
    '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2',  # Get-Changes-All
    '89e95b76-444d-4c62-991a-0facbeda640c'   # Get-Changes-In-Filtered-Set
)

$domain = (Get-ADDomain).DistinguishedName
$acl = Get-Acl "AD:$domain"

$acl.Access | Where-Object {
    $_.AccessControlType -eq 'Allow' -and
    $dcsyncGuids -contains $_.ObjectType.ToString()
} | Select-Object IdentityReference, ActiveDirectoryRights, ObjectType

Expected results: domain controllers, Enterprise Domain Controllers, Administrators, and SYSTEM-equivalent identities. Anything else (a user account, a custom group, a service account) needs justification or removal. BloodHound surfaces the multi-hop ACL chains that grant equivalent power without direct DCSync rights — run it.

Protocol Cleanup: SMBv1 and LDAP Signing

# SMBv1 - remove
Get-WindowsFeature FS-SMB1
Uninstall-WindowsFeature -Name FS-SMB1 -Remove
# On clients
Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart

# Audit current LDAP signing requirement on DCs
Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' `
    -Name LDAPServerIntegrity -ErrorAction SilentlyContinue
# Value 1 = negotiate (weak), 2 = require signing (target state)

# Find clients still doing unsigned LDAP binds (Directory Service log on DCs)
Get-WinEvent -LogName 'Directory Service' -FilterXPath "*[System[EventID=2889]]" -MaxEvents 500 |
    Select-Object TimeCreated, @{n='Client';e={$_.Properties[0].Value}}, `
        @{n='User';e={$_.Properties[1].Value}}

# Set required signing via GPO on Domain Controllers OU:
# Computer Config > Policies > Windows Settings > Security Settings >
# Local Policies > Security Options >
# "Domain controller: LDAP server signing requirements" = Require signing

Stage this: audit mode first using event 2889, fix the non-compliant clients (often old appliances, scanners, or printers binding with anonymous LDAP), then enforce. LDAP signing and channel binding together stop NTLM-relay attacks against domain controllers — the foundation of ADV190023.

Audit Policy and Detection

Without monitoring, every control above is a snapshot — true on the day you set it, possibly untrue a week later. Enable advanced audit on all DCs and high-value member servers, forward to a SIEM with 90+ days retention.

# Minimum audit policy for DCs
auditpol /set /subcategory:"Credential Validation"     /success:enable /failure:enable
auditpol /set /subcategory:"Kerberos Authentication Service" /success:enable /failure:enable
auditpol /set /subcategory:"Kerberos Service Ticket Operations" /success:enable /failure:enable
auditpol /set /subcategory:"User Account Management"  /success:enable /failure:enable
auditpol /set /subcategory:"Security Group Management" /success:enable /failure:enable
auditpol /set /subcategory:"Directory Service Access"  /success:enable /failure:enable
auditpol /set /subcategory:"Directory Service Changes" /success:enable /failure:enable
auditpol /set /subcategory:"Logon"                    /success:enable /failure:enable
auditpol /set /subcategory:"Special Logon"            /success:enable /failure:enable
auditpol /set /subcategory:"Sensitive Privilege Use"  /success:enable /failure:enable

# Verify
auditpol /get /category:*
DETECTION REFERENCE
High-Signal Event IDs
4769
Kerberoasting
TGS-REQ with RC4 (type 0x17) for non-machine accounts; bulk requests from one source.
4662
DCSync
Replication GUID access by accounts not on DC allow-list.
4768
AS-REP Roasting
TGT requests with no pre-auth on accounts that should require it.
4625
Password Spray
Many account failures from one source; status 0xC000006A.
4728 / 4732
Priv Group Change
Member added to Domain Admins, Enterprise Admins, Administrators. Real-time alert.
5136
AdminSDHolder
ACL modification on AdminSDHolder — classic admin-rights persistence.
2887 / 2889
Unsigned LDAP
Directory Service log on DCs — clients still binding without signing.
4820
Silo Violation
Auth Policy Silo blocked a logon — use to tune in audit mode before enforce.

Microsoft Defender for Identity provides these detections tuned and curated. On a budget: Sysmon + Wazuh or Elastic Security covers the high-signal set.

What This Guide Defers

Three areas demand their own focused projects rather than a paragraph here. ADCS hardening — if you run Active Directory Certificate Services, audit it with Certipy for the ESC1–ESC15 misconfiguration classes; certificate template abuse is one of the most reliable domain compromise paths in 2026. Entra ID / hybrid identity — Conditional Access, PIM, Identity Protection, and treating Entra Connect as Tier 0. Privileged Access Workstations — the structural answer to credential exposure, expensive and operationally demanding, but eventually unavoidable for Tier 0.

FAQ

Order of operations on a brand-new environment? Run BloodHound and PingCastle. Inventory privileged groups. Deploy LAPS. Reset krbtgt after stabilizing service accounts. Roll out Auth Policy Silos in audit mode. Anything beyond that in week one risks breakage without security gain.

Protected Users with MFA — redundant? No. MFA protects the auth event; Protected Users protects what happens after (caching, delegation, weak ciphers). They stack.

2008 R2 domain functional level? Upgrade first. Protected Users, gMSAs, modern LAPS, and Auth Policy Silos all require 2012 R2 minimum.

BloodHound safe in production? Yes. Read-only LDAP queries. Run quarterly to measure drift.

The Stance

The PowerShell runs in minutes. The hard part is the conversation with the application owner whose product breaks when you remove SMBv1, the exec who has been a Domain Admin since 2014, the backup vendor whose architecture quietly makes their server Tier 0. Run BloodHound this week. Map your tier boundaries next week. Twelve months from now, the attacker who would have reached Domain Admin in an afternoon will spend weeks failing — and triggering alerts every step of the way.

Add a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Cybersecurity intelligence delivered directly to your inbox.

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Advertisement