---
title: "Active Directory Hardening Is a Sequence, Not a Score"
description: "A scanner score cannot tell you whether a helpdesk account can still reach Domain Admin. This is the order I use to turn AD hardening signals into broken attack paths, tested controls, and recoverable identity infrastructure."
date: 2026-06-18
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/active-directory-hardening-sequence/
---

## Active Directory hardening in 60 seconds

An Active Directory assessment usually ends with a large number. Sometimes it is a score,
sometimes a maturity level, and sometimes a red dashboard that slowly becomes green. The number is
useful for tracking work. It is not proof that the directory is hard to compromise.

I have seen the same disconnect repeatedly in hardening reviews: dozens of settings are corrected,
stale users are removed, password policy improves, and the shortest path from a support account to
domain control still survives. The path may run through an old group nesting, a certificate
template, a server where a privileged administrator signs in, or a service account whose password
has not changed since the application was installed.

That is why I do not start an AD hardening programme with the scanner's highest-scoring rule. I
start with a simpler question:

> What can a normal user, a compromised workstation, or a routine server administrator control
> today that eventually controls the domain?

Hardening is the work of making every answer to that question shorter, rarer, more visible, and
finally impossible.

## The offensive starting point: one user, one workstation

I frame the review around a representative, explicitly synthetic casefile. It is not presented as
a customer incident. It is a compact model of the paths that repeatedly survive otherwise mature
hardening programmes.

The assumed attacker has no exploit and no administrator credential. They have one normal domain
user and execution on that user's workstation. From there, the review asks whether they can convert
one form of access into another:

1. the user is nested into an old helpdesk group;
2. that group can read Windows LAPS passwords for a management-server OU;
3. one of those servers receives interactive logons from identity administrators;
4. the same management plane can modify a certificate template or operate on a Tier 0 system;
5. a durable directory permission turns the temporary workstation compromise into repeatable
   domain control.

No single step has to be labelled “Critical” by a scanner. The danger is the composition. The
hardening objective is therefore not to hide the path or delete the user from a screenshot. It is
to break the earliest durable edge and prevent the privileged credential from appearing on the
lower-tier host in the first place.

<figure class="diagram">
<svg viewBox="0 0 760 326" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Representative offensive Active Directory path. A normal domain user reaches a legacy helpdesk group, reads a LAPS password for a management server, encounters a Tier 0 administrator session, then reaches certificate authority control and domain control. Defensive cut points are shown under each edge.">
<defs><marker id="case-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="24" class="dg-crit">REPRESENTATIVE OFFENSIVE CASEFILE</text>
<rect x="0" y="54" width="126" height="74" rx="9" class="dg-box"/><text x="14" y="81" class="dg-label">Domain user</text><text x="14" y="104" class="dg-muted">workstation</text>
<line x1="128" y1="91" x2="160" y2="91" class="dg-line-crit" marker-end="url(#case-a)"/>
<rect x="164" y="54" width="126" height="74" rx="9" class="dg-box"/><text x="178" y="81" class="dg-label">Helpdesk</text><text x="178" y="104" class="dg-muted">nested group</text>
<line x1="292" y1="91" x2="324" y2="91" class="dg-line-crit" marker-end="url(#case-a)"/>
<rect x="328" y="54" width="126" height="74" rx="9" class="dg-box"/><text x="342" y="81" class="dg-label">LAPS read</text><text x="342" y="104" class="dg-muted">mgmt server</text>
<line x1="456" y1="91" x2="488" y2="91" class="dg-line-crit" marker-end="url(#case-a)"/>
<rect x="492" y="54" width="126" height="74" rx="9" class="dg-box-crit"/><text x="506" y="81" class="dg-label">T0 session</text><text x="506" y="104" class="dg-crit">credential exposure</text>
<line x1="620" y1="91" x2="652" y2="91" class="dg-line-crit" marker-end="url(#case-a)"/>
<rect x="656" y="54" width="104" height="74" rx="9" class="dg-box-crit"/><text x="670" y="81" class="dg-label">Domain</text><text x="670" y="104" class="dg-crit">control</text>
<line x1="226" y1="132" x2="226" y2="176" class="dg-line"/><rect x="162" y="180" width="128" height="58" rx="8" class="dg-box-accent"/><text x="176" y="204" class="dg-accent">CUT 1</text><text x="176" y="224" class="dg-muted">remove nesting</text>
<line x1="390" y1="132" x2="390" y2="176" class="dg-line"/><rect x="326" y="180" width="128" height="58" rx="8" class="dg-box-accent"/><text x="340" y="204" class="dg-accent">CUT 2</text><text x="340" y="224" class="dg-muted">narrow LAPS ACL</text>
<line x1="554" y1="132" x2="554" y2="176" class="dg-line"/><rect x="490" y="180" width="128" height="58" rx="8" class="dg-box-accent"/><text x="504" y="204" class="dg-accent">CUT 3</text><text x="504" y="224" class="dg-muted">block T0 logon</text>
<text x="0" y="282" class="dg-muted">ENTRY ASSUMPTION</text><text x="146" y="282" class="dg-mono">one standard user + one user workstation</text>
<text x="0" y="306" class="dg-accent">PASS CONDITION</text><text x="146" y="306" class="dg-mono">every independently tested route to Tier 0 ends in a recorded denial</text>
</svg>
<figcaption>The casefile gives every control a purpose. Group cleanup, LAPS ACLs, tiered logons, and PKI review are not separate checklist items; they are independent cut points in the same path.</figcaption>
</figure>

### Scope and safety contract

The commands in this article are deliberately read-only. Run them only in an authorized domain,
preferably with a dedicated standard audit account and from the approved assessment host. They
enumerate directory objects, permissions, configuration, and event records. They do not request
certificates, relay authentication, modify templates, change group membership, retrieve managed
password values, or attempt credential extraction.

Tool output remains a candidate until the path's effective permission, reachable target, protected
consumer, and business impact are confirmed. The offensive lens decides what to investigate; it
does not turn every red label into a finding.

## What I mean by “hardened”

A hardened directory is not one with every legacy feature disabled. It is one where the following
statements have evidence behind them:

- ordinary user and workstation compromise cannot expose control-plane credentials;
- server administrators cannot silently become domain administrators through group nesting, ACLs,
  delegation, certificates, backup infrastructure, or management tooling;
- service identities have narrow rights, managed credentials, and a known set of hosts that may use
  them;
- authentication protocols reject the relay and downgrade paths the estate claims to have closed;
- privileged changes produce telemetry that someone reviews;
- the organization can restore a clean forest without depending on the compromised forest itself.

That definition changes the unit of work. “Enable LDAP signing” is a task. “Unsigned LDAP binds are
rejected, the incompatible clients have named owners, and relay validation now fails” is a tested
control.

<figure class="diagram">
<svg viewBox="0 0 760 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A checklist model closes independent settings but leaves a connected path to domain control. An attack-path model follows the complete chain from a workstation through credentials, server administration, and certificate authority permissions, then breaks the earliest durable edge.">
<defs><marker id="adh-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker><marker id="adh-c" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="24" class="dg-muted">CHECKLIST VIEW</text>
<rect x="0" y="44" width="164" height="54" rx="8" class="dg-box"/><text x="16" y="68" class="dg-label">Password policy</text><text x="16" y="87" class="dg-accent">PASS</text>
<rect x="180" y="44" width="164" height="54" rx="8" class="dg-box"/><text x="196" y="68" class="dg-label">SMB signing</text><text x="196" y="87" class="dg-accent">PASS</text>
<rect x="360" y="44" width="164" height="54" rx="8" class="dg-box"/><text x="376" y="68" class="dg-label">Stale users</text><text x="376" y="87" class="dg-accent">PASS</text>
<rect x="540" y="44" width="220" height="54" rx="8" class="dg-box-crit"/><text x="556" y="68" class="dg-label">Domain path</text><text x="556" y="87" class="dg-crit">STILL OPEN</text>
<text x="0" y="140" class="dg-accent">ATTACK-PATH VIEW</text>
<rect x="0" y="162" width="132" height="62" rx="8" class="dg-box"/><text x="14" y="188" class="dg-label">Workstation</text><text x="14" y="208" class="dg-muted">user context</text>
<line x1="134" y1="193" x2="176" y2="193" class="dg-line-crit" marker-end="url(#adh-c)"/>
<rect x="180" y="162" width="132" height="62" rx="8" class="dg-box"/><text x="194" y="188" class="dg-label">Helpdesk</text><text x="194" y="208" class="dg-muted">nested group</text>
<line x1="314" y1="193" x2="356" y2="193" class="dg-line-crit" marker-end="url(#adh-c)"/>
<rect x="360" y="162" width="132" height="62" rx="8" class="dg-box"/><text x="374" y="188" class="dg-label">PKI admin</text><text x="374" y="208" class="dg-muted">template ACL</text>
<line x1="494" y1="193" x2="536" y2="193" class="dg-line-crit" marker-end="url(#adh-c)"/>
<rect x="540" y="162" width="220" height="62" rx="8" class="dg-box-crit"/><text x="556" y="188" class="dg-label">Domain control</text><text x="556" y="208" class="dg-crit">certificate logon</text>
<line x1="330" y1="148" x2="330" y2="238" class="dg-line"/><rect x="304" y="234" width="52" height="30" rx="6" class="dg-box-accent"/><text x="315" y="254" class="dg-accent">CUT</text>
<text x="0" y="286" class="dg-muted">The useful remediation removes the durable permission that connects the chain—not the screenshot at its end.</text>
</svg>
<figcaption>A green setting can coexist with a complete compromise path. I use the path to decide which permission, credential exposure, or protocol edge must be removed first.</figcaption>
</figure>

## First, draw the real control plane

“Tier 0” is often reduced to domain controllers and Domain Admins. That inventory is too small.
The practical definition is straightforward: if compromising a system lets an attacker control
domain controllers, change authentication, mint identities, or restore a compromised directory,
that system belongs in the control plane.

That normally includes:

- domain controllers, forest and domain administration groups, and their delegated operators;
- AD CS certification authorities, certificate templates, PKI administration, and enrollment
  services capable of producing authentication certificates;
- Entra Connect, federation services, identity lifecycle platforms, password reset systems, and
  privileged access tooling;
- backup systems holding domain-controller data and the accounts able to restore it;
- hypervisors, management agents, patch platforms, EDR consoles, and monitoring systems that can
  execute code on domain controllers;
- the workstations and jump hosts from which any of those systems are administered.

This exercise usually changes the programme. A company may have two well-protected domain
controllers and an ordinary virtualization team that can mount their disks. It may restrict Domain
Admins while a backup operator can restore `ntds.dit`. It may protect privileged users with
dedicated workstations while the same users manage the certificate authority from a daily laptop.

The domain is only as isolated as the least protected system that can control it.

### Read-only control-plane baseline

Start by resolving the built-in privileged groups by SID instead of assuming their English names.
The following inventory expands nested membership and records the objects; it changes nothing.

```powershell
Import-Module ActiveDirectory

$domain = Get-ADDomain
$forest = Get-ADForest
$domainAdminSid = "$($domain.DomainSID.Value)-512"
$enterpriseAdminSid = "$((Get-ADDomain -Identity $forest.RootDomain).DomainSID.Value)-519"

$tier0Groups = @(
  Get-ADGroup -Identity $domainAdminSid
  Get-ADGroup -Identity $enterpriseAdminSid
)

$tier0Groups | ForEach-Object {
  $group = $_
  Get-ADGroupMember -Identity $group -Recursive |
    Select-Object @{n='PrivilegedGroup';e={$group.SamAccountName}},
                  objectClass, SamAccountName, DistinguishedName
} | Sort-Object PrivilegedGroup, objectClass, SamAccountName
```

Then extend that inventory beyond groups. Record systems that can execute on domain controllers,
restore their data, mint authentication certificates, synchronize identities, reset privileged
credentials, or administer the virtualization layer. Those relationships often live outside AD,
so no LDAP query or BloodHound collection can prove the inventory complete.

For protected-object inheritance, inspect `AdminSDHolder` rather than changing it during discovery:

```powershell
$adminSdHolder = "AD:CN=AdminSDHolder,CN=System,$($domain.DistinguishedName)"
(Get-Acl $adminSdHolder).Access |
  Select-Object IdentityReference, ActiveDirectoryRights,
                AccessControlType, ObjectType, IsInherited |
  Sort-Object IdentityReference
```

An unexpected ACE is a lead. Before reporting it, resolve nested membership, confirm the right is
effective, identify which protected objects inherit or retain it, and show the privileged action it
would authorize.

## Separate administration by where the credential may appear

Administrative tiering fails when it is treated as an organizational chart. Creating “Tier 0
Admins” and “Server Admins” groups does not help if the same person signs in to both environments
with the same account from the same workstation.

The control I care about is where a credential can be presented, cached, delegated, or stolen.

For Tier 0 work, use a separate privileged identity and a dedicated privileged access workstation.
Block that identity from interactive and network logon to lower-tier servers and user devices.
Do not browse email, open documents, administer ordinary applications, or use the account as a
service identity. Emergency accounts should be few, independently monitored, and tested rather than
assumed to work.

The negative control is important. A Tier 0 account should successfully reach its approved
management path and fail when used on a Tier 1 server or normal workstation. Without that failed
logon, the design exists only in group names and documentation.

Use logon telemetry to challenge the architecture. The query below collects successful logons and
special-privilege assignments from an approved event source; it does not attempt authentication.

```powershell
$start = (Get-Date).AddDays(-14)
Get-WinEvent -ComputerName 'dc01.corp.local' -FilterHashtable @{
  LogName   = 'Security'
  Id        = 4624,4672
  StartTime = $start
} | Select-Object TimeCreated, Id, MachineName, Message
```

The review question is not merely whether event `4672` exists. Correlate the account, logon type,
source host, destination, and approved administration route. A Tier 0 identity appearing from a
general server or user workstation is evidence that the tier boundary failed operationally.

<figure class="diagram">
<svg viewBox="0 0 760 324" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Three administrative tiers. A dedicated privileged workstation reaches the control plane. Server administration remains in the management plane and workstation support remains in the user-device plane. Cross-tier logons are blocked in the upward direction to prevent credential theft.">
<defs><marker id="tier-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<rect x="0" y="26" width="760" height="76" rx="10" class="dg-box-accent"/><text x="18" y="54" class="dg-accent">CONTROL PLANE / TIER 0</text><text x="18" y="78" class="dg-label">DC · AD CS · identity sync · recovery · management of DCs</text><rect x="604" y="42" width="138" height="42" rx="7" class="dg-box"/><text x="620" y="68" class="dg-mono">T0 PAW + T0 ID</text>
<rect x="0" y="124" width="760" height="76" rx="10" class="dg-box"/><text x="18" y="152" class="dg-muted">MANAGEMENT PLANE / TIER 1</text><text x="18" y="176" class="dg-label">member servers · applications · server operations</text><rect x="604" y="140" width="138" height="42" rx="7" class="dg-box"/><text x="620" y="166" class="dg-mono">T1 PAW + T1 ID</text>
<rect x="0" y="222" width="760" height="76" rx="10" class="dg-box"/><text x="18" y="250" class="dg-muted">USER ACCESS / TIER 2</text><text x="18" y="274" class="dg-label">workstations · helpdesk · daily user activity</text><rect x="604" y="238" width="138" height="42" rx="7" class="dg-box"/><text x="620" y="264" class="dg-mono">USER DEVICE</text>
<line x1="680" y1="140" x2="680" y2="92" class="dg-line" marker-end="url(#tier-a)"/><line x1="648" y1="238" x2="648" y2="190" class="dg-line" marker-end="url(#tier-a)"/>
<text x="396" y="116" class="dg-crit">NO T0 CREDENTIAL ON T1</text><text x="396" y="214" class="dg-crit">NO ADMIN CREDENTIAL ON USER DEVICE</text>
</svg>
<figcaption>The separation is about credential exposure, not job titles. A higher-tier identity must never authenticate to a lower-tier system that could capture it.</figcaption>
</figure>

## Remove stored privilege before rotating passwords

Password rotation is visible and easy to measure. Old permissions are quieter and often more
important. A twenty-character password does not protect a group that can add itself to another
group, rewrite a GPO linked to domain controllers, read a managed service password, or change a
certificate template.

I inventory privilege in four places:

1. membership of built-in and delegated administrative groups, including nesting;
2. ACLs on the domain root, OUs, groups, GPOs, admin accounts, and certificate templates;
3. local administration and remote-management rights on Tier 0 and Tier 1 systems;
4. temporary facts such as privileged sessions, which explain credential exposure but should not
   be confused with durable permissions.

From an offensive perspective, I rank edges by three properties: **privilege gained**, **how long
the edge survives**, and **whether the target is reachable from the assumed foothold**. A permanent
`WriteDacl` right on an OU usually deserves attention before a session observed once on a laptop,
even when the session produces a shorter graph path.

<figure class="diagram">
<svg viewBox="0 0 760 322" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Offensive prioritization matrix. The horizontal axis represents how long an Active Directory edge persists, from minutes to years. The vertical axis represents privilege gained. Sessions are high privilege but short lived, while WriteDacl, GPO control, LAPS read rights, gMSA read rights, and certificate template ownership are durable and high impact.">
<line x1="92" y1="270" x2="730" y2="270" class="dg-axis"/><line x1="92" y1="270" x2="92" y2="42" class="dg-axis"/>
<text x="294" y="310" class="dg-muted">DURABILITY OF THE EDGE →</text><text x="0" y="32" class="dg-muted">PRIVILEGE</text><text x="0" y="50" class="dg-muted">GAINED ↑</text>
<line x1="92" y1="160" x2="730" y2="160" class="dg-line"/><line x1="402" y1="42" x2="402" y2="270" class="dg-line"/>
<text x="112" y="292" class="dg-mono">minutes</text><text x="640" y="292" class="dg-mono">years</text>
<circle cx="190" cy="88" r="13" class="dg-dot"/><text x="210" y="93" class="dg-label">Tier 0 session</text>
<circle cx="310" cy="204" r="11" class="dg-dot"/><text x="330" y="209" class="dg-muted">temporary local admin</text>
<circle cx="488" cy="108" r="13" class="dg-dot-accent"/><text x="508" y="113" class="dg-label">ReadLAPSPassword</text>
<circle cx="540" cy="76" r="13" class="dg-dot-accent"/><text x="560" y="81" class="dg-label">GPO control</text>
<circle cx="574" cy="142" r="13" class="dg-dot-accent"/><text x="594" y="147" class="dg-label">gMSA read</text>
<circle cx="612" cy="54" r="14" class="dg-dot-accent"/><text x="632" y="59" class="dg-accent">Template owner</text>
<circle cx="662" cy="116" r="13" class="dg-dot-accent"/><text x="598" y="101" class="dg-label">WriteDacl</text>
<text x="108" y="146" class="dg-crit">VALIDATE NOW</text><text x="422" y="254" class="dg-accent">REMEDIATE AS STANDING CONTROL FAILURE</text>
</svg>
<figcaption>A short-lived session can validate exposure, but durable ACL and ownership edges recreate the path every day. Hardening should remove the standing edge, then prevent privileged sessions from appearing below their tier.</figcaption>
</figure>

### Enumerate the permissions attackers chain

The native queries below surface delegation and service-account candidates without exercising
them:

```powershell
# Unconstrained delegation on computers
Get-ADComputer -Filter 'TrustedForDelegation -eq $true' `
  -Properties TrustedForDelegation,PrimaryGroupID |
  Select-Object Name,DistinguishedName,TrustedForDelegation,PrimaryGroupID

# Classic constrained delegation on any directory object
Get-ADObject -LDAPFilter '(msDS-AllowedToDelegateTo=*)' `
  -Properties msDS-AllowedToDelegateTo |
  Select-Object Name,ObjectClass,DistinguishedName,msDS-AllowedToDelegateTo

# Resource-based constrained delegation descriptors
Get-ADObject -LDAPFilter '(msDS-AllowedToActOnBehalfOfOtherIdentity=*)' `
  -Properties msDS-AllowedToActOnBehalfOfOtherIdentity |
  Select-Object Name,ObjectClass,DistinguishedName
```

For durable graph paths, constrain BloodHound to permission edges instead of allowing transient
sessions to dominate the result:

```cypher
MATCH p = shortestPath(
  (n)-[:MemberOf|Owns|GenericAll|GenericWrite|WriteDacl|WriteOwner|
       AddMember|ForceChangePassword|AllExtendedRights|ReadLAPSPassword|
       ReadGMSAPassword*1..8]->(g:Group)
)
WHERE toUpper(g.name) STARTS WITH 'DOMAIN ADMINS@'
RETURN p
```

The graph is the hypothesis. For each edge, return to the raw security descriptor, resolve nested
groups, verify inheritance and object scope, and confirm the target is reachable from the approved
test position. If the path depends on a session, capture its time and recollect before claiming it
is repeatable.

This is where a graph is useful, with one warning: the shortest path is not automatically the best
finding. A live session may disappear in minutes; `WriteDacl` on an OU may survive for years. I
separate durable permissions from temporary sessions and remediate the durable edge that makes the
path repeatable. The detailed reasoning is in [the BloodHound edge triage note](/posts/bloodhound-edge-triage/).

Each delegated permission should have a named owner, business purpose, approved principal, scope,
review date, and expiry where possible. “Inherited from an old project group” is not ownership.

## Treat service accounts as paths, not password exceptions

Traditional service accounts accumulate three properties attackers value: a stable password, an
SPN that allows offline password guessing, and privileges nobody wants to retest because the
application is fragile.

Where the application supports it, migrate to a gMSA so Windows manages the password and SPN.
Then review the permission that determines which computers can retrieve that managed password.
Moving to gMSA eliminates manual password rotation; it does not make an over-broad
`PrincipalsAllowedToRetrieveManagedPassword` harmless.

For every remaining user-based service account, record:

- the service and hosts that use it;
- interactive and remote logon restrictions;
- SPNs and supported Kerberos encryption types;
- direct and nested group memberships;
- local administrator rights and access to network resources;
- the last successful rotation and the tested rollback plan;
- an accountable application owner and a retirement or migration decision.

The priority is not simply “accounts with SPNs.” It is accounts whose offline-crackable credential
opens a privileged second hop. That distinction is why [Kerberoasting triage](/posts/kerberoasting-triage/)
starts with privilege and service ownership rather than crack speed.

### Inventory the credential readers—not the credentials

The following commands list service identities and the principals allowed to retrieve managed
passwords. They intentionally do not call `Get-LapsADPassword` and do not retrieve a gMSA password.

```powershell
# User-based service identities with SPNs
Get-ADUser -LDAPFilter '(&(objectCategory=person)(objectClass=user)(servicePrincipalName=*))' `
  -Properties ServicePrincipalName,Enabled,LastLogonDate,
              PasswordLastSet,msDS-SupportedEncryptionTypes |
  Select-Object SamAccountName,Enabled,LastLogonDate,PasswordLastSet,
                msDS-SupportedEncryptionTypes,ServicePrincipalName

# gMSAs and the hosts/groups allowed to retrieve each managed password
Get-ADServiceAccount -Filter * `
  -Properties PrincipalsAllowedToRetrieveManagedPassword,
              ServicePrincipalNames,msDS-SupportedEncryptionTypes |
  Select-Object Name,Enabled,PrincipalsAllowedToRetrieveManagedPassword,
                msDS-SupportedEncryptionTypes,ServicePrincipalNames

# Extended rights over a Windows LAPS-managed OU
Find-LapsADExtendedRights -Identity 'OU=Workstations,DC=corp,DC=local'
```

For every reader, ask what compromise of that principal gives an attacker next. A workstation
group allowed to retrieve a gMSA used only on web servers may be a serious but bounded error. The
same reader over an identity-management service or backup platform can be a direct control-plane
path. The secret type is not the severity; the consumer is.

## Migrate authentication protocols with evidence, not hope

The protocol layer is where hardening programmes most often choose between two bad approaches:
enforce everything in one maintenance window and break legacy applications, or audit indefinitely
and never enforce anything.

Use a measured migration instead:

1. enable the relevant LDAP, NTLM, and authentication auditing;
2. identify the client, service, account, destination, and owner behind each legacy event;
3. fix or isolate the dependency;
4. enforce the control on a pilot scope;
5. verify both a successful modern authentication and a rejected legacy attempt;
6. expand enforcement and keep an exception register with an expiry date.

### Build the relay map before changing policy

An attacker does not ask whether “signing is enabled in the domain.” They ask which source can be
coerced to authenticate over which transport, which destination will accept that authentication,
and what the captured identity may change there. The defensive inventory must use the same three
columns.

<figure class="diagram">
<svg viewBox="0 0 760 330" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Offensive relay map. Authentication sources on workstations and servers can emit SMB or HTTP authentication. Destinations include SMB, LDAP, LDAPS, and AD CS web enrollment. SMB signing, LDAP signing, channel binding, Extended Protection for Authentication, and NTLM retirement are placed on the specific edges they close.">
<defs><marker id="proto-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="24" class="dg-crit">OFFENSIVE RELAY MAP</text>
<rect x="0" y="54" width="176" height="92" rx="9" class="dg-box"/><text x="16" y="82" class="dg-label">Authentication source</text><text x="16" y="106" class="dg-mono">workstation · server</text><text x="16" y="128" class="dg-muted">SMB / HTTP</text>
<line x1="178" y1="78" x2="306" y2="78" class="dg-line-crit" marker-end="url(#proto-a)"/><text x="204" y="66" class="dg-muted">SMB</text>
<line x1="178" y1="122" x2="306" y2="164" class="dg-line-crit" marker-end="url(#proto-a)"/><text x="204" y="146" class="dg-muted">HTTP</text>
<rect x="310" y="48" width="170" height="62" rx="9" class="dg-box"/><text x="326" y="74" class="dg-label">SMB target</text><text x="326" y="96" class="dg-accent">require signing</text>
<rect x="310" y="130" width="170" height="76" rx="9" class="dg-box-crit"/><text x="326" y="156" class="dg-label">LDAP / LDAPS</text><text x="326" y="178" class="dg-accent">signing + CBT</text><text x="326" y="196" class="dg-muted">separate controls</text>
<line x1="482" y1="168" x2="532" y2="168" class="dg-line-crit" marker-end="url(#proto-a)"/>
<rect x="536" y="130" width="224" height="76" rx="9" class="dg-box-crit"/><text x="552" y="156" class="dg-label">Directory change</text><text x="552" y="178" class="dg-crit">RBCD · group · template</text><text x="552" y="196" class="dg-muted">impact depends on identity</text>
<line x1="178" y1="132" x2="306" y2="246" class="dg-line-crit" marker-end="url(#proto-a)"/>
<rect x="310" y="222" width="170" height="76" rx="9" class="dg-box-crit"/><text x="326" y="248" class="dg-label">AD CS HTTP(S)</text><text x="326" y="270" class="dg-accent">TLS + EPA</text><text x="326" y="288" class="dg-muted">remove HTTP where possible</text>
<line x1="482" y1="260" x2="532" y2="260" class="dg-line-crit" marker-end="url(#proto-a)"/>
<rect x="536" y="222" width="224" height="76" rx="9" class="dg-box-crit"/><text x="552" y="248" class="dg-label">Authentication cert</text><text x="552" y="270" class="dg-crit">identity persistence</text><text x="552" y="288" class="dg-muted">if enrollment is exposed</text>
<text x="0" y="322" class="dg-muted">One global “NTLM risk” rating hides the edge that actually remains open. Record source, transport, destination, identity, and outcome.</text>
</svg>
<figcaption>Controls close specific edges. SMB signing does not protect LDAP, TLS alone does not provide LDAP channel binding, and an AD CS web endpoint needs its own Extended Protection and enrollment review.</figcaption>
</figure>

Collect the full event record, not only a count. Microsoft documents LDAP signing events in the
`2886–2889` range and channel-binding events in the `3039–3041` range. Their exact message and
availability depend on the Windows Server generation and configured diagnostics, so preserve the
event text with the ID.

```powershell
$start = (Get-Date).AddDays(-14)
Get-WinEvent -ComputerName 'dc01.corp.local' -FilterHashtable @{
  LogName   = 'Directory Service'
  Id        = 2886,2887,2888,2889,3039,3040,3041
  StartTime = $start
} | Select-Object TimeCreated,Id,MachineName,LevelDisplayName,Message
```

Event `2887` is useful as a summary that unsigned binds still occur; detailed logging is needed to
attribute incompatible clients. Event `3039` and related channel-binding records help identify
clients that cannot supply a valid token. Do not move to enforcement until each recurring client IP,
identity, application, and owner is known—or deliberately isolated as an expiring exception.

LDAP signing and LDAP channel binding solve different problems. Signing protects LDAP message
integrity; channel binding ties authentication to the TLS channel. An LDAPS listener is not
automatically safe from relay merely because traffic is encrypted. Likewise, SMB signing closes an
SMB relay destination but says nothing about HTTP-to-LDAP relay. The complete protocol model is in
[the NTLM relay note](/posts/ntlm-relay-triage/).

The result I want is not a registry value. It is a rejected unsigned bind tied to the policy change,
plus evidence that the approved client still works.

## Put AD CS inside the same review

AD CS is not an optional appendix to directory hardening. A certification authority that can mint
authentication certificates is an identity control system. Its servers, administrators,
certificate templates, enrollment endpoints, private keys, backup process, and management hosts
belong in the control-plane inventory.

Review more than published template flags. Check who can change each template, who can publish it,
who can administer the CA, who can enroll, which identities a requester may place into a
certificate, whether manager approval or authorized signatures are meaningful, and whether web
enrollment endpoints have current authentication protections.

This is the difference between finding one unsafe template and finding the permission that lets a
routine group make any template unsafe. [The AD CS ownership analysis](/posts/the-template-nobody-owned/)
shows why template ACLs are often the durable root cause.

<figure class="diagram">
<svg viewBox="0 0 760 316" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Active Directory Certificate Services offensive decision chain. A low privileged principal can enroll or modify a template, the certification authority publishes and issues it, domain controllers map the certificate to an identity, and the resulting identity reaches Tier 0. Review gates cover enrollment rights, template write rights, CA administration, issuance constraints, and strong identity mapping.">
<defs><marker id="pki-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="24" class="dg-crit">CERTIFICATE PATH TO DOMAIN CONTROL</text>
<rect x="0" y="54" width="132" height="72" rx="9" class="dg-box"/><text x="14" y="81" class="dg-label">Principal</text><text x="14" y="104" class="dg-muted">enroll / write</text>
<line x1="134" y1="90" x2="170" y2="90" class="dg-line-crit" marker-end="url(#pki-a)"/>
<rect x="174" y="54" width="132" height="72" rx="9" class="dg-box-crit"/><text x="188" y="81" class="dg-label">Template</text><text x="188" y="104" class="dg-crit">identity claims</text>
<line x1="308" y1="90" x2="344" y2="90" class="dg-line-crit" marker-end="url(#pki-a)"/>
<rect x="348" y="54" width="132" height="72" rx="9" class="dg-box"/><text x="362" y="81" class="dg-label">CA issuance</text><text x="362" y="104" class="dg-muted">published policy</text>
<line x1="482" y1="90" x2="518" y2="90" class="dg-line-crit" marker-end="url(#pki-a)"/>
<rect x="522" y="54" width="132" height="72" rx="9" class="dg-box-crit"/><text x="536" y="81" class="dg-label">DC mapping</text><text x="536" y="104" class="dg-crit">certificate → SID</text>
<line x1="656" y1="90" x2="690" y2="90" class="dg-line-crit" marker-end="url(#pki-a)"/>
<rect x="694" y="54" width="66" height="72" rx="9" class="dg-box-crit"/><text x="708" y="81" class="dg-label">T0</text><text x="708" y="104" class="dg-crit">ID</text>
<rect x="24" y="170" width="160" height="88" rx="9" class="dg-box-accent"/><text x="38" y="196" class="dg-accent">GATE 1</text><text x="38" y="218" class="dg-label">Enroll + ACL</text><text x="38" y="240" class="dg-muted">effective principals</text>
<rect x="200" y="170" width="160" height="88" rx="9" class="dg-box-accent"/><text x="214" y="196" class="dg-accent">GATE 2</text><text x="214" y="218" class="dg-label">Template claims</text><text x="214" y="240" class="dg-muted">EKU · subject · approval</text>
<rect x="376" y="170" width="160" height="88" rx="9" class="dg-box-accent"/><text x="390" y="196" class="dg-accent">GATE 3</text><text x="390" y="218" class="dg-label">CA + endpoint</text><text x="390" y="240" class="dg-muted">admins · EPA · HTTP</text>
<rect x="552" y="170" width="184" height="88" rx="9" class="dg-box-accent"/><text x="566" y="196" class="dg-accent">GATE 4</text><text x="566" y="218" class="dg-label">Strong mapping</text><text x="566" y="240" class="dg-muted">SID extension · exceptions</text>
<text x="0" y="302" class="dg-muted">A safe template can become unsafe if its ACL is writable. Review who controls the policy, not only the policy's current flags.</text>
</svg>
<figcaption>The certificate path has multiple independent gates. Enumeration must include who can rewrite the template and CA configuration, not only whether the current template matches an ESC label.</figcaption>
</figure>

### Enumerate AD CS without issuing a certificate

Certipy v5 separates `find` from active commands such as `req`, `auth`, `relay`, `ca`, and
`template`. For a read-only hardening baseline, use only `find`, preserve the JSON evidence, and
review the complete enabled-template set rather than filtering immediately to `-vulnerable`:

```bash
certipy find \
  -u 'audit@corp.local' -k -no-pass \
  -dc-host 'dc01.corp.local' \
  -enabled -json -output 'adcs-baseline'
```

The `-vulnerable` classification understands nested group membership and is useful for triage, but
it remains a tool interpretation. Confirm the template DACL, enrollment principals, issuance
requirements, EKUs, subject construction, CA publication state, CA administration, enrollment
endpoint, and domain-controller certificate mapping independently. This article intentionally stops
before certificate enrollment or authentication testing; those require a separate approved lab
plan and evidence scope.

## Recovery is a security control, not a backup report

A successful backup job proves that bytes were copied. It does not prove the organization can
recover a forest after identity systems, administrators, and management tooling are all considered
untrusted.

A defensible recovery programme answers operational questions:

- Which backup represents the last known-safe directory state?
- Are there at least two regularly backed-up writable domain controllers per domain?
- Are built-in Administrator and DSRM credentials available outside the forest being recovered?
- Can the team rebuild on clean infrastructure without authenticating to compromised management
  systems?
- In what order are the forest root, domains, DNS, SYSVOL, trusts, PKI, and synchronized identity
  services restored?
- When was the procedure last rehearsed, how long did it take, and what failed?

Microsoft's forest recovery guidance is explicit that the operation restores each domain to the
time of the selected trusted backup and loses later directory changes. The difficult decision is
therefore not “do we have a backup?” but “which backup is recent enough to be useful and old enough
to be clean?” That decision requires daily health history and an exercise, not a green backup
console.

## Retest like the attacker, report like an engineer

A remediation is not verified because the responsible team changed a GPO, removed a group member,
or produced a new scanner report. Repeat the original path from the original source context and
require the earliest dangerous action to fail.

I use five states:

1. **candidate** — a tool, ACL, event, or configuration suggests a path;
2. **validated edge** — effective permission and reachability are independently confirmed;
3. **validated path** — the edge reaches a protected consumer or control-plane capability;
4. **remediated** — the root permission, credential exposure, or protocol behavior changed;
5. **retested** — normal behavior succeeds and the original offensive action is denied.

<figure class="diagram">
<svg viewBox="0 0 760 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Active Directory hardening evidence lifecycle. A scanner candidate is manually validated as an effective edge, connected to a protected target, remediated at the root cause, then retested with a positive control and a negative offensive control. Failed retests return to path analysis.">
<defs><marker id="rt-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<text x="0" y="24" class="dg-accent">EVIDENCE LIFECYCLE</text>
<rect x="0" y="58" width="128" height="68" rx="9" class="dg-box"/><text x="14" y="84" class="dg-label">Candidate</text><text x="14" y="106" class="dg-muted">tool signal</text>
<line x1="130" y1="92" x2="160" y2="92" class="dg-line" marker-end="url(#rt-a)"/>
<rect x="164" y="58" width="128" height="68" rx="9" class="dg-box"/><text x="178" y="84" class="dg-label">Edge</text><text x="178" y="106" class="dg-muted">effective right</text>
<line x1="294" y1="92" x2="324" y2="92" class="dg-line" marker-end="url(#rt-a)"/>
<rect x="328" y="58" width="128" height="68" rx="9" class="dg-box-crit"/><text x="342" y="84" class="dg-label">Path</text><text x="342" y="106" class="dg-crit">protected target</text>
<line x1="458" y1="92" x2="488" y2="92" class="dg-line" marker-end="url(#rt-a)"/>
<rect x="492" y="58" width="128" height="68" rx="9" class="dg-box-accent"/><text x="506" y="84" class="dg-label">Root fix</text><text x="506" y="106" class="dg-muted">durable edge cut</text>
<line x1="622" y1="92" x2="652" y2="92" class="dg-line" marker-end="url(#rt-a)"/>
<rect x="656" y="58" width="104" height="68" rx="9" class="dg-box-accent"/><text x="670" y="84" class="dg-label">Retest</text><text x="670" y="106" class="dg-accent">deny</text>
<rect x="184" y="178" width="176" height="70" rx="9" class="dg-box-accent"/><text x="198" y="204" class="dg-accent">POSITIVE CONTROL</text><text x="198" y="228" class="dg-label">approved operation works</text>
<rect x="400" y="178" width="176" height="70" rx="9" class="dg-box-crit"/><text x="414" y="204" class="dg-crit">NEGATIVE CONTROL</text><text x="414" y="228" class="dg-label">original path fails</text>
<line x1="708" y1="130" x2="708" y2="212" class="dg-line"/><line x1="708" y1="212" x2="580" y2="212" class="dg-line" marker-end="url(#rt-a)"/>
<path d="M400 248 C400 282, 228 282, 228 252" class="dg-line-crit" marker-end="url(#rt-a)"/><text x="264" y="290" class="dg-crit">FAIL → REOPEN THE PATH</text>
</svg>
<figcaption>Closure needs both controls: the approved administrative task still works, and the exact unauthorized path now fails. If either result is missing, the finding remains open or the change is unsafe.</figcaption>
</figure>

Use a compact evidence record for every retest:

```text
CLAIM            Tier 0 identities cannot authenticate to Tier 1 servers
SOURCE           approved Tier 1 test host
PRINCIPAL        dedicated Tier 0 test identity
POSITIVE CONTROL Tier 0 PAW → approved DC management succeeds
NEGATIVE CONTROL Tier 1 host → Tier 0 logon is denied
EVIDENCE         policy export + event record + timestamp + tester
RESULT           VERIFIED / FAILED / BLOCKED
```

## If I could do only five things on Monday

1. Build the real control-plane inventory, including AD CS, backup, identity sync, hypervisors, and
   management agents—not only domain controllers.
2. Find every lower-tier host where a Tier 0 identity authenticated during the last fourteen days,
   then block and retest those logon routes.
3. Recalculate durable ACL-only paths to Domain Admins and remove the earliest unnecessary edge.
4. Review who can read Windows LAPS and gMSA passwords and which privileged systems consume those
   credentials.
5. Enumerate all enabled AD CS templates and their write/enrollment principals, then assign a date
   for the next clean-room forest recovery exercise.

## The order I would use for the first 90 days

Trying to remediate every scanner item in parallel creates change without risk reduction. I use a
sequence that closes the highest-value paths first and creates evidence for the next stage.

<figure class="diagram">
<svg viewBox="0 0 760 296" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A 90-day Active Directory hardening sequence. Days 0 to 15 establish control-plane inventory and emergency access. Days 15 to 35 isolate privileged administration. Days 35 to 60 remove durable privilege paths and secure service identities. Days 60 to 80 enforce authentication protocols. Days 80 to 90 rehearse forest recovery and retest the attack graph.">
<defs><marker id="seq-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<line x1="38" y1="74" x2="724" y2="74" class="dg-axis" marker-end="url(#seq-a)"/>
<circle cx="48" cy="74" r="9" class="dg-dot-accent"/><circle cx="184" cy="74" r="9" class="dg-dot"/><circle cx="330" cy="74" r="9" class="dg-dot"/><circle cx="482" cy="74" r="9" class="dg-dot"/><circle cx="622" cy="74" r="9" class="dg-dot"/><circle cx="714" cy="74" r="9" class="dg-dot-accent"/>
<text x="34" y="45" class="dg-mono">D0</text><text x="164" y="45" class="dg-mono">D15</text><text x="306" y="45" class="dg-mono">D35</text><text x="458" y="45" class="dg-mono">D60</text><text x="598" y="45" class="dg-mono">D80</text><text x="688" y="45" class="dg-mono">D90</text>
<rect x="24" y="108" width="132" height="116" rx="9" class="dg-box-accent"/><text x="38" y="134" class="dg-label">Map control</text><text x="38" y="154" class="dg-label">plane</text><text x="38" y="180" class="dg-muted">owners</text><text x="38" y="198" class="dg-muted">break glass</text>
<rect x="170" y="108" width="132" height="116" rx="9" class="dg-box"/><text x="184" y="134" class="dg-label">Isolate admin</text><text x="184" y="154" class="dg-label">logons</text><text x="184" y="180" class="dg-muted">separate IDs</text><text x="184" y="198" class="dg-muted">PAW + deny</text>
<rect x="316" y="108" width="132" height="116" rx="9" class="dg-box"/><text x="330" y="134" class="dg-label">Remove paths</text><text x="330" y="180" class="dg-muted">ACL · GPO</text><text x="330" y="198" class="dg-muted">gMSA · AD CS</text>
<rect x="462" y="108" width="132" height="116" rx="9" class="dg-box"/><text x="476" y="134" class="dg-label">Enforce</text><text x="476" y="154" class="dg-label">protocols</text><text x="476" y="180" class="dg-muted">LDAP · NTLM</text><text x="476" y="198" class="dg-muted">negative tests</text>
<rect x="608" y="108" width="132" height="116" rx="9" class="dg-box-accent"/><text x="622" y="134" class="dg-label">Recover and</text><text x="622" y="154" class="dg-label">retest</text><text x="622" y="180" class="dg-muted">forest drill</text><text x="622" y="198" class="dg-muted">rebuild graph</text>
<text x="24" y="268" class="dg-muted">Each phase ends with a failed attack path or a successful recovery action—not only a completed change ticket.</text>
</svg>
<figcaption>The sequence front-loads control-plane isolation, then removes durable permissions, migrates protocols with compatibility evidence, and closes with recovery and attack-path retesting.</figcaption>
</figure>

**Days 0–15: establish control.** Inventory the real control plane, name owners, reduce emergency
access, secure recovery credentials, and collect a baseline with a normal domain account. Tools may
include PingCastle, BloodHound, native exports, and Certipy, but their output remains a list of
candidates.

**Days 15–35: isolate privileged administration.** Separate accounts, define PAWs or hardened jump
paths, restrict privileged logons to approved systems, remove daily-use activity from admin
identities, and test the deny paths.

**Days 35–60: remove durable escalation.** Review nested groups, ACLs, GPO control, delegation,
LAPS and gMSA password readers, service identities, and AD CS administration. Fix the permission
that creates the path, then recollect the graph.

**Days 60–80: enforce authentication controls.** Use audit data to migrate LDAP, NTLM, SMB, and
web-authentication dependencies. Pilot, enforce, and preserve rejection evidence.

**Days 80–90: recover and retest.** Run a bounded forest recovery exercise, rebuild the attack-path
graph, repeat negative controls, and close only the findings whose original path no longer works.

## What can break—and what to measure first

Offensive hardening is not reckless enforcement. A control that breaks identity-dependent business
services will be rolled back under pressure, often without a second attempt. Measure the dependency,
pilot the change, preserve a rollback path, and define the negative security test before the window.

| Change | Likely dependency | Measure before enforcement | Security pass condition |
| --- | --- | --- | --- |
| Require LDAP signing | Legacy Java appliances, scripts, simple binds | Client IP, identity, bind type, owner, and frequency from Directory Service events | Approved signed clients work; unsigned bind receives a documented rejection |
| Enforce LDAP channel binding | Older LDAPS clients and TLS intermediaries | `3039–3041` event messages, TLS termination path, client support | Valid CBT succeeds; missing or mismatched CBT is rejected according to policy |
| Restrict NTLM | Appliances, old applications, local-account workflows | Source, target, account, protocol, and business owner for each NTLM use | Kerberos/modern path succeeds; expired exception path fails |
| Introduce tiered logon restrictions | Shared administration habits and legacy jump servers | Fourteen to thirty days of privileged logon source/destination evidence | Approved PAW route succeeds; higher-tier identity is denied below its tier |
| Migrate a service to gMSA | Application startup, cluster, SPN, and encryption support | Test instance, service dependencies, allowed retrieval hosts, AES support | Service starts and authenticates; unauthorized host cannot use the gMSA |
| Narrow LAPS readers | Helpdesk and break-fix procedures | Current readers, actual retrieval events, device ownership, emergency route | Approved support retrieval works; unrelated principal cannot read the secret |
| Harden AD CS enrollment | Auto-enrollment, NDES/SCEP, web enrollment, certificate mapping | Published templates, enrollment volume, endpoint protocols, mapping exceptions | Approved enrollment succeeds; unauthorized identity shape is denied without issuance |

## Evidence matrix

| Hardening claim | Evidence I require | Negative control | What is not enough |
| --- | --- | --- | --- |
| Tier 0 is isolated | Complete control-plane inventory, approved admin paths, logon restrictions, and observed Tier 0 sessions only on approved hosts | Attempt the same Tier 0 logon from a lower-tier test system and require failure | A diagram showing three tiers |
| Standing privilege is reduced | Effective membership and ACL evidence before and after the change, plus a recollected attack graph | Re-run the original path from the same low-privilege principal | Removing one user from one screenshot |
| LAPS is effective | Unique rotating local passwords, encrypted storage where applicable, narrow read rights, and retrieval audit events | Use one managed credential against a second host and require failure | Installing the LAPS client or creating a GPO |
| Service identities are hardened | gMSA migration or tested rotation, narrow password-retrieval rights, AES support, denied interactive logon, and application-owner acceptance | Remove an unauthorized retrieval host and confirm it can no longer use the account | A long password with no owner |
| LDAP and relay paths are closed | Audit-to-enforcement record, signed binds, channel-binding policy, compatible client success, and rejected relay/unsigned bind evidence | Send a controlled unsigned or unbound authentication and require rejection | Registry or GPO screenshots alone |
| AD CS cannot mint unintended identities | CA and template ACL review, enrollment constraints, authentication mapping state, endpoint protection, and independent path analysis | Attempt an unauthorized enrollment shape and require denial without issuing a usable certificate | “Certipy found no ESC1” without ACL review |
| Forest recovery is viable | Trusted backup selection, offline credentials, clean-room runbook, timed exercise evidence, and documented failures | Rehearse without relying on the production forest or its management plane | A successful backup job |

## What the final hardening record should contain

Do not hand defenders a score and a hundred screenshots. For every accepted issue, record:

- the exact principal, permission, system, protocol, or configuration that creates the risk;
- the complete path to a protected identity or business asset;
- effective permission and reachability evidence, not only a scanner label;
- a named remediation owner and target state;
- dependencies and approved temporary exceptions;
- the positive test showing normal operation still works;
- the negative test showing the dangerous path now fails;
- the date and evidence for retest.

I added a reusable [offensive AD hardening review register](/downloads/ad-hardening-offensive-review.csv)
with those fields. It is intentionally a decision record rather than a vulnerability checklist:
each row starts from an attacker hypothesis and ends with positive and negative retest evidence.

This prevents two common failures. The first is declaring every tool signal a finding. A BloodHound
edge, Certipy label, or health-check score is a candidate until effective rights, reachability, and
impact are confirmed. The second is closing a finding because a setting changed even though the
original path was never repeated.

## My conclusion

The best Active Directory hardening programmes are not the ones that change the most settings.
They are the ones that can show, in order, which path existed, which durable edge created it, what
changed, what legitimate behavior survived, and why the same path now fails.

Start with the systems that can control identity. Isolate the credentials that administer them.
Remove old permissions before polishing policy. Treat service accounts and certificates as
authentication paths. Enforce protocols with audit evidence. Then prove that the directory can be
recovered without trusting the environment that just failed.

A score can summarize that work. It cannot replace it.

## MITRE ATT&CK mapping

| Tactic | Technique ID | Technique name | Defender verification signal |
| --- | --- | --- | --- |
| Reconnaissance | `T1087.002` | Account Discovery: Domain Account | Audit LDAP search spikes via Event ID 1644 / Directory Service log |
| Credential Access | `T1558.003` | Steal or Forge Kerberos Tickets: Kerberoasting | Event ID 4769 with Ticket Options 0x40810000 and Encryption Type 0x17 |
| Privilege Escalation | `T1484.001` | Group Policy Modification | Audit ACL modifications on `CN=Policies,CN=System` in Directory Service |
| Lateral Movement | `T1550.002` | Use Alternate Authentication Material: Pass the Hash | Event ID 4624 (Logon Type 3) using NTLM instead of Kerberos |

## Defender action checklist

Use this actionable checklist during Active Directory hardening rollouts:

1. **Tier 0 isolation:** Configure User Rights Assignment in GPO to deny Domain Admins from logging on locally or via RDP to Tier 1/2 systems.
2. **Enforce AES-256 for Kerberos:** Disable DES and RC4-HMAC encryption types for Kerberos via Group Policy (`Network security: Configure encryption types allowed for Kerberos`).
3. **Protect high-privilege principals:** Add Tier 0 administrative accounts to the `Protected Users` security group to prevent credential caching.
4. **Require LDAP signing & channel binding:** Set `LDAPServerIntegrity = 2` on Domain Controllers and enforce Channel Binding Tokens (CBT).
5. **Deploy Windows LAPS:** Enforce rotating random local administrator passwords managed directly through Active Directory.

## Sources reviewed

- [Microsoft: AD DS tier model](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/tier-model)
- [Microsoft: privileged access overview](https://learn.microsoft.com/en-us/security/zero-trust/security-concept-privileged-access)
- [Microsoft: Windows LAPS overview](https://learn.microsoft.com/en-us/windows-server/identity/laps/laps-overview)
- [Microsoft: Windows LAPS with Windows Server Active Directory](https://learn.microsoft.com/en-us/windows-server/identity/laps/laps-scenarios-windows-server-active-directory)
- [Microsoft: group Managed Service Accounts overview](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/group-managed-service-accounts/group-managed-service-accounts/group-managed-service-accounts-overview)
- [Microsoft: LDAP signing and channel binding](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/ldap-signing)
- [Microsoft: Active Directory forest recovery](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/forest-recovery-guide/ad-forest-recovery-determine-how-to-recover)
- [Microsoft: certificate template concepts](https://learn.microsoft.com/en-us/windows-server/identity/ad-cs/certificate-template-concepts)
- [Microsoft: Get-ADGroupMember](https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-adgroupmember)
- [Certipy v5 command reference](https://github.com/ly4k/Certipy/wiki/08-%E2%80%90-Command-Reference)
- [BloodHound Cypher search documentation](https://bloodhound.specterops.io/analyze-data/explore/cypher-search)