AD Group Policy Inheritance and Precedence
AD Group Policy Inheritance and Precedence
Several GPOs can appear in a member server’s applied list while only one supplies a particular registry value. A child OU winning a conflict does not mean the parent GPO disappeared. Conversely, a successful policy refresh does not tell us whether an inherited GPO was excluded.
The experiment used LAB-DC1 and LAB-MEMBER, both Windows Server 2022 Datacenter, in the isolated corp.test.checo.cc domain. The DC used Windows PowerShell 5.1.20348.5386; the member reported Windows 10.0.20348. Their IPv4 addresses were 10.77.77.10 and 10.77.77.20, with IPv6 ULA also enabled. DC configuration and member computer-policy refreshes ran as SYSTEM in their respective machines.
The member computer was temporarily moved beneath this OU structure and returned to its original container afterward:
DC=corp,DC=test,DC=checo,DC=cc
OU=KB-Lab-20261009
OU=Parent <- KB-GPO-221-Parent
OU=Child <- KB-GPO-221-ChildA / ChildB
LAB-MEMBERAll three GPOs set HKLM\Software\Policies\KB-GPO-221\Winner to their own name: Parent, ChildA, or ChildB. The parent additionally set ParentOnly=InheritedParent. The dedicated OUs, GPOs, and marker key did not exist at baseline. No default GPO was edited.
The second value matters: Winner identifies the surviving conflict value, while ParentOnly tells us whether a nonconflicting parent setting remains.
1. Compare final values
Four experimental phases covered six link configurations. Each phase started and ended with no experimental links, no inheritance block, and no marker key, followed by refresh and readback.
| Configuration | Winner | ParentOnly | Observation |
|---|---|---|---|
| Ordinary parent and child links | ChildA | InheritedParent | Child wins the conflict; the other parent setting remains |
| ChildA order 1, ChildB order 2 | ChildA | InheritedParent | Lower link-order number has precedence |
| Move ChildB to order 1 | ChildB | InheritedParent | Reversing order changes the winner |
| Block inheritance on Child; ordinary parent link | ChildA | Absent | Direct child link remains; inherited parent settings leave scope |
| Enforce the parent link; normal Child inheritance | Parent | InheritedParent | Enforced parent value wins |
| Enforce parent and block Child inheritance | Parent | InheritedParent | The enforced link crosses the inheritance block |
Ordinary inheritance produced:
{
"Winner": "ChildA",
"ParentOnly": "InheritedParent",
"SecureChannel": true,
"MarkerKeyExists": true
}The parent GPO had not failed wholesale. Its conflicting value lost, but its separate setting still applied. For ordinary processing, local, site, domain, parent OU, and child OU form the familiar LSDOU sequence. This experiment directly changed OU links, not local or site policies. Within one container, order 1 has precedence over order 2. Microsoft documents both the hierarchy and the lower-numbered link’s precedence in Group Policy processing and Set-GPLink.
2. Block inheritance and Enforced
Blocking inheritance on Child kept its directly linked ChildA. The inherited parent and Default Domain Policy appeared as not applied:
Applied Group Policy Objects
-----------------------------
KB-GPO-221-ChildA
The following GPOs were not applied because they were filtered out
-------------------------------------------------------------------
KB-GPO-221-Parent
Filtering: Not Applied (Unknown Reason)
Default Domain Policy
Filtering: Not Applied (Unknown Reason)Unknown Reason is the tool’s recorded text, not proof of security filtering. Here, the DC-side inheritance setting and the missing ParentOnly value established the relevant change. The policy refresh itself succeeded.
With the parent link enforced, Winner=Parent both before and after enabling the child inheritance block. This is a hierarchy-dependent link property, not a universal priority switch detached from link location. Multiple enforced ancestors, security filtering, and WMI conditions were not tested here.
3. Prepare the lab
Run the following on the lab DC with the ActiveDirectory and GroupPolicy modules. Resolve the intended domain and computer before making changes. Keep the same session, or explicitly preserve the variables and created-object GUIDs between steps.
$ErrorActionPreference = 'Stop'
Import-Module ActiveDirectory
Import-Module GroupPolicy
$dc = 'LAB-DC1.corp.test.checo.cc'
$domain = 'corp.test.checo.cc'
$d = Get-ADDomain -Server $dc
$d | Select-Object DNSRoot,DomainSID
$base = $d.DistinguishedName
$root = 'OU=KB-Lab-20261009,' + $base
$parent = 'OU=Parent,' + $root
$child = 'OU=Child,' + $parent
$c = Get-ADComputer LAB-MEMBER -Server $dc
$c | Select-Object DistinguishedName,ObjectGUID
$originalDN = $c.DistinguishedName
$computerGUID = $c.ObjectGUID
$backup = 'C:\KB-GPO-221\initial-backup'
New-Item -ItemType Directory -Path $backup -Force | Out-Null
Backup-GPO -All -Path $backup -Domain $domain -Server $dc
Get-ADOrganizationalUnit -Filter * -Server $dc -Properties gPLink,gPOptions |
Select-Object DistinguishedName,ObjectGUID,gPLink,gPOptionsThe recorded domain SID was S-1-5-21-3628476133-752301219-3504167884. LAB-MEMBER’s object GUID was 27512149-7cc9-4b2e-8297-be529978e6c3; its initial DN was CN=LAB-MEMBER,CN=Computers,DC=corp,DC=test,DC=checo,DC=cc. These identify the historical lab, not values to substitute into another domain.
On the member, check whether the marker already exists:
Test-Path 'HKLM:\Software\Policies\KB-GPO-221'It did not exist in this lab. Choose a different dedicated key if one already exists. Recovery must restore the absence of policy markers, not merely delete them manually before checking.
Back up link targets, link order, enabled/enforced flags, and container inheritance separately from GPO contents. A GPO backup does not recreate its OU links. Plan cleanup before creating objects: remove experimental links and inheritance changes, refresh and verify marker removal, move the computer back, and only then delete the dedicated GPOs and empty OUs.
4. Test links and inheritance
The following creates only dedicated objects beneath the lab root. Store the returned GPO GUIDs; no link is added to the domain root or Domain Controllers OU.
New-ADOrganizationalUnit -Name 'KB-Lab-20261009' -Path $base -ProtectedFromAccidentalDeletion $false -Server $dc
New-ADOrganizationalUnit -Name 'Parent' -Path $root -ProtectedFromAccidentalDeletion $false -Server $dc
New-ADOrganizationalUnit -Name 'Child' -Path $parent -ProtectedFromAccidentalDeletion $false -Server $dc
$ids = @{}
foreach ($name in @('Parent','ChildA','ChildB')) {
$g = New-GPO -Name ('KB-GPO-221-' + $name) -Domain $domain -Server $dc
$ids[$name] = $g.Id
Set-GPRegistryValue -Guid $g.Id -Key 'HKLM\Software\Policies\KB-GPO-221' -ValueName 'Winner' -Type String -Value $name -Domain $domain -Server $dc
}
Set-GPRegistryValue -Guid $ids.Parent -Key 'HKLM\Software\Policies\KB-GPO-221' -ValueName 'ParentOnly' -Type String -Value 'InheritedParent' -Domain $domain -Server $dc
Move-ADObject -Identity $computerGUID -TargetPath $child -Server $dcThe strings are harmless policy markers. They do not control a service or security feature.
For the first phase, link Parent and ChildA normally:
New-GPLink -Guid $ids.Parent -Target $parent -LinkEnabled Yes -Enforced No -Order 1 -Domain $domain -Server $dc
New-GPLink -Guid $ids.ChildA -Target $child -LinkEnabled Yes -Enforced No -Order 1 -Domain $domain -Server $dc
Get-GPInheritance -Target $child -Domain $domain -Server $dcAfter each change, use the same elevated, local member-side observation:
gpupdate /target:computer /force /wait:120
if ($LASTEXITCODE -ne 0) { throw 'gpupdate failed' }
gpresult /scope computer /r
if ($LASTEXITCODE -ne 0) { throw 'gpresult failed' }
Get-ItemProperty 'HKLM:\Software\Policies\KB-GPO-221' |
Select-Object Winner,ParentOnlyThe recorded refresh completed successfully:
Updating policy...
Computer Policy update has completed successfully.The first result was ChildA plus InheritedParent. Reset the experimental links before beginning the next phase.
For the second phase, recreate those links, add ChildB at order 2, refresh and record ChildA, then move ChildB to order 1 and refresh again:
New-GPLink -Guid $ids.ChildB -Target $child -LinkEnabled Yes -Enforced No -Order 2 -Domain $domain -Server $dc
Set-GPLink -Guid $ids.ChildB -Target $child -Order 1 -Domain $domain -Server $dcThose two commands represent separate observations. Do not run both without the intervening readback: that would discard the control showing ChildB initially losing. The second readback was ChildB. Reset again afterward.
In phase three, recreate the ordinary Parent and ChildA links, confirm the normal baseline, then block Child inheritance:
Set-GPInheritance -Target $child -IsBlocked Yes -Domain $domain -Server $dc
Get-GPInheritance -Target $child -Domain $domain -Server $dcChildA remained and ParentOnly disappeared. Remove the block and links, refresh, and confirm both experimental values leave scope.
In phase four, recreate ordinary links, enforce Parent, refresh and read both values, then block Child inheritance and repeat:
Set-GPLink -Guid $ids.Parent -Target $parent -Enforced Yes -Domain $domain -Server $dc
Set-GPInheritance -Target $child -IsBlocked Yes -Domain $domain -Server $dcBoth observations returned Parent and InheritedParent. The useful evidence chain is DC link configuration, member RSoP, and the actual target values. If they disagree, verify object DN, the contacted DC, filtering output, and AD/SYSVOL policy versions before changing link order again.
5. Verify cleanup
Only enumerate the dedicated lab OUs. Reject any unexpected link GUID before removing anything:
foreach ($ou in @($root,$parent,$child)) {
$links = @((Get-GPInheritance -Target $ou -Domain $domain -Server $dc).GpoLinks)
foreach ($link in $links) {
if ($link.GpoId -notin @($ids.Values)) { throw 'Unexpected link' }
Remove-GPLink -Guid $link.GpoId -Target $ou -Domain $domain -Server $dc
}
Set-GPInheritance -Target $ou -IsBlocked No -Domain $domain -Server $dc
}Refresh on the member and read the key again. Do not manually remove it to hide a policy-cleanup failure. Every phase’s reset recorded:
{
"Winner": "ABSENT",
"ParentOnly": "ABSENT",
"SecureChannel": true,
"MarkerKeyExists": false
}For final cleanup, move the computer to its recorded original container. This specific lab started in CN=Computers; use the actual saved parent in a different lab. Then delete only the created GPO GUIDs and the now-empty OUs, from deepest to shallowest:
Move-ADObject -Identity $computerGUID -TargetPath ('CN=Computers,' + $base) -Server $dc
Get-ADComputer -Identity $computerGUID -Server $dc |
Select-Object DistinguishedName,ObjectGUID
foreach ($id in $ids.Values) {
Remove-GPO -Guid $id -Domain $domain -Server $dc -Confirm:$false
}
foreach ($dn in @($child,$parent,$root)) {
$items = @(Get-ADObject -SearchBase $dn -SearchScope OneLevel -Filter * -Server $dc)
if ($items.Count -ne 0) { throw 'OU not empty' }
$ou = Get-ADOrganizationalUnit -Identity $dn -Server $dc
Remove-ADOrganizationalUnit -Identity $ou.ObjectGUID -Server $dc -Confirm:$false
}The emptiness check is deliberate. Recursive deletion would make a mistaken target much harder to notice. Keep the backup, link snapshots, refresh output, and registry readbacks after removing the disposable lab objects.
